Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:
needs_build: ${{ steps.filter.outputs.needs_build }}
needs_tests: ${{ steps.filter.outputs.needs_tests }}
templates: ${{ steps.filter.outputs.templates }}
build_approvals: ${{ steps.filter.outputs.build_approvals }}
steps:
# https://github.com/actions/virtual-environments/issues/1187
- name: tune linux network
Expand Down Expand Up @@ -53,11 +54,18 @@ jobs:
- 'package.json'
templates:
- 'templates/**'
build_approvals:
- '.github/workflows/main.yml'
# Any dependency-graph change (packages/* or the blank workspace member) lands in
# the root lockfile; that, plus the allowlist itself, is what can introduce drift.
- 'pnpm-lock.yaml'
- 'packages/create-payload-app/src/lib/configure-pnpm-builds.ts'
- name: Log filter results
run: |
echo "needs_build: ${{ steps.filter.outputs.needs_build }}"
echo "needs_tests: ${{ steps.filter.outputs.needs_tests }}"
echo "templates: ${{ steps.filter.outputs.templates }}"
echo "build_approvals: ${{ steps.filter.outputs.build_approvals }}"

# Detects upstream Next.js canary publishes that are missing `@next/env` or
# `@next/eslint-plugin-next` for the same version (would break
Expand Down Expand Up @@ -350,8 +358,9 @@ jobs:

- name: Store Playwright's Version
run: |
# Extract the version number using a more targeted regex pattern with awk
PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --depth=0 | awk '/@playwright\/test/ {print $2}')
# `exit` after the first match: v11's `pnpm ls` lists the dep across every workspace
# project, and a multi-line value would break the GITHUB_ENV write.
PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --depth=0 | awk '/@playwright\/test/ {print $2; exit}')
echo "Playwright's Version: $PLAYWRIGHT_VERSION"
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV

Expand Down Expand Up @@ -478,8 +487,9 @@ jobs:

- name: Store Playwright's Version
run: |
# Extract the version number using a more targeted regex pattern with awk
PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --depth=0 | awk '/@playwright\/test/ {print $2}')
# `exit` after the first match: v11's `pnpm ls` lists the dep across every workspace
# project, and a multi-line value would break the GITHUB_ENV write.
PLAYWRIGHT_VERSION=$(pnpm ls @playwright/test --depth=0 | awk '/@playwright\/test/ {print $2; exit}')
echo "Playwright's Version: $PLAYWRIGHT_VERSION"
echo "PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION" >> $GITHUB_ENV

Expand Down Expand Up @@ -515,6 +525,21 @@ jobs:
MONGODB_URL: ${{ steps.db.outputs.MONGODB_URL }}
NEXT_TELEMETRY_DISABLED: 1

# Fails if create-payload-app's build-script allowlist drifts from the blank template's deps:
# scaffolds blank, installs it standalone under pnpm v11, and asserts no ERR_PNPM_IGNORED_BUILDS.
check-template-build-approvals:
runs-on: ubuntu-24.04
needs: [changes, build]
if: needs.changes.outputs.build_approvals == 'true'
steps:
- uses: actions/checkout@v5

- name: Node setup
uses: ./.github/actions/setup

- name: Check create-payload-app build approvals
run: pnpm --filter scripts check-template-build-approvals

tests-type-generation:
runs-on: ubuntu-24.04
needs: [changes, build]
Expand Down
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
pnpm 10.27.0
pnpm 11.9.0
nodejs 24.15.0
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ Examples:
- LLMS.txt: <https://payloadcms.com/llms.txt>
- LLMS-FULL.txt: <https://payloadcms.com/llms-full.txt>
- Node version: >=24.15.0
- pnpm version: ^10.27.0
- pnpm version: ^11.9.0

## Admin Panel

