Compare commits

..

37 Commits

Author SHA1 Message Date
Jessica Chowdhury
97dc651e97 merge conflicts 2024-03-19 13:42:17 +00:00
Jessica Chowdhury
58aa2e8709 merge conflicts 2024-03-19 13:39:30 +00:00
Alessio Gravili
ebd521d0b1 fix: browser console warning: Skipping auto-scroll behavior due to position: sticky or position: fixed on element 2024-03-19 13:39:04 +00:00
Elliot DeNolf
8409975dd3 test: fix seed helper, was causing errors in versions suite 2024-03-19 13:39:04 +00:00
Elliot DeNolf
ab3547b707 test: fix useField imports 2024-03-19 13:39:03 +00:00
PatrikKozak
9f5fcd1746 test: updates text field config imports to relative imports 2024-03-19 13:39:03 +00:00
Paul Popus
78c0f16871 fix: further fixes for live preview 2024-03-19 13:39:03 +00:00
Elliot DeNolf
b53573238b ci: explicitly install playwright 2024-03-19 13:39:03 +00:00
Elliot DeNolf
32938ffd21 test: fix mongodb destroy 2024-03-19 13:39:03 +00:00
Alessio Gravili
8e2fa91f49 fix: document permissions not working for Document Drawers 2024-03-19 13:39:03 +00:00
Paul Popus
394db778dc fix: core fixes to the live-preview e2e test suite 2024-03-19 13:39:03 +00:00
Jessica Chowdhury
035b071a38 merge conflicts 2024-03-19 13:38:18 +00:00
Patrik
e26e1f434f test: passing text field suite (#5341) 2024-03-19 13:38:05 +00:00
Alessio Gravili
0607162f31 chore: e2e tests: get nav-toggler helpers to work on all screen sizes 2024-03-19 13:38:05 +00:00
Elliot DeNolf
6c01f1e300 chore: add jest runner to extensions 2024-03-19 13:38:05 +00:00
Elliot DeNolf
19594c7339 chore: add proper playwright ext to extensions.json 2024-03-19 13:38:05 +00:00
Elliot DeNolf
fa101ecd02 test: fix custom-graphql beforeAll 2024-03-19 13:38:05 +00:00
Alessio Gravili
4a819ea16f chore: keep sync and ui next versions in sync. Only the monorepo next version has to be different 2024-03-19 13:38:05 +00:00
Alessio Gravili
e696579b33 chore: fix playwright by making sure ui uses a different next version than anything else 2024-03-19 13:38:05 +00:00
Alessio Gravili
edc785e639 fix: properly type getPreferences and update richtext-lexical's block collapsed handling to match that type 2024-03-19 13:38:05 +00:00
Alessio Gravili
faee408001 chore: fix ts-eslint not being able to work with types imported across packages 2024-03-19 13:38:05 +00:00
Elliot DeNolf
01fc1e5914 chore(richtext-lexical): do not pass DefaultCell 2024-03-19 13:38:05 +00:00
Elliot DeNolf
ddeb3bf842 revert(db-mongodb): add back 'as any' removed during lint 2024-03-19 13:38:05 +00:00
Paul Popus
f409ae56e3 fix: not returning not found when fetching docPermissions when id doesnt exist 2024-03-19 13:38:05 +00:00
Paul Popus
361924a4d8 fix: add conditional for getCustomViewByRoute 2024-03-19 13:38:05 +00:00
Alessio Gravili
b4e25337dc fix(eslint-config-payload): get eslint-config-prettier to work properly by moving it to the end of the extends array 2024-03-19 13:38:04 +00:00
Paul Popus
fca6e7bab9 chore: track website media pictures 2024-03-19 13:38:04 +00:00
James
7507951900 chore: optimizes buildFormState by passing prefs from admin 2024-03-19 13:38:04 +00:00
Alessio Gravili
857331cab1 chore: add lint & prettier commit to .git-blame-ignore-revs 2024-03-19 13:38:04 +00:00
Alessio Gravili
afe9ed60f9 chore: run lint & prettier on everything 2024-03-19 13:38:04 +00:00
Alessio Gravili
3840120e19 chore: more eslint rule improvements 2024-03-19 13:38:04 +00:00
Alessio Gravili
cc611cb553 chore: remove unused eslint-plugin-import 2024-03-19 13:38:04 +00:00
Alessio Gravili
a4f56c886d chore: eslint: turn off react/jsx-one-expression-per-line rule which is conflicting with prettier 2024-03-19 13:38:04 +00:00
Alessio Gravili
612115a3c9 chore: add @typescript-eslint/no-unnecessary-type-constraint rule to warn-only 2024-03-19 13:38:03 +00:00
Alessio Gravili
63da5eaffc chore: add missing .prettierignore files. Without them, folders like "dist" would not be excluded on a package-level despite a root-level .prettierignore being present 2024-03-19 13:38:03 +00:00
Alessio Gravili
fb4ee0493e chore: improve pre-defined eslint ignorePatterns 2024-03-19 13:38:03 +00:00
Jessica Chowdhury
4d39381235 fix: missing error states 2024-03-15 12:53:30 +00:00
1146 changed files with 10144 additions and 12245 deletions

View File

@@ -10,4 +10,3 @@
**/temp
playwright.config.ts
jest.config.js
test/live-preview/next-app

View File

@@ -3,13 +3,6 @@ module.exports = {
extends: ['@payloadcms'],
ignorePatterns: ['README.md', 'packages/**/*.spec.ts'],
overrides: [
{
files: ['packages/**'],
plugins: ['payload'],
rules: {
'payload/no-jsx-import-statements': 'warn',
},
},
{
files: ['scripts/**'],
rules: {

13
.github/CODEOWNERS vendored
View File

@@ -1,24 +1,32 @@
# Order matters. The last matching pattern takes precedence.
### Catch-all ###
* @denolfe @jmikrut @DanRibbens
.* @denolfe @jmikrut @DanRibbens
### Core ###
/packages/payload/ @denolfe @jmikrut @DanRibbens
/packages/payload/src/uploads/ @denolfe
/packages/payload/src/admin/ @jmikrut @jacobsfletch @JarrodMFlesch
### Adapters ###
/packages/bundler-*/ @denolfe @jmikrut @DanRibbens @JarrodMFlesch
/packages/db-*/ @denolfe @jmikrut @DanRibbens
/packages/richtext-*/ @denolfe @jmikrut @DanRibbens @AlessioGr
### Plugins ###
/packages/plugin-*/ @denolfe @jmikrut @DanRibbens
/packages/plugin-*/ @denolfe @jmikrut @DanRibbens @jacobsfletch @JarrodMFlesch @AlessioGr
/packages/plugin-cloud*/ @denolfe
/packages/plugin-form-builder/ @jacobsfletch
/packages/plugin-live-preview*/ @jacobsfletch
/packages/plugin-nested-docs/ @jacobsfletch
/packages/plugin-password-protection/ @jmikrut
/packages/plugin-redirects/ @jacobsfletch
/packages/plugin-search/ @jacobsfletch
/packages/plugin-sentry/ @JessChowdhury
/packages/plugin-seo/ @jacobsfletch
/packages/plugin-stripe/ @jacobsfletch
/packages/plugin-zapier/ @JarrodMFlesch
### Examples ###
/examples/ @jacobsfletch
@@ -27,7 +35,8 @@
/examples/whitelabel/ @JessChowdhury
### Templates ###
/templates/ @jacobsfletch @denolfe
/templates/ @jacobsfletch
/templates/blank/ @denolfe
### Misc ###
/packages/create-payload-app/ @denolfe

View File

@@ -2,7 +2,7 @@ name: build
on:
pull_request:
types: [opened, reopened, synchronize]
types: [ opened, reopened, synchronize ]
push:
branches: ['main', 'alpha']
@@ -18,7 +18,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 25
- uses: dorny/paths-filter@v3
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
@@ -46,12 +46,12 @@ jobs:
fetch-depth: 25
- name: Use Node.js 18
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v3
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
@@ -61,7 +61,7 @@ jobs:
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
@@ -74,7 +74,7 @@ jobs:
- run: pnpm run build:core
- name: Cache build
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -90,12 +90,12 @@ jobs:
fetch-depth: 25
- name: Use Node.js 18
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v3
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
@@ -105,7 +105,7 @@ jobs:
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
- uses: actions/cache@v3
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
@@ -123,12 +123,7 @@ jobs:
strategy:
fail-fast: false
matrix:
database:
- mongodb
- postgres
- postgres-custom-schema
- postgres-uuid
- supabase
database: [mongoose, postgres, postgres-custom-schema, postgres-uuid, supabase]
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
@@ -140,18 +135,18 @@ jobs:
steps:
- name: Use Node.js 18
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v3
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -162,7 +157,7 @@ jobs:
- name: Start PostgreSQL
uses: CasperWA/postgresql-action@v1.2
with:
postgresql version: '14' # See https://hub.docker.com/_/postgres for available versions
postgresql version: '14' # See https://hub.docker.com/_/postgres for available versions
postgresql db: ${{ env.POSTGRES_DB }}
postgresql user: ${{ env.POSTGRES_USER }}
postgresql password: ${{ env.POSTGRES_PASSWORD }}
@@ -202,7 +197,7 @@ jobs:
if: matrix.database == 'supabase'
- name: Integration Tests
run: pnpm test:int --testPathIgnorePatterns=test/fields # Ignore fields tests until reworked
run: pnpm test:int
env:
NODE_OPTIONS: --max-old-space-size=8096
PAYLOAD_DATABASE: ${{ matrix.database }}
@@ -214,38 +209,22 @@ jobs:
strategy:
fail-fast: false
matrix:
# find test -type f -name 'e2e.spec.ts' | sort | xargs dirname | xargs -I {} basename {}
suite:
- _community
- access-control
# - admin
- auth
# - field-error-states
# - fields-relationship
# - fields
- fields/lexical
- live-preview
# - localization
# - plugin-nested-docs
# - plugin-seo
# - refresh-permissions
# - uploads
# - versions
part: [ 1/8, 2/8, 3/8, 4/8, 5/8, 6/8, 7/8, 8/8 ]
steps:
- name: Use Node.js 18
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v3
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -254,39 +233,38 @@ jobs:
run: pnpm exec playwright install
- name: E2E Tests
uses: nick-fields/retry@v3
uses: nick-fields/retry@v2
with:
retry_on: error
max_attempts: 2
timeout_minutes: 15
command: pnpm test:e2e ${{ matrix.suite }}
command: pnpm test:e2e --part ${{ matrix.part }} --bail
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v3
if: always()
with:
name: test-results
path: test/test-results/
path: test-results/
retention-days: 1
tests-type-generation:
if: false # This should be replaced with gen on a real Payload project
runs-on: ubuntu-latest
needs: core-build
steps:
- name: Use Node.js 18
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v3
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -315,18 +293,18 @@ jobs:
steps:
- name: Use Node.js 18
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install pnpm
uses: pnpm/action-setup@v3
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false
- name: Restore build
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}
@@ -336,15 +314,16 @@ jobs:
- name: Test ${{ matrix.pkg }}
run: pnpm --filter ${{ matrix.pkg }} run test
if: matrix.pkg != 'create-payload-app' # degit doesn't work within GitHub Actions
templates:
needs: changes
if: false # Disable until templates are updated for 3.0
if: ${{ needs.changes.outputs.templates == 'true' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
template: [blank, website, ecommerce]
template: [ blank, website, ecommerce ]
steps:
- uses: actions/checkout@v4
@@ -352,7 +331,7 @@ jobs:
fetch-depth: 25
- name: Use Node.js 18
uses: actions/setup-node@v4
uses: actions/setup-node@v3
with:
node-version: 18

16
.release-it.pre.js Normal file
View File

@@ -0,0 +1,16 @@
module.exports = {
verbose: true,
git: {
requireCleanWorkingDir: false,
commit: false,
push: false,
tag: false,
},
npm: {
skipChecks: true,
tag: 'beta',
},
hooks: {
'before:init': ['pnpm install', 'pnpm clean', 'pnpm build'],
},
}

9
.vscode/launch.json vendored
View File

@@ -10,19 +10,12 @@
"cwd": "${workspaceFolder}"
},
{
"command": "node --no-deprecation test/dev.js fields",
"command": "pnpm run dev _community -- --no-turbo",
"cwd": "${workspaceFolder}",
"name": "Run Dev Community",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm run dev live-preview -- --no-turbo",
"cwd": "${workspaceFolder}",
"name": "Run Dev Live Preview",
"request": "launch",
"type": "node-terminal"
},
{
"command": "pnpm run dev plugin-cloud-storage",
"cwd": "${workspaceFolder}",

View File

@@ -1,3 +0,0 @@
import PageTemplate from './(pages)/[slug]/page.js'
export default PageTemplate

View File

@@ -167,6 +167,53 @@ those three fields plus the ID field.
so your admin queries can remain performant.
</Banner>
### Admin Hooks
In addition to collection hooks themselves, Payload provides for admin UI-specific hooks that you can leverage.
**`beforeDuplicate`**
The `beforeDuplicate` hook is an async function that accepts an object containing the data to duplicate, as well as the
locale of the doc to duplicate. Within this hook, you can modify the data to be duplicated, which is useful in cases
where you have unique fields that need to be incremented or similar, as well as if you want to automatically modify a
document's `title`.
Example:
```ts
import { BeforeDuplicate, CollectionConfig } from 'payload/types'
// Your auto-generated Page type
import { Page } from '../payload-types.ts'
const beforeDuplicate: BeforeDuplicate<Page> = ({ data }) => {
return {
...data,
title: `${data.title} Copy`,
uniqueField: data.uniqueField ? `${data.uniqueField}-copy` : '',
}
}
export const Page: CollectionConfig = {
slug: 'pages',
admin: {
hooks: {
beforeDuplicate,
},
},
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'uniqueField',
type: 'text',
unique: true,
},
],
}
```
### TypeScript
You can import collection types as follows:

View File

@@ -21,7 +21,6 @@ functionalities to be easily reusable across your projects.
- [beforeValidate](#beforevalidate)
- [beforeChange](#beforechange)
- beforeDuplicate(#beforeduplicate)
- [afterChange](#afterchange)
- [afterRead](#afterread)
@@ -39,7 +38,6 @@ const ExampleField: Field = {
hooks: {
beforeValidate: [(args) => {...}],
beforeChange: [(args) => {...}],
beforeDuplicate: [(args) => {...}],
afterChange: [(args) => {...}],
afterRead: [(args) => {...}],
}
@@ -219,27 +217,6 @@ Here, the `afterRead` hook for the `dateField` is used to format the date into a
using `toLocaleDateString()`. This hook modifies the way the date is presented to the user, making it more
user-friendly.
### beforeDuplicate
The `beforeDuplicate` field hook is only called when duplicating a document. It may be used when documents having the
exact same properties may cause issue. This gives you a way to avoid duplicate names on `unique`, `required` fields or
to unset values by returning `null`. This is called immediately after `defaultValue` and before validation occurs.
```ts
import { Field } from 'payload/types'
const numberField: Field = {
name: 'number',
type: 'number',
hooks: {
// increment existing value by 1
beforeDuplicate: [({ value }) => {
return (value ?? 0) + 1
}],
}
}
```
## TypeScript
Payload exports a type for field hooks which can be accessed and used as follows:

View File

@@ -32,4 +32,4 @@
"ts-node": "^9.1.1",
"typescript": "^4.8.4"
}
}
}

View File

@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"strict": false,
"esModuleInterop": true,
@@ -12,12 +16,22 @@
"sourceMap": true,
"resolveJsonModule": true,
"paths": {
"payload/generated-types": ["./src/payload-types.ts"],
"node_modules/*": ["./node_modules/*"]
}
"payload/generated-types": [
"./src/payload-types.ts"
],
"node_modules/*": [
"./node_modules/*"
]
},
},
"include": ["src"],
"exclude": ["node_modules", "dist", "build"],
"include": [
"src"
],
"exclude": [
"node_modules",
"dist",
"build",
],
"ts-node": {
"transpileOnly": true
}

View File

@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"outDir": "./dist",
"skipLibCheck": true,
"strict": false,
@@ -14,8 +18,10 @@
"jsx": "preserve",
"sourceMap": true
},
"include": ["src"],
"include": [
"src"
],
"ts-node": {
"transpileOnly": true
}
}
}

View File

@@ -26,4 +26,4 @@
"ts-node": "^9.1.1",
"typescript": "^4.8.4"
}
}
}

View File

@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"strict": false,
"esModuleInterop": true,
@@ -10,8 +14,14 @@
"rootDir": "./src",
"jsx": "react"
},
"include": ["src"],
"exclude": ["node_modules", "dist", "build"],
"include": [
"src",
],
"exclude": [
"node_modules",
"dist",
"build",
],
"ts-node": {
"transpileOnly": true
}