Expand Down
36 changes: 4 additions & 32 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@
"lint:scss": "stylelint \"packages/ui/src/**/*.scss\" \"templates/**/*.scss\"",
"obliterate-playwright-cache-macos": "rm -rf ~/Library/Caches/ms-playwright && find /System/Volumes/Data/private/var/folders -type d -name 'playwright*' -exec rm -rf {} +",
"prepare": "husky && pnpm turbo build --filter @payloadcms/typescript-plugin",
"prepare-run-test-against-prod": "pnpm bf && rm -rf test/packed && rm -rf test/node_modules && rm -rf app && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..",
"prepare-run-test-against-prod:ci": "rm -rf test/packed && rm -rf test/node_modules && rm -rf app && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --ignore-workspace && cd ..",
"prepare-run-test-against-prod:ci:frozen": "rm -rf test/packed && rm -rf test/node_modules && rm -rf app && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --frozen-lockfile --ignore-workspace && cd ..",
"prepare-run-test-against-prod": "pnpm bf && rm -rf test/packed && rm -rf test/node_modules && rm -rf app && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i && cd ..",
"prepare-run-test-against-prod:ci": "rm -rf test/packed && rm -rf test/node_modules && rm -rf app && rm -f test/pnpm-lock.yaml && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i && cd ..",
"prepare-run-test-against-prod:ci:frozen": "rm -rf test/packed && rm -rf test/node_modules && rm -rf app && pnpm run script:pack --all --no-build --dest test/packed && pnpm runts test/setupProd.ts && cd test && pnpm i --frozen-lockfile && cd ..",
"publish-prerelease": "pnpm --filter releaser publish-prerelease",
"reinstall": "pnpm clean:all && pnpm install",
"release": "pnpm --filter releaser release --tag beta",
Expand Down Expand Up @@ -203,33 +203,5 @@
"wrangler": "~4.61.1",
"zod": "4.3.6"
},
"packageManager": "pnpm@10.27.0",
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",
"@sentry/cli",
"@swc/core",
"@vercel/git-hooks",
"better-sqlite3",
"bufferutil",
"esbuild",
"mongodb-memory-server",
"sharp",
"unrs-resolver",
"utf-8-validate",
"workerd"
],
"overrides": {
"@types/request>form-data": "^2.5.6",
"amazon-cognito-identity-js>js-cookie": "^3.0.7",
"copyfiles": "$copyfiles",
"cross-env": "$cross-env",
"dotenv": "$dotenv",
"graphql": "16.8.1",
"mcp-handler>@modelcontextprotocol/sdk": "1.27.1",
"react": "$react",
"react-dom": "$react-dom",
"typescript": "$typescript"
}
}
"packageManager": "pnpm@11.9.0"
}
107 changes: 107 additions & 0 deletions packages/create-payload-app/src/lib/configure-pnpm-builds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import fse from 'fs-extra'
import path from 'path'

import type { PackageManager } from '../types.js'

/*
* Build scripts pnpm v11 must be allowed to run for the projects create-payload-app scaffolds;
* otherwise the install fails with ERR_PNPM_IGNORED_BUILDS. esbuild/sharp/unrs-resolver cover the
* default templates; workerd is required by with-cloudflare-d1.
* TODO(follow-up): the CI drift guard only verifies the blank template — extend it across every
* getValidTemplates() entry so this single list can't drift per-template.
*/
const ALLOWED_BUILDS = ['esbuild', 'sharp', 'unrs-resolver', 'workerd']

/**
* pnpm v11 fails installs that have unapproved dependency build scripts, reading approvals only from
* pnpm-workspace.yaml (the `pnpm` field in package.json is ignored). create-next-app now scaffolds a
* placeholder `allowBuilds` block (sharp/unrs-resolver left unset) plus an `ignoredBuiltDependencies`
* denylist, so we must merge rather than skip: force Payload's build scripts to `true` and remove them
* from the denylist. No-op for non-pnpm package managers.
*/
export async function ensurePnpmBuildApprovals(args: {
packageManager: PackageManager
projectDir: string
}): Promise<void> {
const { packageManager, projectDir } = args
if (packageManager !== 'pnpm') {
return
}

const workspacePath = path.join(projectDir, 'pnpm-workspace.yaml')
const existing = (await fse.pathExists(workspacePath))
? await fse.readFile(workspacePath, 'utf-8')
: ''

await fse.writeFile(workspacePath, upsertBuildApprovals(existing))
}

type WorkspaceBlock = {
key: null | string
lines: string[]
}

/**
* Merges Payload's required build approvals into a pnpm-workspace.yaml string. Pure and idempotent:
* sets each required package to `true` under `allowBuilds` (adding the block if missing) and strips
* those packages from any `ignoredBuiltDependencies` denylist. Top-level keys are detected by a
* zero-indent `key:`, with their two-space children grouped beneath them.
*/
export function upsertBuildApprovals(content: string): string {
const blocks = toTopLevelBlocks(content)

const ignored = blocks.find((block) => block.key === 'ignoredBuiltDependencies')
if (ignored) {
ignored.lines = ignored.lines.filter((line, index) => {
if (index === 0) {
return true
}
const name = /^\s*-\s*'?([^'\s]+)'?\s*$/.exec(line)?.[1]
return !(name && ALLOWED_BUILDS.includes(name))
})
}

const allow = blocks.find((block) => block.key === 'allowBuilds')
if (allow) {
for (const name of ALLOWED_BUILDS) {
const pattern = new RegExp(`^\\s+'?${escapeRegExp(name)}'?:`)
const index = allow.lines.findIndex((line) => pattern.test(line))
if (index >= 0) {
allow.lines[index] = ` ${name}: true`
} else {
allow.lines.push(` ${name}: true`)
}
}
} else {
blocks.push({
key: 'allowBuilds',
lines: ['allowBuilds:', ...ALLOWED_BUILDS.map((name) => ` ${name}: true`)],
})
}

const output = blocks.flatMap((block) => block.lines).join('\n')
return output.endsWith('\n') ? output : `${output}\n`
}