View File

@@ -26,4 +26,4 @@
"ts-node": "^9.1.1",
"typescript": "^4.8.4"
}
}
}

View File

@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"strict": false,
"esModuleInterop": true,
@@ -10,8 +14,14 @@
"rootDir": "./src",
"jsx": "react"
},
"include": ["src"],
"exclude": ["node_modules", "dist", "build"],
"include": [
"src"
],
"exclude": [
"node_modules",
"dist",
"build",
],
"ts-node": {
"transpileOnly": true
}

View File

@@ -1,7 +1,7 @@
/** @type {import('jest').Config} */
const customJestConfig = {
extensionsToTreatAsEsm: ['.ts', '.tsx'],
setupFilesAfterEnv: ['<rootDir>/test/jest.setup.ts'],
globalSetup: './test/jest.setup.ts',
moduleNameMapper: {
'\\.(css|scss)$': '<rootDir>/test/helpers/mocks/emptyModule.js',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
@@ -18,8 +18,4 @@ const customJestConfig = {
verbose: true,
}
if (process.env.CI) {
customJestConfig.reporters = [['github-actions', { silent: false }], 'summary']
}
export default customJestConfig

View File

@@ -19,9 +19,6 @@ export default withBundleAnalyzer(
},
]
},
images: {
domains: ['localhost'],
},
webpack: (webpackConfig) => {
webpackConfig.resolve.extensionAlias = {
'.cjs': ['.cts', '.cjs'],

View File

@@ -1,11 +1,10 @@
{
"name": "payload-monorepo",
"version": "3.0.0-alpha.49",
"version": "3.0.0-alpha.48",
"private": true,
"type": "module",
"workspaces:": [
"packages/*",
"test/*"
"packages/*"
],
"scripts": {
"build": "pnpm run build:core",
@@ -35,15 +34,14 @@
"build:plugin-stripe": "turbo build --filter plugin-stripe",
"build:richtext-lexical": "turbo build --filter richtext-lexical",
"build:richtext-slate": "turbo build --filter richtext-slate",
"build:tests": "pnpm --filter test run typecheck",
"build:translations": "turbo build --filter translations",
"build:ui": "turbo build --filter ui",
"clean": "turbo clean",
"clean:cache": "rimraf node_modules/.cache && rimraf packages/payload/node_modules/.cache && rimraf .next",
"clean:build": "find . \\( -type d \\( -name dist -o -name .cache -o -name .next -o -name .turbo \\) -o -type f -name tsconfig.tsbuildinfo \\) -not -path '*/node_modules/*' -exec rm -rf {} +",
"clean:all": "find . \\( -type d \\( -name node_modules -o -name dist -o -name .cache -o -name .next -o -name .turbo \\) -o -type f -name tsconfig.tsbuildinfo \\) -exec rm -rf {} +",
"dev": "cross-env NODE_OPTIONS=--no-deprecation node ./test/dev.js",
"devsafe": "rimraf .next && pnpm dev",
"dev": "cross-env node --no-deprecation ./test/dev.js",
"devsafe": "rm -rf .next && cross-env node --no-deprecation ./test/dev.js",
"dev:generate-graphql-schema": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/generateGraphQLSchema.ts",
"dev:generate-types": "cross-env NODE_OPTIONS=--no-deprecation tsx ./test/generateTypes.ts",
"dev:postgres": "pnpm --filter payload run dev:postgres",
@@ -58,16 +56,15 @@
"pretest": "pnpm build",
"reinstall": "pnpm clean:all && pnpm install",
"script:list-packages": "tsx ./scripts/list-packages.ts",
"script:pack": "tsx scripts/pack-all-to-dest.ts",
"release:alpha": "tsx ./scripts/release.ts --bump prerelease --tag alpha",
"release:beta": "tsx ./scripts/release.ts --bump prerelease --tag beta",
"test": "pnpm test:int && pnpm test:components && pnpm test:e2e",
"test:components": "cross-env NODE_OPTIONS=--no-deprecation jest --config=jest.components.config.js",
"test:e2e": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 tsx ./test/runE2E.ts",
"test:e2e:debug": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 PWDEBUG=1 DISABLE_LOGGING=true playwright test",
"test:e2e:headed": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 DISABLE_LOGGING=true playwright test --headed",
"test:int:postgres": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 PAYLOAD_DATABASE=postgres DISABLE_LOGGING=true jest --forceExit --detectOpenHandles",
"test:int": "cross-env NODE_OPTIONS=--no-deprecation NODE_NO_WARNINGS=1 DISABLE_LOGGING=true jest --forceExit --detectOpenHandles",
"test:e2e": "NODE_OPTIONS=--no-deprecation tsx ./test/runE2E.ts",
"test:e2e:debug": "cross-env NODE_OPTIONS=--no-deprecation PWDEBUG=1 DISABLE_LOGGING=true playwright test",
"test:e2e:headed": "cross-env NODE_OPTIONS=--no-deprecation DISABLE_LOGGING=true playwright test --headed",
"test:int:postgres": "cross-env NODE_OPTIONS=--no-deprecation PAYLOAD_DATABASE=postgres DISABLE_LOGGING=true jest --forceExit --detectOpenHandles",
"test:int": "cross-env NODE_OPTIONS=--no-deprecation DISABLE_LOGGING=true jest --forceExit --detectOpenHandles",
"translateNewKeys": "pnpm --filter payload run translateNewKeys"
},
"devDependencies": {
@@ -75,7 +72,6 @@
"@next/bundle-analyzer": "^14.1.0",
"@octokit/core": "^5.1.0",
"@payloadcms/eslint-config": "workspace:*",
"@payloadcms/live-preview-react": "workspace:*",
"@playwright/test": "^1.42.1",
"@swc/cli": "^0.1.62",
"@swc/jest": "0.2.36",
@@ -89,7 +85,7 @@
"@types/fs-extra": "^11.0.2",
"@types/jest": "29.5.12",
"@types/minimist": "1.2.2",
"@types/node": "20.11.28",
"@types/node": "20.5.7",
"@types/prompts": "^2.4.5",
"@types/qs": "6.9.7",
"@types/react": "18.2.15",
@@ -109,20 +105,17 @@
"dotenv": "8.6.0",
"drizzle-kit": "0.20.14-1f2c838",
"drizzle-orm": "0.29.4",
"escape-html": "^1.0.3",
"eslint-plugin-payload": "workspace:*",
"execa": "5.1.1",
"form-data": "3.0.1",
"fs-extra": "10.1.0",
"get-port": "5.1.1",
"get-stream": "6.0.1",
"glob": "8.1.0",
"globby": "11.1.0",
"husky": "^8.0.3",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"json5": "^2.2.3",
"jwt-decode": "4.0.0",
"jwt-decode": "3.1.2",
"lexical": "0.13.1",
"lint-staged": "^14.0.1",
"minimist": "1.2.8",
@@ -130,7 +123,6 @@
"next": "14.2.0-canary.22",
"node-mocks-http": "^1.14.1",
"nodemon": "3.0.3",
"open": "^10.1.0",
"pino": "8.15.0",
"pino-pretty": "10.2.0",
"playwright": "^1.42.1",
@@ -153,8 +145,8 @@
"tempy": "^1.0.1",
"ts-node": "10.9.1",
"tsx": "^4.7.1",
"turbo": "^1.13.0",
"typescript": "5.4.2",
"turbo": "^1.12.5",
"typescript": "5.2.2",
"uuid": "^9.0.1",
"yocto-queue": "^1.0.0"
},
@@ -167,7 +159,6 @@
"pnpm": ">=8"
},
"lint-staged": {
"*.{md,mdx,yml,json}": "prettier --write",
"*.{js,jsx,ts,tsx}": [
"prettier --write",
"eslint --cache --fix"

View File

@@ -3,7 +3,7 @@ import baseConfig from '../../jest.config.js'
/** @type {import('@jest/types').Config} */
const customJestConfig = {
...baseConfig,
setupFilesAfterEnv: null,
globalSetup: null,
testMatch: ['**/src/**/?(*.)+(spec|test|it-test).[tj]s?(x)'],
testTimeout: 20000,
}

View File

@@ -7,8 +7,7 @@
"create-payload-app": "bin/cli.js"
},
"scripts": {
"build": "pnpm copyfiles && pnpm typecheck && pnpm build:swc",
"typecheck": "tsc",
"build": "pnpm copyfiles && pnpm build:swc",
"copyfiles": "copyfiles -u 2 \"../../app/(payload)/**\" \"dist\"",
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
"clean": "rimraf {dist,*.tsbuildinfo}",
@@ -28,7 +27,6 @@
"comment-json": "^4.2.3",
"degit": "^2.8.4",
"detect-package-manager": "^3.0.1",
"esprima": "^4.0.1",
"execa": "^5.0.0",
"figures": "^3.2.0",
"fs-extra": "^9.0.1",
@@ -41,17 +39,9 @@
"devDependencies": {
"@types/command-exists": "^1.2.0",
"@types/degit": "^2.8.3",
"@types/esprima": "^4.0.6",
"@types/fs-extra": "^9.0.12",
"@types/jest": "^27.0.3",
"@types/node": "^16.6.2",
"@types/prompts": "^2.4.1"
},
"exports": {
"./commands": {
"import": "./src/lib/init-next.ts",
"require": "./src/lib/init-next.ts",
"types": "./src/lib/init-next.ts"
}
}
}

View File

@@ -1,10 +1,10 @@
import fse from 'fs-extra'
import globby from 'globby'
import path from 'path'
import type { DbDetails } from '../types.js'
import { warning } from '../utils/log.js'
import { dbReplacements } from './packages.js'
import { bundlerPackages, dbPackages, editorPackages } from './packages.js'
/** Update payload config with necessary imports and adapters */
export async function configurePayloadConfig(args: {
@@ -15,10 +15,46 @@ export async function configurePayloadConfig(args: {
return
}
// Update package.json
const packageJsonPath = path.resolve(args.projectDir, 'package.json')
try {
const payloadConfigPath = (
await globby('**/payload.config.ts', { absolute: true, cwd: args.projectDir })
)?.[0]
const packageObj = await fse.readJson(packageJsonPath)
packageObj.dependencies['payload'] = '^2.0.0'
const dbPackage = dbPackages[args.dbDetails.type]
const bundlerPackage = bundlerPackages['webpack']
const editorPackage = editorPackages['slate']
// Delete all other db adapters
Object.values(dbPackages).forEach((p) => {
if (p.packageName !== dbPackage.packageName) {
delete packageObj.dependencies[p.packageName]
}
})
packageObj.dependencies[dbPackage.packageName] = dbPackage.version
packageObj.dependencies[bundlerPackage.packageName] = bundlerPackage.version
packageObj.dependencies[editorPackage.packageName] = editorPackage.version
await fse.writeJson(packageJsonPath, packageObj, { spaces: 2 })
} catch (err: unknown) {
warning('Unable to update name in package.json')
}
try {
const possiblePaths = [
path.resolve(args.projectDir, 'src/payload.config.ts'),
path.resolve(args.projectDir, 'src/payload/payload.config.ts'),
]
let payloadConfigPath: string | undefined
possiblePaths.forEach((p) => {
if (fse.pathExistsSync(p) && !payloadConfigPath) {
payloadConfigPath = p
}
})
if (!payloadConfigPath) {
warning('Unable to update payload.config.ts with plugins')
@@ -28,7 +64,9 @@ export async function configurePayloadConfig(args: {
const configContent = fse.readFileSync(payloadConfigPath, 'utf-8')
const configLines = configContent.split('\n')
const dbReplacement = dbReplacements[args.dbDetails.type]
const dbReplacement = dbPackages[args.dbDetails.type]
const bundlerReplacement = bundlerPackages['webpack']
const editorReplacement = editorPackages['slate']
let dbConfigStartLineIndex: number | undefined
let dbConfigEndLineIndex: number | undefined
@@ -37,6 +75,21 @@ export async function configurePayloadConfig(args: {
if (l.includes('// database-adapter-import')) {
configLines[i] = dbReplacement.importReplacement
}
if (l.includes('// bundler-import')) {
configLines[i] = bundlerReplacement.importReplacement
}
if (l.includes('// bundler-config')) {
configLines[i] = bundlerReplacement.configReplacement
}
if (l.includes('// editor-import')) {
configLines[i] = editorReplacement.importReplacement
}
if (l.includes('// editor-config')) {
configLines[i] = editorReplacement.configReplacement
}
if (l.includes('// database-adapter-config-start')) {
dbConfigStartLineIndex = i

View File

@@ -1,15 +1,12 @@
import fse from 'fs-extra'
import path from 'path'
import type { CliArgs, DbType, ProjectTemplate } from '../types.js'
import type { BundlerType, CliArgs, DbType, ProjectTemplate } from '../types.js'
import { createProject } from './create-project.js'
import { fileURLToPath } from 'node:url'
import { dbReplacements } from './packages.js'
import { bundlerPackages, dbPackages, editorPackages } from './packages.js'
import exp from 'constants'
import { getValidTemplates } from './templates.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
const projectDir = path.resolve(dirname, './tmp')
const projectDir = path.resolve(__dirname, './tmp')
describe('createProject', () => {
beforeAll(() => {
console.log = jest.fn()
@@ -31,11 +28,33 @@ describe('createProject', () => {
const args = {
_: ['project-name'],
'--db': 'mongodb',
'--local-template': 'blank',
'--no-deps': true,
} as CliArgs
const packageManager = 'yarn'
it('creates starter project', async () => {
const projectName = 'starter-project'
const template: ProjectTemplate = {
name: 'blank',
type: 'starter',
url: 'https://github.com/payloadcms/payload/templates/blank',
description: 'Blank Template',
}
await createProject({
cliArgs: args,
projectName,
projectDir,
template,
packageManager,
})
const packageJsonPath = path.resolve(projectDir, 'package.json')
const packageJson = fse.readJsonSync(packageJsonPath)
// Check package name and description
expect(packageJson.name).toEqual(projectName)
})
it('creates plugin template', async () => {
const projectName = 'plugin'
const template: ProjectTemplate = {
@@ -59,34 +78,26 @@ describe('createProject', () => {
expect(packageJson.name).toEqual(projectName)
})
describe('creates project from template', () => {
describe('db adapters and bundlers', () => {
const templates = getValidTemplates()
it.each([
['blank-3.0', 'mongodb'],
['blank-3.0', 'postgres'],
// TODO: Re-enable these once 3.0 is stable and templates updated
// ['website', 'mongodb'],
// ['website', 'postgres'],
// ['ecommerce', 'mongodb'],
// ['ecommerce', 'postgres'],
])('update config and deps: %s, %s', async (templateName, db) => {
['blank', 'mongodb', 'webpack'],
['blank', 'postgres', 'webpack'],
['website', 'mongodb', 'webpack'],
['website', 'postgres', 'webpack'],
['ecommerce', 'mongodb', 'webpack'],
['ecommerce', 'postgres', 'webpack'],
])('update config and deps: %s, %s, %s', async (templateName, db, bundler) => {
const projectName = 'starter-project'
const template = templates.find((t) => t.name === templateName)
const cliArgs = {
...args,
'--db': db,
'--local-template': templateName,
} as CliArgs
await createProject({
cliArgs,
cliArgs: args,
projectName,
projectDir,
template: template as ProjectTemplate,
template,
packageManager,
dbDetails: {
dbUri: `${db}://localhost:27017/create-project-test`,
@@ -94,17 +105,30 @@ describe('createProject', () => {
},
})
const dbReplacement = dbReplacements[db as DbType]
const dbReplacement = dbPackages[db as DbType]
const bundlerReplacement = bundlerPackages[bundler as BundlerType]
const editorReplacement = editorPackages['slate']
const packageJsonPath = path.resolve(projectDir, 'package.json')
const packageJson = fse.readJsonSync(packageJsonPath)
// Check deps
expect(packageJson.dependencies['payload']).toEqual('^2.0.0')
expect(packageJson.dependencies[dbReplacement.packageName]).toEqual(dbReplacement.version)
// Should only have one db adapter
expect(
Object.keys(packageJson.dependencies).filter((n) => n.startsWith('@payloadcms/db-')),
).toHaveLength(1)
let payloadConfigPath = path.resolve(projectDir, 'payload.config.ts')
expect(packageJson.dependencies[bundlerReplacement.packageName]).toEqual(
bundlerReplacement.version,
)
expect(packageJson.dependencies[editorReplacement.packageName]).toEqual(
editorReplacement.version,
)
let payloadConfigPath = path.resolve(projectDir, 'src/payload.config.ts')
// Website and ecommerce templates have payload.config.ts in src/payload
if (!fse.existsSync(payloadConfigPath)) {
@@ -119,6 +143,12 @@ describe('createProject', () => {
expect(content).not.toContain('// database-adapter-config-start')
expect(content).not.toContain('// database-adapter-config-end')
expect(content).toContain(dbReplacement.configReplacement.join('\n'))
expect(content).not.toContain('// bundler-config-import')
expect(content).toContain(bundlerReplacement.importReplacement)
expect(content).not.toContain('// bundler-config')
expect(content).toContain(bundlerReplacement.configReplacement)
})
})
})

View File

@@ -2,18 +2,14 @@ import chalk from 'chalk'
import degit from 'degit'
import execa from 'execa'
import fse from 'fs-extra'
import { fileURLToPath } from 'node:url'
import ora from 'ora'
import path from 'path'
import type { CliArgs, DbDetails, PackageManager, ProjectTemplate } from '../types.js'
import { debug, error, success, warning } from '../utils/log.js'
import { error, success, warning } from '../utils/log.js'
import { configurePayloadConfig } from './configure-payload-config.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
async function createOrFindProjectDir(projectDir: string): Promise<void> {
const pathExists = await fse.pathExists(projectDir)
if (!pathExists) {
@@ -44,7 +40,7 @@ async function installDeps(args: {
})
return true
} catch (err: unknown) {
error(`Error installing dependencies${err instanceof Error ? `: ${err.message}` : ''}.`)
console.log({ err })
return false
}
}
@@ -59,30 +55,12 @@ export async function createProject(args: {
}): Promise<void> {
const { cliArgs, dbDetails, packageManager, projectDir, projectName, template } = args
if (cliArgs['--dry-run']) {
console.log(`\n Dry run: Creating project in ${chalk.green(projectDir)}\n`)
return
}
await createOrFindProjectDir(projectDir)
console.log(`\n Creating project in ${chalk.green(projectDir)}\n`)
console.log(`\n Creating project in ${chalk.green(path.resolve(projectDir))}\n`)
if (cliArgs['--local-template']) {
// Copy template from local path. For development purposes.
const localTemplate = path.resolve(
dirname,
'../../../../templates/',
cliArgs['--local-template'],
)
await fse.copy(localTemplate, projectDir)
} else if ('url' in template) {
let templateUrl = template.url
if (cliArgs['--template-branch']) {
templateUrl = `${template.url}#${cliArgs['--template-branch']}`
debug(`Using template url: ${templateUrl}`)
}
const emitter = degit(templateUrl)
if ('url' in template) {
const emitter = degit(template.url)
await emitter.clone(projectDir)
}
@@ -97,19 +75,14 @@ export async function createProject(args: {
await fse.remove(lockPath)
}
if (!cliArgs['--no-deps']) {
spinner.text = 'Installing dependencies...'
const result = await installDeps({ cliArgs, packageManager, projectDir })
spinner.stop()
spinner.clear()
if (result) {
success('Dependencies installed')
} else {
error('Error installing dependencies')
}
spinner.text = 'Installing dependencies...'
const result = await installDeps({ cliArgs, packageManager, projectDir })
spinner.stop()
spinner.clear()
if (result) {
success('Dependencies installed')
} else {
spinner.stop()
spinner.clear()
error('Error installing dependencies')
}
}

View File

@@ -2,6 +2,7 @@ import type { CompilerOptions } from 'typescript'
import chalk from 'chalk'
import { parse, stringify } from 'comment-json'
import { detect } from 'detect-package-manager'
import execa from 'execa'
import fs from 'fs'
import fse from 'fs-extra'
@@ -16,24 +17,24 @@ const dirname = path.dirname(filename)
import { fileURLToPath } from 'node:url'
import type { CliArgs, PackageManager } from '../types.js'
import type { CliArgs } from '../types.js'
import { copyRecursiveSync } from '../utils/copy-recursive-sync.js'
import { error, info, debug as origDebug, success, warning } from '../utils/log.js'
type InitNextArgs = Pick<CliArgs, '--debug'> & {
packageManager: PackageManager
projectDir?: string
useDistFiles?: boolean
}
type InitNextResult = { reason?: string; success: boolean; userAppDir?: string }
export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
const { packageManager, projectDir } = args
args.projectDir = args.projectDir || process.cwd()
const { projectDir } = args
const templateResult = await applyPayloadTemplateFiles(args)
if (!templateResult.success) return templateResult
const { success: installSuccess } = await installDeps(projectDir, packageManager)
const { success: installSuccess } = await installDeps(projectDir)
if (!installSuccess) {
return { ...templateResult, reason: 'Failed to install dependencies', success: false }
}
@@ -102,7 +103,7 @@ async function applyPayloadTemplateFiles(args: InitNextArgs): Promise<InitNextRe
}
// Next.js configs can be next.config.js, next.config.mjs, etc.
const foundConfig = (await globby('next.config.*js', { absolute: true, cwd: projectDir }))?.[0]
const foundConfig = (await globby('next.config.*js', { cwd: projectDir }))?.[0]
if (!foundConfig) {
throw new Error(`No next.config.js found at ${projectDir}`)
@@ -135,14 +136,11 @@ async function applyPayloadTemplateFiles(args: InitNextArgs): Promise<InitNextRe
}
// src/app or app
const userAppDir = (
await globby(['**/app'], {
absolute: true,
cwd: projectDir,
onlyDirectories: true,
})
)?.[0]
const userAppDirGlob = await globby(['**/app'], {
cwd: projectDir,
onlyDirectories: true,
})
const userAppDir = path.resolve(projectDir, userAppDirGlob?.[0])
if (!fs.existsSync(userAppDir)) {
return { reason: `Could not find user app directory inside ${projectDir}`, success: false }
} else {
@@ -155,13 +153,18 @@ async function applyPayloadTemplateFiles(args: InitNextArgs): Promise<InitNextRe
return { success: true, userAppDir }
}
async function installDeps(projectDir: string, packageManager: PackageManager) {
async function installDeps(projectDir: string) {
const packageManager = await detect({ cwd: projectDir })
if (!packageManager) {
throw new Error('Could not detect package manager')
}
info(`Installing dependencies with ${packageManager}`, 1)
const packagesToInstall = [
'payload',
'@payloadcms/db-mongodb',
'@payloadcms/next',
'@payloadcms/richtext-lexical',
'@payloadcms/richtext-slate',
].map((pkg) => `${pkg}@alpha`)
let exitCode = 0
@@ -203,7 +206,7 @@ function findOrCreatePayloadConfig(projectDir: string) {
const defaultConfig = `import path from "path";
import { mongooseAdapter } from "@payloadcms/db-mongodb"; // database-adapter-import
import { lexicalEditor } from "@payloadcms/richtext-lexical"; // editor-import
import { slateEditor } from "@payloadcms/richtext-slate"; // editor-import
import { buildConfig } from "payload/config";
export default buildConfig({

View File

@@ -1,9 +1,24 @@
import type { DbType } from '../types.js'
import type { BundlerType, DbType, EditorType } from '../types.js'
type DbAdapterReplacement = {
configReplacement: string[]
importReplacement: string
packageName: string
version: string
}
type BundlerReplacement = {
configReplacement: string
importReplacement: string
packageName: string
version: string
}
type EditorReplacement = {
configReplacement: string
importReplacement: string
packageName: string
version: string
}
const mongodbReplacement: DbAdapterReplacement = {
@@ -11,6 +26,7 @@ const mongodbReplacement: DbAdapterReplacement = {
packageName: '@payloadcms/db-mongodb',
// Replacement between `// database-adapter-config-start` and `// database-adapter-config-end`
configReplacement: [' db: mongooseAdapter({', ' url: process.env.DATABASE_URI,', ' }),'],
version: '^1.0.0',
}
const postgresReplacement: DbAdapterReplacement = {
@@ -23,9 +39,45 @@ const postgresReplacement: DbAdapterReplacement = {
],
importReplacement: "import { postgresAdapter } from '@payloadcms/db-postgres'",
packageName: '@payloadcms/db-postgres',
version: '^0.x', // up to, not including 1.0.0
}
export const dbReplacements: Record<DbType, DbAdapterReplacement> = {
export const dbPackages: Record<DbType, DbAdapterReplacement> = {
mongodb: mongodbReplacement,
postgres: postgresReplacement,
}
const webpackReplacement: BundlerReplacement = {
importReplacement: "import { webpackBundler } from '@payloadcms/bundler-webpack'",
packageName: '@payloadcms/bundler-webpack',
// Replacement of line containing `// bundler-config`
configReplacement: ' bundler: webpackBundler(),',
version: '^1.0.0',
}
const viteReplacement: BundlerReplacement = {
configReplacement: ' bundler: viteBundler(),',
importReplacement: "import { viteBundler } from '@payloadcms/bundler-vite'",
packageName: '@payloadcms/bundler-vite',
version: '^0.x', // up to, not including 1.0.0
}
export const bundlerPackages: Record<BundlerType, BundlerReplacement> = {
vite: viteReplacement,
webpack: webpackReplacement,
}
export const editorPackages: Record<EditorType, EditorReplacement> = {
lexical: {
configReplacement: ' editor: lexicalEditor({}),',
importReplacement: "import { lexicalEditor } from '@payloadcms/richtext-lexical'",
packageName: '@payloadcms/richtext-lexical',
version: '^0.x', // up to, not including 1.0.0
},
slate: {
configReplacement: ' editor: slateEditor({}),',
importReplacement: "import { slateEditor } from '@payloadcms/richtext-slate'",
packageName: '@payloadcms/richtext-slate',
version: '^1.0.0',
},
}

View File

@@ -3,7 +3,6 @@ import prompts from 'prompts'
import type { CliArgs } from '../types.js'
export async function parseProjectName(args: CliArgs): Promise<string> {
if (args['--init-next']) return '.'
if (args['--name']) return args['--name']
if (args._[0]) return args._[0]

View File

@@ -58,36 +58,26 @@ export async function selectDb(args: CliArgs, projectName: string): Promise<DbDe
const dbChoice = dbChoiceRecord[dbType]
let dbUri: string | undefined = undefined
const initialDbUri = `${dbChoice.dbConnectionPrefix}${
projectName === '.' ? `payload-${getRandomDigitSuffix()}` : slugify(projectName)
}`
if (args['--db-accept-recommended']) {
dbUri = initialDbUri
} else if (args['--db-connection-string']) {
dbUri = args['--db-connection-string']
} else {
const dbUriRes = await prompts(
{
name: 'value',
type: 'text',
initial: initialDbUri,
message: `Enter ${dbChoice.title.split(' ')[0]} connection string`, // strip beta from title
validate: (value: string) => !!value.length,
const dbUriRes = await prompts(
{
name: 'value',
type: 'text',
initial: `${dbChoice.dbConnectionPrefix}${
projectName === '.' ? `payload-${getRandomDigitSuffix()}` : slugify(projectName)
}`,
message: `Enter ${dbChoice.title.split(' ')[0]} connection string`, // strip beta from title
validate: (value: string) => !!value.length,
},
{
onCancel: () => {
process.exit(0)
},
{
onCancel: () => {
process.exit(0)
},
},
)
dbUri = dbUriRes.value
}
},
)
return {
type: dbChoice.value,
dbUri,
dbUri: dbUriRes.value,
}
}

View File

@@ -14,12 +14,6 @@ export function validateTemplate(templateName: string): boolean {
export function getValidTemplates(): ProjectTemplate[] {
return [
{
name: 'blank-3.0',
type: 'starter',
description: 'Blank 3.0 Template',
url: 'https://github.com/payloadcms/payload/templates/blank-3.0',
},
{
name: 'blank',
type: 'starter',

View File

@@ -1,54 +0,0 @@
import { parseAndInsertWithPayload, withPayloadImportStatement } from './wrap-next-config.js'
const defaultNextConfig = `/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;
`
const nextConfigWithFunc = `const nextConfig = {
// Your Next.js config here
}
export default someFunc(nextConfig)
`
const nextConfigWithFuncMultiline = `const nextConfig = {
// Your Next.js config here
}
export default someFunc(
nextConfig
)
`
const nextConfigExportNamedDefault = `const nextConfig = {
// Your Next.js config here
}
const wrapped = someFunc(asdf)
export { wrapped as default }
`
describe('parseAndInsertWithPayload', () => {
it('should parse the default next config', () => {
const { modifiedConfigContent } = parseAndInsertWithPayload(defaultNextConfig)
expect(modifiedConfigContent).toContain(withPayloadImportStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
})
it('should parse the config with a function', () => {
const { modifiedConfigContent } = parseAndInsertWithPayload(nextConfigWithFunc)
expect(modifiedConfigContent).toContain('withPayload(someFunc(nextConfig))')
})
it('should parse the config with a function on a new line', () => {
const { modifiedConfigContent } = parseAndInsertWithPayload(nextConfigWithFuncMultiline)
expect(modifiedConfigContent).toContain(withPayloadImportStatement)
expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n nextConfig\n\)\)/)
})
// Unsupported: export { wrapped as default }
it('should give warning with a named export as default', () => {
const { modifiedConfigContent, error } = parseAndInsertWithPayload(nextConfigExportNamedDefault)
expect(modifiedConfigContent).toContain(withPayloadImportStatement)
expect(error).toBeTruthy()
})
})

View File

@@ -1,118 +0,0 @@
import { parseModule } from 'esprima'
import fs from 'fs'
import globby from 'globby'
import path from 'path'
export const withPayloadImportStatement = `import { withPayload } from '@payloadcms/next'\n`
export const wrapNextConfig = async (args: { projectDir: string }): Promise<void> => {
const foundConfig = (await globby('next.config.*js', { cwd: args.projectDir }))?.[0]
if (!foundConfig) {
throw new Error(`No Next config found at ${args.projectDir}`)
}
const configPath = path.resolve(args.projectDir, foundConfig)
const configContent = fs.readFileSync(configPath, 'utf8')
const { error, modifiedConfigContent: newConfig } = parseAndInsertWithPayload(configContent)
if (error) {
console.warn(error)
}
fs.writeFileSync(configPath, newConfig)
}
export function parseAndInsertWithPayload(content: string): {
error?: string
modifiedConfigContent: string
} {
content = withPayloadImportStatement + content
const ast = parseModule(content, { loc: true })
const exportDefaultDeclaration = ast.body.find((p) => p.type === 'ExportDefaultDeclaration') as
| Directive
| undefined
const exportNamedDeclaration = ast.body.find((p) => p.type === 'ExportNamedDeclaration') as
| ExportNamedDeclaration
| undefined
if (!exportDefaultDeclaration && !exportNamedDeclaration) {
throw new Error('Could not find ExportDefaultDeclaration in next.config.js')
}
if (exportDefaultDeclaration) {
const modifiedConfigContent = insertBeforeAndAfter(
content,
exportDefaultDeclaration.declaration?.loc,
)
return { modifiedConfigContent }
} else if (exportNamedDeclaration) {
const exportSpecifier = exportNamedDeclaration.specifiers.find(
(s) =>
s.type === 'ExportSpecifier' &&
s.exported?.name === 'default' &&
s.local?.type === 'Identifier' &&
s.local?.name,
)
if (exportSpecifier) {
// TODO: Improve with this example and/or link to docs
return {
error: `Automatic wrapping of named exports as default not supported yet.
Please manually wrap your Next config with the withPayload function`,
modifiedConfigContent: content,
}
}
} else {
throw new Error('Could not automatically wrap next.config.js with withPayload')
}
}
type Directive = {
declaration?: {
loc: Loc
}
}
type ExportNamedDeclaration = {
declaration: null
loc: Loc
specifiers: {
exported: {
loc: Loc
name: string
type: string
}
loc: Loc
local: {
loc: Loc
name: string
type: string
}
type: string
}[]
type: string
}
type Loc = {
end: { column: number; line: number }
start: { column: number; line: number }
}
function insertBeforeAndAfter(content: string, loc: Loc) {
const { end, start } = loc
const lines = content.split('\n')
const insert = (line: string, column: number, text: string) => {
return line.slice(0, column) + text + line.slice(column)
}
// insert ) after end
lines[end.line - 1] = insert(lines[end.line - 1], end.column, ')')
// insert withPayload before start
if (start.line === end.line) {
lines[end.line - 1] = insert(lines[end.line - 1], start.column, 'withPayload(')
} else {
lines[start.line - 1] = insert(lines[start.line - 1], start.column, 'withPayload(')
}
return lines.join('\n')
}

View File

@@ -1,26 +1,18 @@
import chalk from 'chalk'
import fs from 'fs-extra'
import path from 'path'
import type { CliArgs, ProjectTemplate } from '../types.js'
import type { ProjectTemplate } from '../types.js'
import { error, success } from '../utils/log.js'
/** Parse and swap .env.example values and write .env */
export async function writeEnvFile(args: {
cliArgs: CliArgs
databaseUri: string
payloadSecret: string
projectDir: string
template: ProjectTemplate
}): Promise<void> {
const { cliArgs, databaseUri, payloadSecret, projectDir, template } = args
if (cliArgs['--dry-run']) {
success(`DRY RUN: .env file created`)
return
}
const { databaseUri, payloadSecret, projectDir, template } = args
try {
if (template.type === 'starter' && fs.existsSync(path.join(projectDir, '.env.example'))) {
// Parse .env file into key/value pairs

View File

@@ -1,8 +1,6 @@
/* eslint-disable no-console */
import slugify from '@sindresorhus/slugify'
import arg from 'arg'
import { detect } from 'detect-package-manager'
import path from 'path'
import commandExists from 'command-exists'
import type { CliArgs, PackageManager } from './types.js'
@@ -25,14 +23,10 @@ export class Main {
this.args = arg(
{
'--db': String,
'--db-accept-recommended': Boolean,
'--db-connection-string': String,
'--help': Boolean,
'--local-template': String,
'--name': String,
'--secret': String,
'--template': String,
'--template-branch': String,
// Next.js
'--init-next': Boolean,
@@ -65,25 +59,14 @@ export class Main {
process.exit(0)
}
const projectName = await parseProjectName(this.args)
const projectDir = path.resolve(
projectName === '.' || this.args['--init-next']
? path.basename(process.cwd())
: `./${slugify(projectName)}`,
)
console.log(welcomeMessage)
const packageManager = await getPackageManager(this.args, projectDir)
if (this.args['--init-next']) {
const result = await initNext({ ...this.args, packageManager })
const result = await initNext(this.args)
if (!result.success) {
error(result.reason || 'Failed to initialize Payload app in Next.js project')
} else {
success('Payload app successfully initialized in Next.js project')
}
process.exit(result.success ? 0 : 1)
// TODO: This should continue the normal prompt flow
}
const templateArg = this.args['--template']
@@ -95,13 +78,18 @@ export class Main {
}
}
console.log(welcomeMessage)
const projectName = await parseProjectName(this.args)
const validTemplates = getValidTemplates()
const template = await parseTemplate(this.args, validTemplates)
switch (template.type) {
case 'starter': {
const dbDetails = await selectDb(this.args, projectName)
const payloadSecret = generateSecret()
const projectDir = projectName === '.' ? process.cwd() : `./${slugify(projectName)}`
const packageManager = await getPackageManager(this.args)
if (template.type !== 'plugin') {
const dbDetails = await selectDb(this.args, projectName)
const payloadSecret = generateSecret()
if (!this.args['--dry-run']) {
await createProject({
cliArgs: this.args,
dbDetails,
@@ -111,15 +99,14 @@ export class Main {
template,
})
await writeEnvFile({
cliArgs: this.args,
databaseUri: dbDetails.dbUri,
payloadSecret,
projectDir,
template,
})
break
}
case 'plugin': {
} else {
if (!this.args['--dry-run']) {
await createProject({
cliArgs: this.args,
packageManager,
@@ -127,7 +114,6 @@ export class Main {
projectName,
template,
})
break
}
}
@@ -139,7 +125,7 @@ export class Main {
}
}
async function getPackageManager(args: CliArgs, projectDir: string): Promise<PackageManager> {
async function getPackageManager(args: CliArgs): Promise<PackageManager> {
let packageManager: PackageManager = 'npm'
if (args['--use-npm']) {
@@ -149,8 +135,15 @@ async function getPackageManager(args: CliArgs, projectDir: string): Promise<Pac
} else if (args['--use-pnpm']) {
packageManager = 'pnpm'
} else {
const detected = await detect({ cwd: projectDir })
packageManager = detected || 'npm'
try {
if (await commandExists('yarn')) {
packageManager = 'yarn'
} else if (await commandExists('pnpm')) {
packageManager = 'pnpm'
}
} catch (error: unknown) {
packageManager = 'npm'
}
}
return packageManager
}

View File

@@ -3,18 +3,14 @@ import type arg from 'arg'
export interface Args extends arg.Spec {
'--beta': BooleanConstructor
'--db': StringConstructor
'--db-accept-recommended': BooleanConstructor
'--db-connection-string': StringConstructor
'--debug': BooleanConstructor
'--dry-run': BooleanConstructor
'--help': BooleanConstructor
'--init-next': BooleanConstructor
'--local-template': StringConstructor
'--name': StringConstructor
'--no-deps': BooleanConstructor
'--secret': StringConstructor
'--template': StringConstructor
'--template-branch': StringConstructor
'--use-npm': BooleanConstructor
'--use-pnpm': BooleanConstructor
'--use-yarn': BooleanConstructor
@@ -54,7 +50,7 @@ interface Template {
type: ProjectTemplate['type']
}
export type PackageManager = 'bun' | 'npm' | 'pnpm' | 'yarn'
export type PackageManager = 'npm' | 'pnpm' | 'yarn'
export type DbType = 'mongodb' | 'postgres'
@@ -63,4 +59,5 @@ export type DbDetails = {
type: DbType
}
export type BundlerType = 'vite' | 'webpack'
export type EditorType = 'lexical' | 'slate'

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-console */
import chalk from 'chalk'
import figures from 'figures'

View File

@@ -3,9 +3,9 @@ import figures from 'figures'
import path from 'path'
import terminalLink from 'terminal-link'
import type { ProjectTemplate } from '../types.js'
import type { ProjectTemplate } from '../types'
import { getValidTemplates } from '../lib/templates.js'
import { getValidTemplates } from '../lib/templates'
const header = (message: string): string => `${chalk.yellow(figures.star)} ${chalk.bold(message)}`

View File

@@ -7,6 +7,17 @@
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
"rootDir": "./src" /* Specify the root folder within your source files. */
},
"exclude": ["dist", "build", "tests", "test", "node_modules", ".eslintrc.js"],
"include": ["src/**/*.ts", "src/**/*.spec.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"]
"exclude": [
"dist",
"build",
"tests",
"test",
"node_modules",
".eslintrc.js",
"src/**/*.spec.js",
"src/**/*.spec.jsx",
"src/**/*.spec.ts",
"src/**/*.spec.tsx"
],
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-mongodb",
"version": "3.0.0-alpha.49",
"version": "3.0.0-alpha.48",
"description": "The officially supported MongoDB database adapter for Payload - Update 2",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",
@@ -11,8 +11,8 @@
"name": "Payload",
"url": "https://payloadcms.com"
},
"main": "./src/index.ts",
"types": "./src/types.ts",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "pnpm build:swc && pnpm build:types",
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
@@ -41,23 +41,10 @@
"peerDependencies": {
"payload": "workspace:*"
},
"exports": {
".": {
"import": "./src/index.ts",
"require": "./src/index.ts",
"types": "./src/index.ts"
}
},
"publishConfig": {
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
}
"registry": "https://registry.npmjs.org/",
"types": "./dist/index.d.ts"
},
"files": [
"dist",

View File

@@ -35,12 +35,6 @@ export const connect: Connect = async function connect(
try {
this.connection = (await mongoose.connect(urlToConnect, connectionOptions)).connection
// If we are running a replica set with MongoDB Memory Server,
// wait until the replica set elects a primary before proceeding
if (this.mongoMemoryServer) {
await new Promise((resolve) => setTimeout(resolve, 2000))
}
const client = this.connection.getClient()
if (!client.options.replicaSet) {

View File

@@ -26,7 +26,6 @@ export const updateOne: UpdateOne = async function updateOne(
})
let result
try {
result = await Model.findOneAndUpdate(query, data, options)
} catch (error) {

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/db-postgres",
"version": "3.0.0-alpha.49",
"version": "3.0.0-alpha.48",
"description": "The officially supported Postgres database adapter for Payload",
"repository": "https://github.com/payloadcms/payload",
"license": "MIT",
@@ -11,8 +11,8 @@
"name": "Payload",
"url": "https://payloadcms.com"
},
"main": "./src/index.ts",
"types": "./src/types.ts",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "pnpm build:swc && pnpm build:types",
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
@@ -39,33 +39,10 @@
"peerDependencies": {
"payload": "workspace:*"
},
"exports": {
".": {
"import": "./src/index.ts",
"require": "./src/index.ts",
"types": "./src/index.ts"
},
"./types": {
"import": "./src/types.ts",
"require": "./src/types.ts",
"types": "./src/types.ts"
}
},
"publishConfig": {
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./types": {
"import": "./dist/types.js",
"require": "./dist/types.js",
"types": "./dist/types.d.ts"
}
}
"registry": "https://registry.npmjs.org/",
"types": "./dist/index.d.ts"
},
"files": [
"dist",

View File

@@ -1,68 +1,47 @@
import type { DeleteOne } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
import { eq } from 'drizzle-orm'
import toSnakeCase from 'to-snake-case'
import type { PostgresAdapter } from './types.js'
import { buildFindManyArgs } from './find/buildFindManyArgs.js'
import buildQuery from './queries/buildQuery.js'
import { selectDistinct } from './queries/selectDistinct.js'
import { transform } from './transform/read/index.js'
export const deleteOne: DeleteOne = async function deleteOne(
this: PostgresAdapter,
{ collection: collectionSlug, req = {} as PayloadRequest, where: whereArg },
{ collection, req = {} as PayloadRequest, where: incomingWhere },
) {
const db = this.sessions[req.transactionID]?.db || this.drizzle
const collection = this.payload.collections[collectionSlug].config
const tableName = toSnakeCase(collectionSlug)
let docToDelete: Record<string, unknown>
const collectionConfig = this.payload.collections[collection].config
const tableName = toSnakeCase(collection)
const { joinAliases, joins, selectFields, where } = await buildQuery({
const { where } = await buildQuery({
adapter: this,
fields: collection.fields,
locale: req.locale,
fields: collectionConfig.fields,
tableName,
where: whereArg,
where: incomingWhere,
})
const selectDistinctResult = await selectDistinct({
const findManyArgs = buildFindManyArgs({
adapter: this,
chainedMethods: [{ args: [1], method: 'limit' }],
db,
joinAliases,
joins,
selectFields,
depth: 0,
fields: collectionConfig.fields,
tableName,
where,
})
if (selectDistinctResult?.[0]?.id) {
docToDelete = await db.query[tableName].findFirst({
where: eq(this.tables[tableName].id, selectDistinctResult[0].id),
})
} else {
const findManyArgs = buildFindManyArgs({
adapter: this,
depth: 0,
fields: collection.fields,
tableName,
})
findManyArgs.where = where
findManyArgs.where = where
docToDelete = await db.query[tableName].findFirst(findManyArgs)
}
const docToDelete = await db.query[tableName].findFirst(findManyArgs)
const result = transform({
config: this.payload.config,
data: docToDelete,
fields: collection.fields,
fields: collectionConfig.fields,
})
await db.delete(this.tables[tableName]).where(eq(this.tables[tableName].id, docToDelete.id))
await db.delete(this.tables[tableName]).where(where)
return result
}

View File

@@ -1,5 +1,3 @@
import type { QueryPromise } from 'drizzle-orm'
export type ChainedMethods = {
args: unknown[]
method: string
@@ -10,7 +8,7 @@ export type ChainedMethods = {
* @param methods
* @param query
*/
const chainMethods = <T>({ methods, query }): QueryPromise<T> => {
const chainMethods = ({ methods, query }): Promise<unknown> => {
return methods.reduce((query, { args, method }) => {
return query[method](...args)
}, query)

View File

@@ -7,7 +7,6 @@ import type { PostgresAdapter } from '../types.js'
import type { ChainedMethods } from './chainMethods.js'
import buildQuery from '../queries/buildQuery.js'
import { selectDistinct } from '../queries/selectDistinct.js'
import { transform } from '../transform/read/index.js'
import { buildFindManyArgs } from './buildFindManyArgs.js'
import { chainMethods } from './chainMethods.js'
@@ -40,6 +39,7 @@ export const findMany = async function find({
let hasPrevPage: boolean
let hasNextPage: boolean
let pagingCounter: number
let selectDistinctResult
const { joinAliases, joins, orderBy, selectFields, where } = await buildQuery({
adapter,
@@ -69,21 +69,36 @@ export const findMany = async function find({
tableName,
})
selectDistinctMethods.push({ args: [skip || (page - 1) * limit], method: 'offset' })
selectDistinctMethods.push({ args: [limit === 0 ? undefined : limit], method: 'limit' })
// only fetch IDs when a sort or where query is used that needs to be done on join tables, otherwise these can be done directly on the table in findMany
if (Object.keys(joins).length > 0 || joinAliases.length > 0) {
if (where) {
selectDistinctMethods.push({ args: [where], method: 'where' })
}
const selectDistinctResult = await selectDistinct({
adapter,
chainedMethods: selectDistinctMethods,
db,
joinAliases,
joins,
selectFields,
tableName,
where,
})
joinAliases.forEach(({ condition, table }) => {
selectDistinctMethods.push({
args: [table, condition],
method: 'leftJoin',
})
})
Object.entries(joins).forEach(([joinTable, condition]) => {
if (joinTable) {
selectDistinctMethods.push({
args: [adapter.tables[joinTable], condition],
method: 'leftJoin',
})
}
})
selectDistinctMethods.push({ args: [skip || (page - 1) * limit], method: 'offset' })
selectDistinctMethods.push({ args: [limit === 0 ? undefined : limit], method: 'limit' })
selectDistinctResult = await chainMethods({
methods: selectDistinctMethods,
query: db.selectDistinct(selectFields).from(table),
})
if (selectDistinctResult) {
if (selectDistinctResult.length === 0) {
return {
docs: [],
@@ -97,14 +112,13 @@ export const findMany = async function find({
totalDocs: 0,
totalPages: 0,
}
} else {
// set the id in an object for sorting later
selectDistinctResult.forEach(({ id }, i) => {
orderedIDMap[id] = i
})
orderedIDs = Object.keys(orderedIDMap)
findManyArgs.where = inArray(adapter.tables[tableName].id, orderedIDs)
}
// set the id in an object for sorting later
selectDistinctResult.forEach(({ id }, i) => {
orderedIDMap[id as number | string] = i
})
orderedIDs = Object.keys(orderedIDMap)
findManyArgs.where = inArray(adapter.tables[tableName].id, orderedIDs)
} else {
findManyArgs.limit = limitArg === 0 ? undefined : limitArg

View File

@@ -85,10 +85,6 @@ export const sanitizeQueryValue = ({
}
}
if ('hasMany' in field && field.hasMany && operator === 'contains') {
operator = 'equals'
}
if (operator === 'near' || operator === 'within' || operator === 'intersects') {
throw new APIError(
`Querying with '${operator}' is not supported with the postgres database adapter.`,

View File

@@ -1,60 +0,0 @@
import type { QueryPromise, SQL } from 'drizzle-orm'
import type { ChainedMethods } from '../find/chainMethods.js'
import type { DrizzleDB, PostgresAdapter } from '../types.js'
import type { BuildQueryJoinAliases, BuildQueryJoins } from './buildQuery.js'
import { chainMethods } from '../find/chainMethods.js'
import { type GenericColumn } from '../types.js'
type Args = {
adapter: PostgresAdapter
chainedMethods?: ChainedMethods
db: DrizzleDB
joinAliases: BuildQueryJoinAliases
joins: BuildQueryJoins
selectFields: Record<string, GenericColumn>
tableName: string
where: SQL
}
/**
* Selects distinct records from a table only if there are joins that need to be used, otherwise return null
*/
export const selectDistinct = ({
adapter,
chainedMethods = [],
db,
joinAliases,
joins,
selectFields,
tableName,
where,
}: Args): QueryPromise<Record<string, GenericColumn> & { id: number | string }[]> => {
if (Object.keys(joins).length > 0 || joinAliases.length > 0) {
if (where) {
chainedMethods.push({ args: [where], method: 'where' })
}
joinAliases.forEach(({ condition, table }) => {
chainedMethods.push({
args: [table, condition],
method: 'leftJoin',
})
})
Object.entries(joins).forEach(([joinTable, condition]) => {
if (joinTable) {
chainedMethods.push({
args: [adapter.tables[joinTable], condition],
method: 'leftJoin',
})
}
})
return chainMethods({
methods: chainedMethods,
query: db.selectDistinct(selectFields).from(adapter.tables[tableName]),
})
}
}

View File

@@ -2,10 +2,11 @@ import type { UpdateOne } from 'payload/database'
import toSnakeCase from 'to-snake-case'
import type { ChainedMethods } from './find/chainMethods.js'
import type { PostgresAdapter } from './types.js'
import { chainMethods } from './find/chainMethods.js'
import buildQuery from './queries/buildQuery.js'
import { selectDistinct } from './queries/selectDistinct.js'
import { upsertRow } from './upsertRow/index.js'
export const updateOne: UpdateOne = async function updateOne(
@@ -16,7 +17,6 @@ export const updateOne: UpdateOne = async function updateOne(
const collection = this.payload.collections[collectionSlug].config
const tableName = toSnakeCase(collectionSlug)
const whereToUse = whereArg || { id: { equals: id } }
let idToUpdate = id
const { joinAliases, joins, selectFields, where } = await buildQuery({
adapter: this,
@@ -26,19 +26,42 @@ export const updateOne: UpdateOne = async function updateOne(
where: whereToUse,
})
const selectDistinctResult = await selectDistinct({
adapter: this,
chainedMethods: [{ args: [1], method: 'limit' }],
db,
joinAliases,
joins,
selectFields,
tableName,
where,
})
let idToUpdate = id
if (selectDistinctResult?.[0]?.id) {
idToUpdate = selectDistinctResult?.[0]?.id
// only fetch IDs when a sort or where query is used that needs to be done on join tables, otherwise these can be done directly on the table in findMany
if (Object.keys(joins).length > 0 || joinAliases.length > 0) {
const selectDistinctMethods: ChainedMethods = []
if (where) {
selectDistinctMethods.push({ args: [where], method: 'where' })
}
joinAliases.forEach(({ condition, table }) => {
selectDistinctMethods.push({
args: [table, condition],
method: 'leftJoin',
})
})
Object.entries(joins).forEach(([joinTable, condition]) => {
if (joinTable) {
selectDistinctMethods.push({
args: [this.tables[joinTable], condition],
method: 'leftJoin',
})
}
})
selectDistinctMethods.push({ args: [1], method: 'limit' })
const selectDistinctResult = await chainMethods({
methods: selectDistinctMethods,
query: db.selectDistinct(selectFields).from(this.tables[tableName]),
})
if (selectDistinctResult?.[0]?.id) {
idToUpdate = selectDistinctResult?.[0]?.id
}
}
const result = await upsertRow({

View File

@@ -22,7 +22,6 @@ const baseRules = {
},
},
],
'payload/no-jsx-import-statements': 'error',
}
const reactRules = {
@@ -113,7 +112,7 @@ module.exports = {
overrides: [
{
files: ['**/*.ts'],
plugins: ['@typescript-eslint', 'payload'],
plugins: ['@typescript-eslint'],
extends: [
...baseExtends,
'plugin:@typescript-eslint/recommended-type-checked',
@@ -127,7 +126,7 @@ module.exports = {
},
{
files: ['**/*.tsx'],
plugins: ['@typescript-eslint', 'payload'],
plugins: ['@typescript-eslint'],
extends: [
...baseExtends,
'plugin:@typescript-eslint/recommended-type-checked',
@@ -145,7 +144,7 @@ module.exports = {
},
{
files: ['**/*.spec.ts'],
plugins: ['@typescript-eslint', 'payload'],
plugins: ['@typescript-eslint'],
extends: [
...baseExtends,
'plugin:@typescript-eslint/recommended-type-checked',
@@ -160,7 +159,6 @@ module.exports = {
},
},
{
plugins: ['payload'],
files: ['*.config.ts'],
rules: {
...baseRules,
@@ -169,7 +167,6 @@ module.exports = {
},
},
{
plugins: ['payload'],
files: ['config.ts'],
rules: {
...baseRules,

View File

@@ -13,9 +13,9 @@
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"@types/eslint": "8.56.6",
"@typescript-eslint/eslint-plugin": "7.3.1",
"@typescript-eslint/parser": "7.3.1",
"@types/eslint": "8.56.5",
"@typescript-eslint/eslint-plugin": "7.2.0",
"@typescript-eslint/parser": "7.2.0",
"eslint": "8.57.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-jest": "27.9.0",
@@ -23,10 +23,10 @@
"eslint-plugin-jsx-a11y": "6.8.0",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-perfectionist": "2.7.0",
"eslint-plugin-react": "7.34.1",
"eslint-plugin-playwright": "1.5.2",
"eslint-plugin-react": "7.34.0",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-regexp": "2.3.0",
"eslint-plugin-payload": "workspace:*"
"eslint-plugin-regexp": "2.3.0"
},
"keywords": []
}

View File

@@ -1,122 +0,0 @@
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
docs: {
description:
'Disallow non-retryable assertions in Playwright E2E tests unless they are wrapped in an expect.poll() or expect().toPass()',
category: 'Best Practices',
recommended: true,
},
schema: [],
},
create: function (context) {
const nonRetryableAssertions = [
'toBe',
'toBeCloseTo',
'toBeDefined',
'toBeFalsy',
'toBeGreaterThan',
'toBeGreaterThanOrEqual',
'toBeInstanceOf',
'toBeLessThan',
'toBeLessThanOrEqual',
'toBeNaN',
'toBeNull',
'toBeTruthy',
'toBeUndefined',
'toContain',
'toContainEqual',
'toEqual',
'toHaveLength',
'toHaveProperty',
'toMatch',
'toMatchObject',
'toStrictEqual',
'toThrow',
'any',
'anything',
'arrayContaining',
'closeTo',
'objectContaining',
'stringContaining',
'stringMatching',
]
function isNonRetryableAssertion(node) {
return (
node.type === 'MemberExpression' &&
node.property.type === 'Identifier' &&
nonRetryableAssertions.includes(node.property.name)
)
}
function isExpectPollOrToPass(node) {
if (
node.type === 'MemberExpression' &&
(node?.property?.name === 'poll' || node?.property?.name === 'toPass')
) {
return true
}
return (
node.type === 'CallExpression' &&
node.callee.type === 'MemberExpression' &&
((node.callee.object.type === 'CallExpression' &&
node.callee.object.callee.type === 'MemberExpression' &&
node.callee.object.callee.property.name === 'poll') ||
node.callee.property.name === 'toPass')
)
}
function hasExpectPollOrToPassInChain(node) {
let ancestor = node
while (ancestor) {
if (isExpectPollOrToPass(ancestor)) {
return true
}
ancestor = 'object' in ancestor ? ancestor.object : ancestor.callee
}
return false
}
function hasExpectPollOrToPassInParentChain(node) {
let ancestor = node
while (ancestor) {
if (isExpectPollOrToPass(ancestor)) {
return true
}
ancestor = ancestor.parent
}
return false
}
return {
CallExpression(node) {
// node.callee is MemberExpressiom
if (isNonRetryableAssertion(node.callee)) {
if (hasExpectPollOrToPassInChain(node.callee)) {
return
}
if (hasExpectPollOrToPassInParentChain(node)) {
return
}
context.report({
node: node.callee.property,
message:
'Non-retryable, flaky assertion used in Playwright test: "{{ assertion }}". Those need to be wrapped in expect.poll() or expect().toPass().',
data: {
assertion: node.callee.property.name,
},
})
}
},
}
},
}

View File

@@ -1,28 +0,0 @@
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Disallow imports from .jsx extensions',
},
fixable: 'code',
schema: [],
},
create: function (context) {
return {
ImportDeclaration(node) {
const importPath = node.source.value
if (!importPath.endsWith('.jsx')) return
context.report({
node: node.source,
message: 'JSX imports are invalid. Use .js instead.',
fix: (fixer) => {
return fixer.removeRange([node.source.range[1] - 2, node.source.range[1] - 1])
},
})
},
}
},
}

View File

@@ -1,65 +0,0 @@
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Disallow non-retryable assertions in Playwright E2E tests',
category: 'Best Practices',
recommended: true,
},
schema: [],
},
create: function (context) {
const nonRetryableAssertions = [
'toBe',
'toBeCloseTo',
'toBeDefined',
'toBeFalsy',
'toBeGreaterThan',
'toBeGreaterThanOrEqual',
'toBeInstanceOf',
'toBeLessThan',
'toBeLessThanOrEqual',
'toBeNaN',
'toBeNull',
'toBeTruthy',
'toBeUndefined',
'toContain',
'toContainEqual',
'toEqual',
'toHaveLength',
'toHaveProperty',
'toMatch',
'toMatchObject',
'toStrictEqual',
'toThrow',
'any',
'anything',
'arrayContaining',
'closeTo',
'objectContaining',
'stringContaining',
'stringMatching',
]
return {
CallExpression(node) {
if (
node.callee.type === 'MemberExpression' &&
//node.callee.object.name === 'expect' &&
node.callee.property.type === 'Identifier' &&
nonRetryableAssertions.includes(node.callee.property.name)
) {
context.report({
node: node.callee.property,
message:
'Non-retryable, flaky assertion used in Playwright test: "{{ assertion }}". Those need to be wrapped in expect.poll or expect.toPass.',
data: {
assertion: node.callee.property.name,
},
})
}
},
}
},
}

View File

@@ -1,29 +0,0 @@
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Disallow imports from relative monorepo packages/*/src',
category: 'Best Practices',
recommended: true,
},
schema: [],
},
create: function (context) {
return {
ImportDeclaration(node) {
const importPath = node.source.value
// Match imports starting with any number of "../" followed by "packages/"
const regex = /^(\.\.\/)*packages\/[^/]+\/src/
if (regex.test(importPath)) {
context.report({
node: node.source,
message: 'Import from relative "packages/*/src" is not allowed',
})
}
},
}
},
}

View File

@@ -1,25 +0,0 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
rules: {
'no-jsx-import-statements': require('./customRules/no-jsx-import-statements'),
'no-non-retryable-assertions': require('./customRules/no-non-retryable-assertions'),
'no-relative-monorepo-imports': require('./customRules/no-relative-monorepo-imports'),
'no-flaky-assertions': require('./customRules/no-flaky-assertions'),
'no-wait-function': {
create: function (context) {
return {
CallExpression(node) {
// Check if the function being called is named "wait"
if (node.callee.name === 'wait') {
context.report({
node,
message:
'Usage of "wait" function is discouraged as it\'s flaky. Proper assertions should be used instead.',
})
}
},
}
},
},
},
}

View File

@@ -1,31 +0,0 @@
{
"name": "eslint-plugin-payload",
"version": "1.0.0",
"description": "Payload plugins for ESLint",
"license": "MIT",
"author": {
"email": "info@payloadcms.com",
"name": "Payload",
"url": "https://payloadcms.com"
},
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"@types/eslint": "8.56.6",
"@typescript-eslint/eslint-plugin": "7.3.1",
"@typescript-eslint/parser": "7.3.1",
"eslint": "8.57.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-jest": "27.9.0",
"eslint-plugin-jest-dom": "5.1.0",
"eslint-plugin-jsx-a11y": "6.8.0",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-perfectionist": "2.7.0",
"eslint-plugin-react": "7.34.1",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-regexp": "2.3.0"
},
"keywords": []
}

View File

@@ -1,6 +1,6 @@
{
"name": "@payloadcms/graphql",
"version": "3.0.0-alpha.49",
"version": "3.0.0-alpha.48",
"main": "./src/index.ts",
"types": "./src/index.d.ts",
"type": "module",
@@ -45,7 +45,8 @@
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
}
},
"registry": "https://registry.npmjs.org/"
},
"files": [
"dist"

View File

@@ -1,45 +0,0 @@
import type { GeneratedTypes } from 'payload'
import type { PayloadRequest } from 'payload/types'
import type { Collection } from 'payload/types'
import { duplicateOperation } from 'payload/operations'
import { isolateObjectProperty } from 'payload/utilities'
import type { Context } from '../types.js'
export type Resolver<T> = (
_: unknown,
args: {
draft: boolean
fallbackLocale?: string
id: string
locale?: string
},
context: {
req: PayloadRequest
},
) => Promise<T>
export default function duplicateResolver<T extends keyof GeneratedTypes['collections']>(
collection: Collection,
): Resolver<GeneratedTypes['collections'][T]> {
return async function resolver(_, args, context: Context) {
const { req } = context
const locale = req.locale
const fallbackLocale = req.fallbackLocale
req.locale = args.locale || locale
req.fallbackLocale = args.fallbackLocale || fallbackLocale
const options = {
id: args.id,
collection,
depth: 0,
draft: args.draft,
req: isolateObjectProperty(req, 'transactionID'),
}
const result = await duplicateOperation(options)
return result
}
}

View File

@@ -27,7 +27,6 @@ import verifyEmail from '../resolvers/auth/verifyEmail.js'
import createResolver from '../resolvers/collections/create.js'
import getDeleteResolver from '../resolvers/collections/delete.js'
import { docAccessResolver } from '../resolvers/collections/docAccess.js'
import duplicateResolver from '../resolvers/collections/duplicate.js'
import findResolver from '../resolvers/collections/find.js'
import findByIDResolver from '../resolvers/collections/findByID.js'
import findVersionByIDResolver from '../resolvers/collections/findVersionByID.js'
@@ -238,14 +237,6 @@ function initCollectionsGraphQL({ config, graphqlResult }: InitCollectionsGraphQ
resolve: getDeleteResolver(collection),
}
graphqlResult.Mutation.fields[`duplicate${singularName}`] = {
type: collection.graphQL.type,
args: {
id: { type: new GraphQLNonNull(idType) },
},
resolve: duplicateResolver(collection),
}
if (collectionConfig.versions) {
const versionIDType = config.db.defaultIDType === 'text' ? GraphQLString : GraphQLInt
const versionCollectionFields: Field[] = [

View File

@@ -30,8 +30,8 @@
},
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
"default": "./src/index.ts",
"types": "./src/index.ts"
}
},
"publishConfig": {

View File

@@ -23,8 +23,8 @@
},
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
"default": "./src/index.ts",
"types": "./src/index.ts"
}
},
"publishConfig": {

View File

@@ -1,8 +0,0 @@
export declare const handleMessage: <T>(args: {
apiRoute?: string
depth?: number
event: MessageEvent
initialData: T
serverURL: string
}) => Promise<T>
//# sourceMappingURL=handleMessage.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"handleMessage.d.ts","sourceRoot":"","sources":["handleMessage.ts"],"names":[],"mappings":"AAYA,eAAO,MAAM,aAAa;eACb,MAAM;YACT,MAAM;WACP,YAAY;;eAER,MAAM;gBAyClB,CAAA"}

View File

@@ -1,6 +0,0 @@
export { handleMessage } from './handleMessage.js'
export { mergeData } from './mergeData.js'
export { ready } from './ready.js'
export { subscribe } from './subscribe.js'
export { unsubscribe } from './unsubscribe.js'
//# sourceMappingURL=index.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAClC,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA"}

View File

@@ -1,26 +0,0 @@
import type { fieldSchemaToJSON } from 'payload/utilities'
import type { UpdatedDocument } from './types.js'
export declare const mergeData: <T>(args: {
apiRoute?: string
collectionPopulationRequestHandler?: ({
apiPath,
endpoint,
serverURL,
}: {
apiPath: string
endpoint: string
serverURL: string
}) => Promise<Response>
depth?: number
externallyUpdatedRelationship?: UpdatedDocument
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
incomingData: Partial<T>
initialData: T
returnNumberOfRequests?: boolean
serverURL: string
}) => Promise<
T & {
_numberOfRequests?: number
}
>
//# sourceMappingURL=mergeData.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"mergeData.d.ts","sourceRoot":"","sources":["mergeData.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAE1D,OAAO,KAAK,EAA2B,eAAe,EAAE,MAAM,YAAY,CAAA;AAc1E,eAAO,MAAM,SAAS;eACT,MAAM;;iBAMN,MAAM;kBACL,MAAM;mBACL,MAAM;UACb,QAAQ,QAAQ,CAAC;YACf,MAAM;oCACkB,eAAe;iBAClC,WAAW,wBAAwB,CAAC;;;6BAGxB,OAAO;eACrB,MAAM;;wBAGK,MAAM;EA6D7B,CAAA"}

View File

@@ -1,8 +0,0 @@
export declare const subscribe: <T>(args: {
apiRoute?: string
callback: (data: T) => void
depth?: number
initialData: T
serverURL: string
}) => (event: MessageEvent) => void
//# sourceMappingURL=subscribe.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"subscribe.d.ts","sourceRoot":"","sources":["subscribe.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,SAAS;eACT,MAAM;2BACM,IAAI;YACnB,MAAM;;eAEH,MAAM;cACN,YAAY,KAAK,IAa7B,CAAA"}

View File

@@ -1,10 +0,0 @@
import type { fieldSchemaToJSON } from 'payload/utilities'
import type { PopulationsByCollection, UpdatedDocument } from './types.js'
export declare const traverseFields: <T>(args: {
externallyUpdatedRelationship?: UpdatedDocument
fieldSchema: ReturnType<typeof fieldSchemaToJSON>
incomingData: T
populationsByCollection: PopulationsByCollection
result: T
}) => void
//# sourceMappingURL=traverseFields.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"traverseFields.d.ts","sourceRoot":"","sources":["traverseFields.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAE1D,OAAO,KAAK,EAAE,uBAAuB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAI1E,eAAO,MAAM,cAAc;oCACO,eAAe;iBAClC,WAAW,wBAAwB,CAAC;;6BAExB,uBAAuB;;MAE9C,IA4QH,CAAA"}

View File

@@ -1,8 +1,8 @@
{
"name": "@payloadcms/next",
"version": "3.0.0-alpha.49",
"main": "./src/index.js",
"types": "./src/index.js",
"version": "3.0.0-alpha.48",
"main": "./src/index.ts",
"types": "./src/index.d.ts",
"type": "module",
"bin": {
"@payloadcms/next": "./dist/bin/index.js"
@@ -13,21 +13,36 @@
"build:types": "tsc --emitDeclarationOnly --outDir dist",
"build:webpack": "webpack --config webpack.config.js",
"clean": "rimraf {dist,*.tsbuildinfo}",
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" \"src/app/api/**\" dist/",
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" \"src/app/api/**\" dist/ && pnpm copyfiles:api",
"copyfiles:api": "copyfiles -u 1 \"src/app/(payload)/**\" dist/template",
"fix": "eslint \"src/**/*.{ts,tsx}\" --fix",
"lint": "eslint \"src/**/*.{ts,tsx}\"",
"prepublishOnly": "pnpm clean && pnpm turbo build"
},
"exports": {
".": {
"import": "./src/index.js",
"require": "./src/index.js",
"types": "./src/index.js"
},
"./*": {
"import": "./src/exports/*.ts",
"require": "./src/exports/*.ts",
"types": "./src/exports/*.ts"
"import": "./dist/exports/*.js",
"require": "./dist/exports/*.js",
"types": "./dist/exports/*.ts"
},
"./utilities/*": {
"import": "./dist/utilities/*.js",
"require": "./dist/utilities/*.js",
"types": "./src/utilities/*.ts"
},
"./layouts/*": {
"import": "./dist/layouts/*.js",
"require": "./dist/layouts/*.js",
"types": "./src/layouts/*.ts"
},
"./views/*": {
"import": "./dist/views/*.js",
"require": "./dist/views/*.js",
"types": "./src/views/*.ts"
},
"./routes": {
"import": "./dist/routes/index.js",
"require": "./dist/routes/index.js"
}
},
"devDependencies": {
@@ -73,22 +88,48 @@
"payload": "workspace:*"
},
"publishConfig": {
"main": "./dist/index.js",
"types": "./dist/index.js",
"main": "./dist/exports/index.js",
"types": "./dist/exports/index.d.ts",
"exports": {
"./css": {
"import": "./dist/prod/styles.css",
"require": "./dist/prod/styles.css",
"default": "./dist/prod/styles.css"
},
"./withPayload": {
"import": "./dist/withPayload.js",
"require": "./dist/withPayload.js",
"types": "./dist/withPayload.d.ts"
},
".": {
"import": "./dist/index.js",
"require": "./dist/index.js"
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./*": {
"import": "./dist/exports/*.js",
"require": "./dist/exports/*.js",
"types": "./dist/exports/*.d.ts"
"./utilities/*": {
"import": "./dist/utilities/*.js",
"require": "./dist/utilities/*.js",
"types": "./dist/utilities/*.d.ts"
},
"./layouts/*": {
"import": "./dist/layouts/*.js",
"require": "./dist/layouts/*.js",
"types": "./dist/layouts/*.d.ts"
},
"./views/*": {
"import": "./dist/views/*.js",
"require": "./dist/views/*.js",
"types": "./dist/views/*.d.ts"
},
"./app/*": {
"import": "./dist/app/*.js",
"require": "./dist/app/*.js",
"types": "./dist/app/*.d.ts"
},
"./routes": {
"import": "./dist/routes/index.js",
"require": "./dist/routes/index.js",
"types": "./dist/routes/index.d.ts"
}
},
"registry": "https://registry.npmjs.org/"

View File

@@ -0,0 +1,26 @@
import minimist from 'minimist'
const args = minimist(process.argv.slice(2))
const scriptIndex = args._.findIndex((x) => x === 'install')
const script = scriptIndex === -1 ? args._[0] : args._[scriptIndex]
const { debug } = args
import { install } from './install.js'
main()
async function main() {
if (debug) console.log({ debug, pwd: process.cwd() })
switch (script.toLowerCase()) {
case 'install': {
if (debug) console.log('Running install')
await install({ debug })
break
}
default:
console.log(`Unknown script "${script}".`)
break
}
}

View File

@@ -0,0 +1,68 @@
import fs from 'fs'
import path from 'path'
export const install = (args?: { debug?: boolean }): Promise<void> => {
const debug = args?.debug
const nextConfigPath = path.resolve(process.cwd(), 'next.config.js')
if (!fs.existsSync(nextConfigPath)) {
console.log(`No next.config.js found at ${nextConfigPath}`)
process.exit(1)
}
const apiTemplateDir = path.resolve(__dirname, '../..', 'dist', 'template/app/(payload)')
const userProjectDir = process.cwd()
if (!fs.existsSync(apiTemplateDir)) {
console.log(`Could not find template source files from ${apiTemplateDir}`)
process.exit(1)
}
if (!fs.existsSync(path.resolve(userProjectDir, 'app'))) {
console.log(`Could not find user app directory at ${userProjectDir}/app`)
process.exit(1)
}
const templateFileDest = path.resolve(userProjectDir, 'app/(payload)')
if (debug) {
console.log({
cwd: process.cwd(),
// Paths
apiTemplateDir,
templateFileDest,
userProjectDir,
})
}
// Merge api dir into user's app/api, user's files take precedence
copyRecursiveSync(apiTemplateDir, templateFileDest, debug)
process.exit(0)
}
/**
* Recursively copy files from src to dest
*/
function copyRecursiveSync(src: string, dest: string, debug?: boolean) {
const exists = fs.existsSync(src)
const stats = exists && fs.statSync(src)
const isDirectory = exists && stats.isDirectory()
if (isDirectory) {
if (debug) console.log(`Dir: ${src}\n--Dest: ${dest}`)
fs.mkdirSync(dest, { recursive: true })
fs.readdirSync(src).forEach((childItemName) => {
copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName))
})
} else {
if (debug) console.log(`File: ${src}\n--Dest: ${dest}`)
fs.copyFileSync(src, dest)
}
}
if (require.main === module) {
install({ debug: true }).catch((e) => {
console.error(e)
process.exit(1)
})
}

View File

@@ -1,9 +1,9 @@
'use client'
import { Button } from '@payloadcms/ui/elements/Button'
import { Modal, useModal } from '@payloadcms/ui/elements/Modal'
import { useFormModified } from '@payloadcms/ui/forms/Form'
import { useAuth } from '@payloadcms/ui/providers/Auth'
import { useTranslation } from '@payloadcms/ui/providers/Translation'
import { Modal, useModal } from '@payloadcms/ui'
import { Button } from '@payloadcms/ui/elements'
import { useFormModified } from '@payloadcms/ui/forms'
import { useAuth } from '@payloadcms/ui/providers'
import { useTranslation } from '@payloadcms/ui/providers'
import React, { useCallback, useEffect } from 'react'
import './index.scss'

View File

@@ -0,0 +1 @@
export { default as withPayload } from '../withPayload.js'

View File

@@ -1 +0,0 @@
export { RootLayout, metadata } from '../layouts/Root/index.js'

View File

@@ -1,8 +0,0 @@
export { GRAPHQL_PLAYGROUND_GET, GRAPHQL_POST } from '../routes/graphql/index.js'
export {
DELETE as REST_DELETE,
GET as REST_GET,
PATCH as REST_PATCH,
POST as REST_POST,
} from '../routes/rest/index.js'

View File

@@ -1 +0,0 @@
export { getPayloadHMR } from '../utilities/getPayloadHMR.js'

View File

@@ -1,3 +0,0 @@
export { EditView } from '../views/Edit/index.js'
export { NotFoundView } from '../views/NotFound/index.js'
export { type GenerateViewMetadata, RootPage, generatePageMetadata } from '../views/Root/index.js'

View File

@@ -1 +0,0 @@
export { default as withPayload } from './withPayload.js'

View File

@@ -1,10 +1,9 @@
import type { SanitizedConfig } from 'payload/types'
import { translations } from '@payloadcms/translations/client'
import { RootProvider } from '@payloadcms/ui/providers/Root'
import { RootProvider, buildComponentMap } from '@payloadcms/ui'
import '@payloadcms/ui/scss/app.scss'
import { buildComponentMap } from '@payloadcms/ui/utilities/buildComponentMap'
import { headers as getHeaders, cookies as nextCookies } from 'next/headers.js'
import { headers as getHeaders } from 'next/headers.js'
import { parseCookies } from 'payload/auth'
import { createClientConfig } from 'payload/config'
import { deepMerge } from 'payload/utilities'
@@ -13,6 +12,7 @@ import 'react-toastify/dist/ReactToastify.css'
import { getRequestLanguage } from '../../utilities/getRequestLanguage.js'
import { DefaultEditView } from '../../views/Edit/Default/index.js'
import { DefaultCell } from '../../views/List/Default/Cell/index.js'
import { DefaultListView } from '../../views/List/Default/index.js'
export const metadata = {
@@ -37,7 +37,6 @@ export const RootLayout = async ({
const lang =
getRequestLanguage({
config,
cookies,
headers,
}) ?? clientConfig.i18n.fallbackLanguage
@@ -51,17 +50,8 @@ export const RootLayout = async ({
value: language,
}))
// eslint-disable-next-line @typescript-eslint/require-await
async function switchLanguageServerAction(lang: string): Promise<void> {
'use server'
nextCookies().set({
name: `${config.cookiePrefix || 'payload'}-lng'`,
path: '/',
value: lang,
})
}
const { componentMap, wrappedChildren } = buildComponentMap({
DefaultCell,
DefaultEditView,
DefaultListView,
children,
@@ -77,8 +67,6 @@ export const RootLayout = async ({
fallbackLang={clientConfig.i18n.fallbackLanguage}
lang={lang}
languageOptions={languageOptions}
// eslint-disable-next-line react/jsx-no-bind
switchLanguageServerAction={switchLanguageServerAction}
translations={mergedTranslations[lang]}
>
{wrappedChildren}

View File

@@ -1,21 +1,13 @@
import type { Collection, PayloadRequest } from 'payload/types'
import httpStatus from 'http-status'
import { APIError, ValidationError } from 'payload/errors'
import { APIError } from 'payload/errors'
export type ErrorResponse = { data?: any; errors: unknown[]; stack?: string }
const formatErrors = (incoming: { [key: string]: unknown } | APIError): ErrorResponse => {
const formatErrors = (incoming: { [key: string]: unknown } | APIError | Error): ErrorResponse => {
if (incoming) {
// Cannot use `instanceof` to check error type: https://github.com/microsoft/TypeScript/issues/13965
// Instead, get the prototype of the incoming error and check its constructor name
const proto = Object.getPrototypeOf(incoming)
// Payload 'ValidationError' and 'APIError'
if (
(proto.constructor.name === 'ValidationError' || proto.constructor.name === 'APIError') &&
incoming.data
) {
if (incoming instanceof APIError && incoming.data) {
return {
errors: [
{
@@ -27,8 +19,8 @@ const formatErrors = (incoming: { [key: string]: unknown } | APIError): ErrorRes
}
}
// Mongoose 'ValidationError': https://mongoosejs.com/docs/api/error.html#Error.ValidationError
if (proto.constructor.name === 'ValidationError' && 'errors' in incoming && incoming.errors) {
// mongoose
if (!(incoming instanceof APIError || incoming instanceof Error) && incoming.errors) {
return {
errors: Object.keys(incoming.errors).reduce((acc, key) => {
acc.push({
@@ -66,7 +58,7 @@ const formatErrors = (incoming: { [key: string]: unknown } | APIError): ErrorRes
}
}
export const routeError = async ({
export const RouteError = async ({
collection,
err,
req,
@@ -86,9 +78,7 @@ export const routeError = async ({
}
const { config, logger } = req.payload
let response = formatErrors(err)
let status = err.status || httpStatus.INTERNAL_SERVER_ERROR
logger.error(err.stack)

View File

@@ -1,14 +1,9 @@
import type { BuildFormStateArgs } from '@payloadcms/ui/forms/buildStateFromSchema'
import type { BuildFormStateArgs, FieldSchemaMap } from '@payloadcms/ui'
import type { Field, PayloadRequest, SanitizedConfig } from 'payload/types'
import { buildStateFromSchema } from '@payloadcms/ui/forms/buildStateFromSchema'
import { reduceFieldsToValues } from '@payloadcms/ui/utilities/reduceFieldsToValues'
import { buildFieldSchemaMap, buildStateFromSchema, reduceFieldsToValues } from '@payloadcms/ui'
import httpStatus from 'http-status'
import type { FieldSchemaMap } from '../../utilities/buildFieldSchemaMap/types.js'
import { buildFieldSchemaMap } from '../../utilities/buildFieldSchemaMap/index.js'
let cached = global._payload_fieldSchemaMap
if (!cached) {

View File

@@ -1,4 +1,3 @@
import { getTranslation } from '@payloadcms/translations'
import httpStatus from 'http-status'
import { createOperation } from 'payload/operations'
import { isNumber } from 'payload/utilities'
@@ -24,7 +23,7 @@ export const create: CollectionRouteHandler = async ({ collection, req }) => {
{
doc,
message: req.t('general:successfullyCreated', {
label: getTranslation(collection.config.labels.singular, req.i18n),
label: collection.config.labels.singular,
}),
},
{

View File

@@ -1,35 +0,0 @@
import { getTranslation } from '@payloadcms/translations'
import httpStatus from 'http-status'
import { duplicateOperation } from 'payload/operations'
import { isNumber } from 'payload/utilities'
import type { CollectionRouteHandlerWithID } from '../types.js'
export const duplicate: CollectionRouteHandlerWithID = async ({ id, collection, req }) => {
const { searchParams } = req
const depth = searchParams.get('depth')
// draft defaults to true, unless explicitly set requested as false to prevent the newly duplicated document from being published
const draft = searchParams.get('draft') !== 'false'
const doc = await duplicateOperation({
id,
collection,
depth: isNumber(depth) ? Number(depth) : undefined,
draft,
req,
})
const message = req.t('general:successfullyDuplicated', {
label: getTranslation(collection.config.labels.singular, req.i18n),
})
return Response.json(
{
doc,
message,
},
{
status: httpStatus.OK,
},
)
}

View File

@@ -6,7 +6,7 @@ import path from 'path'
import { APIError } from 'payload/errors'
import { streamFile } from '../../../next-stream-file/index.js'
import { routeError } from '../routeError.js'
import { RouteError } from '../RouteError.js'
import { checkFileAccess } from './checkFileAccess.js'
// /:collectionSlug/file/:filename
@@ -64,7 +64,7 @@ export const getFile = async ({ collection, filename, req }: Args): Promise<Resp
status: httpStatus.OK,
})
} catch (error) {
return routeError({
return RouteError({
collection,
err: error,
req,

View File

@@ -12,7 +12,7 @@ import type {
} from './types.js'
import { createPayloadRequest } from '../../utilities/createPayloadRequest.js'
import { routeError } from './routeError.js'
import { RouteError } from './RouteError.js'
import { access } from './auth/access.js'
import { forgotPassword } from './auth/forgotPassword.js'
import { init } from './auth/init.js'
@@ -30,7 +30,6 @@ import { create } from './collections/create.js'
import { deleteDoc } from './collections/delete.js'
import { deleteByID } from './collections/deleteByID.js'
import { docAccess } from './collections/docAccess.js'
import { duplicate } from './collections/duplicate.js'
import { find } from './collections/find.js'
import { findByID } from './collections/findByID.js'
import { findVersionByID } from './collections/findVersionByID.js'
@@ -72,7 +71,6 @@ const endpoints = {
'doc-access-by-id': docAccess,
'doc-verify-by-id': verifyEmail,
'doc-versions-by-id': restoreVersion,
duplicate,
'first-register': registerFirstUser,
'forgot-password': forgotPassword,
login,
@@ -281,7 +279,7 @@ export const GET =
return RouteNotFoundResponse(slug)
} catch (error) {
return routeError({
return RouteError({
collection,
err: error,
req,
@@ -358,9 +356,6 @@ export const POST =
res = await (
endpoints.collection.POST[`doc-${slug2}-by-id`] as CollectionRouteHandlerWithID
)({ id: slug3, collection, req })
} else if (slug3 === 'duplicate') {
// /:collection/:id/duplicate
res = await endpoints.collection.POST.duplicate({ id: slug2, collection, req })
}
break
}
@@ -423,7 +418,7 @@ export const POST =
return RouteNotFoundResponse(slug)
} catch (error) {
return routeError({
return RouteError({
collection,
err: error,
req,
@@ -492,7 +487,7 @@ export const DELETE =
return RouteNotFoundResponse(slug)
} catch (error) {
return routeError({
return RouteError({
collection,
err: error,
req,
@@ -561,7 +556,7 @@ export const PATCH =
return RouteNotFoundResponse(slug)
} catch (error) {
return routeError({
return RouteError({
collection,
err: error,
req,

View File

@@ -10,7 +10,7 @@ import { translations } from '@payloadcms/translations/api'
import { getAuthenticatedUser } from 'payload/auth'
import { parseCookies } from 'payload/auth'
import { getDataLoader } from 'payload/utilities'
import qs from 'qs'
import QueryString from 'qs'
import { URL } from 'url'
import { getDataAndFile } from './getDataAndFile.js'
@@ -67,7 +67,6 @@ export const createPayloadRequest = async ({
}
const language = getRequestLanguage({
config,
cookies,
headers: request.headers,
})
@@ -98,10 +97,11 @@ export const createPayloadRequest = async ({
port: urlProperties.port,
protocol: urlProperties.protocol,
query: urlProperties.search
? qs.parse(urlProperties.search, {
? QueryString.parse(urlProperties.search, {
arrayLimit: 1000,
depth: 10,
ignoreQueryPrefix: true,
strictNullHandling: true,
})
: {},
routeParams: params || {},

View File

@@ -7,16 +7,19 @@ import { cookies, headers } from 'next/headers.js'
import { getRequestLanguage } from './getRequestLanguage.js'
export const getNextI18n = ({
export const getNextI18n = async ({
config,
language,
}: {
config: SanitizedConfig
language?: string
}): I18n =>
initI18n({
}): Promise<I18n> => {
const i18n = initI18n({
config: config.i18n,
context: 'client',
language: language || getRequestLanguage({ config, cookies: cookies(), headers: headers() }),
language: language || getRequestLanguage({ cookies: cookies(), headers: headers() }),
translations,
})
return i18n
}

View File

@@ -1,27 +1,24 @@
import type { ReadonlyRequestCookies } from 'next/dist/server/web/spec-extension/adapters/request-cookies.js'
import type { SanitizedConfig } from 'payload/config'
import { matchLanguage } from '@payloadcms/translations'
type GetRequestLanguageArgs = {
config: SanitizedConfig
cookies: Map<string, string> | ReadonlyRequestCookies
defaultLanguage?: string
headers: Request['headers']
}
export const getRequestLanguage = ({
config,
cookies,
defaultLanguage = 'en',
headers,
}: GetRequestLanguageArgs): string => {
const acceptLanguage = headers.get('Accept-Language')
const cookieLanguage = cookies.get(`${config.cookiePrefix || 'payload'}-lng'`)
const cookieLanguage = cookies.get('lng')
const reqLanguage =
(typeof cookieLanguage === 'string' ? cookieLanguage : cookieLanguage?.value) ||
acceptLanguage ||
(typeof cookieLanguage === 'string' ? cookieLanguage : cookieLanguage?.value) ||
defaultLanguage
return matchLanguage(reqLanguage)

View File

@@ -8,7 +8,7 @@ import type {
import { initI18n } from '@payloadcms/translations'
import { translations } from '@payloadcms/translations/client'
import { findLocaleFromCode } from '@payloadcms/ui/utilities/findLocaleFromCode'
import { findLocaleFromCode } from '@payloadcms/ui'
import { headers as getHeaders } from 'next/headers.js'
import { notFound, redirect } from 'next/navigation.js'
import { createLocalReq } from 'payload/utilities'
@@ -62,7 +62,7 @@ export const initPage = async ({
localization && localization.defaultLocale ? localization.defaultLocale : 'en'
const localeCode = localeParam || defaultLocale
const locale = localization && findLocaleFromCode(localization, localeCode)
const language = getRequestLanguage({ config: payload.config, cookies, headers })
const language = getRequestLanguage({ cookies, headers })
const i18n = initI18n({
config: payload.config.i18n,
@@ -71,19 +71,13 @@ export const initPage = async ({
translations,
})
const queryString = `${qs.stringify(searchParams, { addQueryPrefix: true })}`
const req = await createLocalReq(
{
fallbackLocale: null,
locale: locale.code,
req: {
i18n,
query: qs.parse(queryString, {
depth: 10,
ignoreQueryPrefix: true,
}),
url: `${payload.config.serverURL}${route}${searchParams ? queryString : ''}`,
url: `${payload.config.serverURL}${route}${searchParams ? `${qs.stringify(searchParams, { addQueryPrefix: true })}` : ''}`,
} as PayloadRequest,
user,
},

View File

@@ -1,5 +1,5 @@
'use client'
import { Chevron } from '@payloadcms/ui/icons/Chevron'
import { Chevron } from '@payloadcms/ui'
import * as React from 'react'
import './index.scss'

View File

@@ -1,18 +1,20 @@
'use client'
import { CopyToClipboard } from '@payloadcms/ui/elements/CopyToClipboard'
import { Gutter } from '@payloadcms/ui/elements/Gutter'
import { Checkbox } from '@payloadcms/ui/fields/Checkbox'
import { NumberField as NumberInput } from '@payloadcms/ui/fields/Number'
import { Select } from '@payloadcms/ui/fields/Select'
import { Form } from '@payloadcms/ui/forms/Form'
import { MinimizeMaximize } from '@payloadcms/ui/icons/MinimizeMaximize'
import { SetViewActions } from '@payloadcms/ui/providers/Actions'
import { useComponentMap } from '@payloadcms/ui/providers/ComponentMap'
import { useConfig } from '@payloadcms/ui/providers/Config'
import { useDocumentInfo } from '@payloadcms/ui/providers/DocumentInfo'
import { useLocale } from '@payloadcms/ui/providers/Locale'
import { useTranslation } from '@payloadcms/ui/providers/Translation'
import {
Checkbox,
CopyToClipboard,
Form,
Gutter,
MinimizeMaximize,
Number as NumberInput,
Select,
SetViewActions,
useComponentMap,
useConfig,
useDocumentInfo,
useLocale,
useTranslation,
} from '@payloadcms/ui'
import { useSearchParams } from 'next/navigation.js'
import qs from 'qs'
import * as React from 'react'

View File

@@ -1,7 +1,6 @@
'use client'
import { ReactSelect } from '@payloadcms/ui/elements/ReactSelect'
import { FieldLabel } from '@payloadcms/ui/forms/FieldLabel'
import { useTranslation } from '@payloadcms/ui/providers/Translation'
import { Label, ReactSelect } from '@payloadcms/ui'
import { useTranslation } from '@payloadcms/ui/providers'
import React from 'react'
import { ToggleTheme } from '../ToggleTheme/index.js'
@@ -14,18 +13,17 @@ export const Settings: React.FC<{
}> = (props) => {
const { className } = props
const { i18n, languageOptions, switchLanguage, t } = useTranslation()
const { i18n, languageOptions, t } = useTranslation()
return (
<div className={[baseClass, className].filter(Boolean).join(' ')}>
<h3>{t('general:payloadSettings')}</h3>
<div className={`${baseClass}__language`}>
<FieldLabel htmlFor="language-select" label={t('general:language')} />
<Label htmlFor="language-select" label={t('general:language')} />
<ReactSelect
inputId="language-select"
onChange={async ({ value }) => {
await switchLanguage(value)
}}
// TODO(i18n): wire up onChange / changeLanguage fn
// onChange={({ value }) => i18n.changeLanguage(value)}
options={languageOptions}
value={languageOptions.find((language) => language.value === i18n.language)}
/>

View File

@@ -1,15 +1,14 @@
'use client'
import type { OnChange, Theme } from '@payloadcms/ui'
import { RadioGroup } from '@payloadcms/ui/fields/RadioGroup'
import { useTheme } from '@payloadcms/ui/providers/Theme'
import { useTranslation } from '@payloadcms/ui/providers/Translation'
import { RadioGroupInput, useTheme, useTranslation } from '@payloadcms/ui'
import React, { useCallback } from 'react'
export const ToggleTheme: React.FC = () => {
const { autoMode, setTheme, theme } = useTheme()
const { t } = useTranslation()
const onChange = useCallback(
const onChange = useCallback<OnChange<Theme>>(
(newTheme) => {
setTheme(newTheme)
},
@@ -17,7 +16,7 @@ export const ToggleTheme: React.FC = () => {
)
return (
<RadioGroup
<RadioGroupInput
label={t('general:adminTheme')}
name="theme"
onChange={onChange}

View File

@@ -1,14 +1,16 @@
import type { DocumentPreferences, ServerSideEditViewProps, TypeWithID } from 'payload/types'
import type { AdminViewProps } from 'payload/types'
import { DocumentHeader } from '@payloadcms/ui/elements/DocumentHeader'
import { HydrateClientUser } from '@payloadcms/ui/elements/HydrateClientUser'
import { RenderCustomComponent } from '@payloadcms/ui/elements/RenderCustomComponent'
import { buildStateFromSchema } from '@payloadcms/ui/forms/buildStateFromSchema'
import { DocumentInfoProvider } from '@payloadcms/ui/providers/DocumentInfo'
import { FormQueryParamsProvider } from '@payloadcms/ui/providers/FormQueryParams'
import { formatDocTitle } from '@payloadcms/ui/utilities/formatDocTitle'
import { formatFields } from '@payloadcms/ui/utilities/formatFields'
import {
DocumentHeader,
DocumentInfoProvider,
FormQueryParamsProvider,
HydrateClientUser,
RenderCustomComponent,
buildStateFromSchema,
formatDocTitle,
formatFields,
} from '@payloadcms/ui'
import { notFound } from 'next/navigation.js'
import React from 'react'
@@ -17,11 +19,7 @@ import { Settings } from './Settings/index.js'
export { generateAccountMetadata } from './meta.js'
export const Account: React.FC<AdminViewProps> = async ({
initPageResult,
params,
searchParams,
}) => {
export const Account: React.FC<AdminViewProps> = async ({ initPageResult, searchParams }) => {
const {
locale,
permissions,
@@ -89,9 +87,8 @@ export const Account: React.FC<AdminViewProps> = async ({
req,
})
const viewComponentProps: ServerSideEditViewProps = {
const serverSideProps: ServerSideEditViewProps = {
initPageResult,
params,
routeSegments: [],
searchParams,
}
@@ -136,7 +133,7 @@ export const Account: React.FC<AdminViewProps> = async ({
typeof CustomAccountComponent === 'function' ? CustomAccountComponent : undefined
}
DefaultComponent={EditView}
componentProps={viewComponentProps}
componentProps={serverSideProps}
/>
</FormQueryParamsProvider>
</DocumentInfoProvider>

View File

@@ -1,8 +1,7 @@
'use client'
import type { FieldMap } from '@payloadcms/ui/utilities/buildComponentMap'
import type { FieldMap } from '@payloadcms/ui'
import { RenderFields } from '@payloadcms/ui/forms/RenderFields'
import { useComponentMap } from '@payloadcms/ui/providers/ComponentMap'
import { RenderFields, useComponentMap } from '@payloadcms/ui'
import React from 'react'
export const CreateFirstUserFields: React.FC<{

View File

@@ -1,10 +1,7 @@
import type { Field } from 'payload/types'
import type { AdminViewProps } from 'payload/types'
import { Form } from '@payloadcms/ui/forms/Form'
import { FormSubmit } from '@payloadcms/ui/forms/Submit'
import { buildStateFromSchema } from '@payloadcms/ui/forms/buildStateFromSchema'
import { mapFields } from '@payloadcms/ui/utilities/buildComponentMap'
import { Form, FormSubmit, buildStateFromSchema, mapFields } from '@payloadcms/ui'
import React from 'react'
import { CreateFirstUserFields } from './index.client.js'

View File

@@ -1,15 +1,18 @@
'use client'
import type { EntityToGroup, Group } from '@payloadcms/ui/utilities/groupNavItems'
import type { EntityToGroup, Group } from '@payloadcms/ui'
import type { Permissions } from 'payload/auth'
import { getTranslation } from '@payloadcms/translations'
import { Button } from '@payloadcms/ui/elements/Button'
import { Card } from '@payloadcms/ui/elements/Card'
import { SetViewActions } from '@payloadcms/ui/providers/Actions'
import { useAuth } from '@payloadcms/ui/providers/Auth'
import { useConfig } from '@payloadcms/ui/providers/Config'
import { useTranslation } from '@payloadcms/ui/providers/Translation'
import { EntityType, groupNavItems } from '@payloadcms/ui/utilities/groupNavItems'
import {
Button,
Card,
EntityType,
SetViewActions,
groupNavItems,
useAuth,
useConfig,
useTranslation,
} from '@payloadcms/ui'
import React, { Fragment, useEffect, useState } from 'react'
import './index.scss'

Some files were not shown because too many files have changed in this diff Show More