function toTopLevelBlocks(content: string): WorkspaceBlock[] {
const lines = content.length ? content.replace(/\n$/, '').split('\n') : []
const blocks: WorkspaceBlock[] = []

for (const line of lines) {
const key = /^[^\s#]/.test(line) ? /^([^:]+):/.exec(line)?.[1] : undefined
const current = blocks[blocks.length - 1]
if (key !== undefined) {
blocks.push({ key, lines: [line] })
} else if (current) {
current.lines.push(line)
} else {
blocks.push({ key: null, lines: [line] })
}
}

return blocks
}

function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
3 changes: 3 additions & 0 deletions packages/create-payload-app/src/lib/create-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from '../utils/resolvePackageVersion.js'
import { configurePayloadConfig } from './configure-payload-config.js'
import { configurePluginProject } from './configure-plugin-project.js'
import { ensurePnpmBuildApprovals } from './configure-pnpm-builds.js'
import { downloadExample } from './download-example.js'
import { downloadSkill } from './download-skill.js'
import { downloadTemplate } from './download-template.js'
Expand Down Expand Up @@ -58,6 +59,8 @@ async function installDeps(args: {
installCmd = 'bun install'
}

await ensurePnpmBuildApprovals({ packageManager, projectDir })

try {
await execa.command(installCmd, {
cwd: path.resolve(projectDir),
Expand Down
3 changes: 3 additions & 0 deletions packages/create-payload-app/src/lib/init-next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
DEFAULT_PAYLOAD_VERSION_TAG,
resolvePackageVersion,
} from '../utils/resolvePackageVersion.js'
import { ensurePnpmBuildApprovals } from './configure-pnpm-builds.js'
import { installPackages } from './install-packages.js'
import { wrapNextConfig } from './wrap-next-config.js'

Expand Down Expand Up @@ -250,6 +251,8 @@ async function installDeps(args: {
// Match graphql version of @payloadcms/next
packagesToInstall.push('graphql@^16.8.1')

await ensurePnpmBuildApprovals({ packageManager, projectDir })

return await installPackages({ packageManager, packagesToInstall, projectDir })
}

Expand Down
33 changes: 33 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,36 @@ symlink: true
nodeLinker: isolated
hoistWorkspacePackages: false # the default in pnpm v9 is true, but that can break our runtime dependency checks
savePrefix: ''
# pnpm v11 defaults this to `install`, which auto-reinstalls when node_modules looks
# stale before any `pnpm run`/`pnpm exec`. The root `prepare` script nests `pnpm turbo`
# inside the outer install's lifecycle, so that check deadlocks against the in-flight
# install (CI hangs until SIGTERM). Restore the v10 behavior.
# See https://github.com/pnpm/pnpm/issues/8865
verifyDepsBeforeRun: false

overrides:
'@types/request>form-data': '^2.5.6'
'amazon-cognito-identity-js>js-cookie': '^3.0.7'
copyfiles: $copyfiles
cross-env: $cross-env
dotenv: $dotenv
graphql: '16.8.1'
'mcp-handler>@modelcontextprotocol/sdk': '1.27.1'
react: $react
react-dom: $react-dom
typescript: $typescript

# Replaces pnpm v10 `onlyBuiltDependencies`: packages permitted to run install scripts.
allowBuilds:
'@parcel/watcher': true
'@sentry/cli': true
'@swc/core': true
'@vercel/git-hooks': true
better-sqlite3: true
bufferutil: true
esbuild: true
mongodb-memory-server: true
sharp: true
unrs-resolver: true
utf-8-validate: true
workerd: true
2 changes: 1 addition & 1 deletion templates/_template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
},
"engines": {
"node": ">=24.15.0",
"pnpm": "^9 || ^10"
"pnpm": "^9 || ^10 || ^11"
},
"pnpm": {
"onlyBuiltDependencies": [
Expand Down
2 changes: 1 addition & 1 deletion templates/blank/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
},
"engines": {
"node": ">=24.15.0",
"pnpm": "^9 || ^10"
"pnpm": "^9 || ^10 || ^11"
},
"pnpm": {
"onlyBuiltDependencies": [
Expand Down
2 changes: 1 addition & 1 deletion templates/plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
},
"engines": {
"node": ">=24.15.0",
"pnpm": "^9 || ^10"
"pnpm": "^9 || ^10 || ^11"
},
"publishConfig": {
"exports": {
Expand Down
2 changes: 1 addition & 1 deletion templates/website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
},
"engines": {
"node": ">=24.15.0",
"pnpm": "^9 || ^10"
"pnpm": "^9 || ^10 || ^11"
},
"pnpm": {
"onlyBuiltDependencies": [
Expand Down
2 changes: 1 addition & 1 deletion templates/with-cloudflare-d1/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
},
"engines": {
"node": ">=24.15.0",
"pnpm": "^9 || ^10"
"pnpm": "^9 || ^10 || ^11"
},
"pnpm": {
"onlyBuiltDependencies": [
Expand Down
2 changes: 1 addition & 1 deletion templates/with-postgres/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
},
"engines": {
"node": ">=24.15.0",
"pnpm": "^9 || ^10"
"pnpm": "^9 || ^10 || ^11"
},
"pnpm": {
"onlyBuiltDependencies": [
Expand Down
6 changes: 0 additions & 6 deletions test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,5 @@
"vitest": "4.1.6",
"wrangler": "~4.61.1",
"zod": "^4.0.0"
},
"pnpm": {
"neverBuiltDependencies": [],
"overrides": {
"graphql": "16.8.1"
}
}
}
6 changes: 6 additions & 0 deletions test/pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Makes test/ its own workspace root for the prod-test flow (`cd test && pnpm i`), without
# `--ignore-workspace` (which v11 uses to skip this file). setupProd.ts overwrites this with the
# full tarball overrides at prod-test time; the graphql override is the committed baseline.
verifyDepsBeforeRun: false
overrides:
graphql: '16.8.1'
Loading
Loading