From ac9d53331cbb22d028277addb328e501b8418a63 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Wed, 2 Sep 2026 16:47:35 +0530 Subject: [PATCH 01/13] fix(nextjs): preserve absolute Windows standalone links safely Signed-off-by: Aman Varshney --- .../nextjs/src/__tests__/assemble.test.ts | 49 +++++++++++++++ .../2-authoring/nextjs/src/control/build.ts | 62 ++++++++++++++++++- 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/packages/0-framework/2-authoring/nextjs/src/__tests__/assemble.test.ts b/packages/0-framework/2-authoring/nextjs/src/__tests__/assemble.test.ts index ea8245ea..d567207b 100644 --- a/packages/0-framework/2-authoring/nextjs/src/__tests__/assemble.test.ts +++ b/packages/0-framework/2-authoring/nextjs/src/__tests__/assemble.test.ts @@ -179,6 +179,55 @@ describe('assemble()', () => { expect(result.watch).toContain(source); }, 20_000); + test('rewrites an absolute package link to its staged in-bundle target', async () => { + const root = makeAppRoot(); + const { appRel } = writeNextBuild(root); + const standalone = path.join(root, '.next', 'standalone'); + const source = path.join(root, 'node_modules', 'pg'); + fs.mkdirSync(source, { recursive: true }); + fs.writeFileSync(path.join(source, 'index.js'), 'module.exports = "pg";\n'); + const linkDir = path.join(standalone, appRel, '.next', 'node_modules'); + fs.mkdirSync(linkDir, { recursive: true }); + fs.symlinkSync(source, path.join(linkDir, 'pg-traced'), 'dir'); + + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'prisma-composer-nextjs-cwd-')); + tmpDirs.push(cwd); + const result = await assemble({ + address: 'storefront.web', + cwd, + build: nextjs({ module: moduleUrl(root), appDir: '..' }), + }); + + const bundle = path.join(cwd, '.prisma-composer', 'artifacts', 'storefront.web', 'bundle'); + const bundledTarget = path.join(bundle, 'node_modules', 'pg'); + const bundledLink = path.join(bundle, appRel, '.next', 'node_modules', 'pg-traced'); + expect(fs.lstatSync(bundledLink).isSymbolicLink()).toBe(true); + expect(path.resolve(path.dirname(bundledLink), fs.readlinkSync(bundledLink))).toBe( + bundledTarget, + ); + expect(fs.readFileSync(path.join(bundledTarget, 'index.js'), 'utf8')).toContain('pg'); + expect(result.watch).toContain(fs.realpathSync(source)); + }, 20_000); + + test('rejects an absolute package link outside the declared tracing root', async () => { + const root = makeAppRoot(); + const { appRel } = writeNextBuild(root); + const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'prisma-composer-nextjs-outside-')); + tmpDirs.push(outside); + fs.writeFileSync(path.join(outside, 'secret.txt'), 'must not ship'); + const linkDir = path.join(root, '.next', 'standalone', appRel, '.next', 'node_modules'); + fs.mkdirSync(linkDir, { recursive: true }); + fs.symlinkSync(outside, path.join(linkDir, 'escaped'), 'dir'); + + await expect( + assemble({ + address: 'storefront.web', + cwd: root, + build: nextjs({ module: moduleUrl(root), appDir: '..' }), + }), + ).rejects.toThrow(/assembled bundle contains a symlink whose target escapes the bundle/); + }, 20_000); + test('refuses a manifest whose app location escapes its tracing root', async () => { const root = makeAppRoot(); writeNextBuild(root); diff --git a/packages/0-framework/2-authoring/nextjs/src/control/build.ts b/packages/0-framework/2-authoring/nextjs/src/control/build.ts index 85dde5e7..38515544 100644 --- a/packages/0-framework/2-authoring/nextjs/src/control/build.ts +++ b/packages/0-framework/2-authoring/nextjs/src/control/build.ts @@ -134,6 +134,63 @@ async function collectSymlinks(root: string): Promise { return links; } +/** + * Windows standalone output can contain absolute package links. An absolute + * build-machine path cannot ship, even when its target belongs to Next's + * declared trace root. Stage that exact target at the corresponding bundle + * path, then preserve the link as a relative in-bundle link. + * + * The link is never dereferenced: its target is copied separately and the + * topology remains a link. Targets outside the declared trace root are left + * untouched for the bundle validator to reject. + */ +async function stageAbsoluteStandaloneLinkTargets( + bundleDir: string, + manifest: ServerFilesManifest, +): Promise { + const tracingRoot = manifest.tracingRoot; + if (tracingRoot === undefined || (await lstatIfPresent(tracingRoot)) === undefined) return []; + + const tracedRootReal = await fs.promises.realpath(tracingRoot); + const stagedSources = new Set(); + let staged = true; + while (staged) { + staged = false; + for (const linkPath of await collectSymlinks(bundleDir)) { + const rawTarget = await fs.promises.readlink(linkPath); + if (!path.isAbsolute(rawTarget)) continue; + + let sourceReal: string; + try { + sourceReal = await fs.promises.realpath(linkPath); + } catch { + continue; + } + if (!isWithin(tracedRootReal, sourceReal)) continue; + + const target = path.join(bundleDir, path.relative(tracedRootReal, sourceReal)); + if (!isWithin(bundleDir, target) || target === linkPath) continue; + if (await hasSymlinkAncestor(bundleDir, target)) continue; + if ((await lstatIfPresent(target)) === undefined) { + await fs.promises.mkdir(path.dirname(target), { recursive: true }); + await fs.promises.cp(sourceReal, target, { recursive: true, verbatimSymlinks: true }); + } + + const sourceStat = await fs.promises.stat(sourceReal); + const relativeTarget = path.relative(path.dirname(linkPath), target); + await fs.promises.rm(linkPath, { recursive: true, force: true }); + await fs.promises.symlink( + relativeTarget, + linkPath, + sourceStat.isDirectory() ? 'dir' : 'file', + ); + stagedSources.add(sourceReal); + staged = true; + } + } + return [...stagedSources]; +} + /** In-bundle link targets that the standalone tree does not contain — the * repairs staging has to make. */ async function missingLinkTargets(bundleDir: string): Promise { @@ -234,7 +291,8 @@ export async function assemble(input: AssembleInput): Promise { recursive: true, verbatimSymlinks: true, }); - const stagedLinkTargets = await stageMissingStandaloneLinkTargets(bundleDir, manifest); + const stagedAbsoluteLinkTargets = await stageAbsoluteStandaloneLinkTargets(bundleDir, manifest); + const stagedMissingLinkTargets = await stageMissingStandaloneLinkTargets(bundleDir, manifest); // The documented copy: Next omits the client assets from standalone; place // them beside the app's server.js so it serves them (docs: `cp -r public @@ -279,7 +337,7 @@ export async function assemble(input: AssembleInput): Promise { return { dir: workDir, entry: path.posix.join('bundle', appRel.split(path.sep).join('/'), 'server.js'), - watch: [standaloneRoot, ...stagedLinkTargets], + watch: [standaloneRoot, ...stagedAbsoluteLinkTargets, ...stagedMissingLinkTargets], }; } From eab26b43c455ace3d32d5c5ab1e2055e960f888a Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Wed, 2 Sep 2026 17:01:27 +0530 Subject: [PATCH 02/13] fix(nextjs): isolate staged absolute link targets Signed-off-by: Aman Varshney --- .../nextjs/src/__tests__/assemble.test.ts | 37 +++++++++++++++++-- .../2-authoring/nextjs/src/control/build.ts | 33 +++++++++++++---- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/packages/0-framework/2-authoring/nextjs/src/__tests__/assemble.test.ts b/packages/0-framework/2-authoring/nextjs/src/__tests__/assemble.test.ts index d567207b..8c7378a5 100644 --- a/packages/0-framework/2-authoring/nextjs/src/__tests__/assemble.test.ts +++ b/packages/0-framework/2-authoring/nextjs/src/__tests__/assemble.test.ts @@ -199,16 +199,45 @@ describe('assemble()', () => { }); const bundle = path.join(cwd, '.prisma-composer', 'artifacts', 'storefront.web', 'bundle'); - const bundledTarget = path.join(bundle, 'node_modules', 'pg'); const bundledLink = path.join(bundle, appRel, '.next', 'node_modules', 'pg-traced'); + const bundledTarget = path.resolve(path.dirname(bundledLink), fs.readlinkSync(bundledLink)); expect(fs.lstatSync(bundledLink).isSymbolicLink()).toBe(true); - expect(path.resolve(path.dirname(bundledLink), fs.readlinkSync(bundledLink))).toBe( - bundledTarget, - ); + expect(bundledTarget.startsWith(`${bundle}${path.sep}`)).toBe(true); expect(fs.readFileSync(path.join(bundledTarget, 'index.js'), 'utf8')).toContain('pg'); expect(result.watch).toContain(fs.realpathSync(source)); }, 20_000); + test('does not let an occupied bundle path shadow an absolute-link target', async () => { + const root = makeAppRoot(); + const { appRel } = writeNextBuild(root); + const standalone = path.join(root, '.next', 'standalone'); + const source = path.join(root, 'node_modules', 'pg'); + fs.mkdirSync(source, { recursive: true }); + fs.writeFileSync(path.join(source, 'index.js'), 'module.exports = "original";\n'); + const occupied = path.join(standalone, 'node_modules', 'pg'); + fs.mkdirSync(occupied, { recursive: true }); + fs.writeFileSync(path.join(occupied, 'index.js'), 'module.exports = "shadow";\n'); + const linkDir = path.join(standalone, appRel, '.next', 'node_modules'); + fs.mkdirSync(linkDir, { recursive: true }); + fs.symlinkSync(source, path.join(linkDir, 'pg-traced'), 'dir'); + + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'prisma-composer-nextjs-cwd-')); + tmpDirs.push(cwd); + await assemble({ + address: 'storefront.web', + cwd, + build: nextjs({ module: moduleUrl(root), appDir: '..' }), + }); + + const bundle = path.join(cwd, '.prisma-composer', 'artifacts', 'storefront.web', 'bundle'); + const bundledLink = path.join(bundle, appRel, '.next', 'node_modules', 'pg-traced'); + const bundledTarget = path.resolve(path.dirname(bundledLink), fs.readlinkSync(bundledLink)); + expect(fs.readFileSync(path.join(bundledTarget, 'index.js'), 'utf8')).toContain('original'); + expect(fs.readFileSync(path.join(bundle, 'node_modules', 'pg', 'index.js'), 'utf8')).toContain( + 'shadow', + ); + }, 20_000); + test('rejects an absolute package link outside the declared tracing root', async () => { const root = makeAppRoot(); const { appRel } = writeNextBuild(root); diff --git a/packages/0-framework/2-authoring/nextjs/src/control/build.ts b/packages/0-framework/2-authoring/nextjs/src/control/build.ts index 38515544..71d79271 100644 --- a/packages/0-framework/2-authoring/nextjs/src/control/build.ts +++ b/packages/0-framework/2-authoring/nextjs/src/control/build.ts @@ -25,6 +25,7 @@ * Paths are file-relative (ADR-0004): `appDir` resolves against * `dirname(build.module)`. */ + import * as fs from 'node:fs'; import * as path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -134,11 +135,25 @@ async function collectSymlinks(root: string): Promise { return links; } +async function createAbsoluteLinkStagingRoot(bundleDir: string): Promise { + const base = path.join(bundleDir, '.prisma-composer-absolute-links'); + for (let suffix = 0; ; suffix += 1) { + const candidate = suffix === 0 ? base : `${base}-${suffix}`; + try { + await fs.promises.mkdir(candidate); + return candidate; + } catch (error) { + if (error instanceof Error && Reflect.get(error, 'code') === 'EEXIST') continue; + throw error; + } + } +} + /** * Windows standalone output can contain absolute package links. An absolute * build-machine path cannot ship, even when its target belongs to Next's - * declared trace root. Stage that exact target at the corresponding bundle - * path, then preserve the link as a relative in-bundle link. + * declared trace root. Stage that exact target under a fresh, collision-free + * bundle directory, then preserve the link as a relative in-bundle link. * * The link is never dereferenced: its target is copied separately and the * topology remains a link. Targets outside the declared trace root are left @@ -153,6 +168,9 @@ async function stageAbsoluteStandaloneLinkTargets( const tracedRootReal = await fs.promises.realpath(tracingRoot); const stagedSources = new Set(); + const stagedTargets = new Map(); + let stagingRoot: string | undefined; + let nextStagedTarget = 0; let staged = true; while (staged) { staged = false; @@ -168,12 +186,13 @@ async function stageAbsoluteStandaloneLinkTargets( } if (!isWithin(tracedRootReal, sourceReal)) continue; - const target = path.join(bundleDir, path.relative(tracedRootReal, sourceReal)); - if (!isWithin(bundleDir, target) || target === linkPath) continue; - if (await hasSymlinkAncestor(bundleDir, target)) continue; - if ((await lstatIfPresent(target)) === undefined) { - await fs.promises.mkdir(path.dirname(target), { recursive: true }); + let target = stagedTargets.get(sourceReal); + if (target === undefined) { + stagingRoot ??= await createAbsoluteLinkStagingRoot(bundleDir); + target = path.join(stagingRoot, String(nextStagedTarget)); + nextStagedTarget += 1; await fs.promises.cp(sourceReal, target, { recursive: true, verbatimSymlinks: true }); + stagedTargets.set(sourceReal, target); } const sourceStat = await fs.promises.stat(sourceReal); From 9c938713f80e0c71ca1a22bda78901ab7ea838f2 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Wed, 2 Sep 2026 19:26:18 +0530 Subject: [PATCH 03/13] fix(nextjs): preserve nested staged link targets Signed-off-by: Aman Varshney --- .../nextjs/src/__tests__/assemble.test.ts | 65 ++++++++++++++ .../2-authoring/nextjs/src/control/build.ts | 87 +++++++++++++------ 2 files changed, 126 insertions(+), 26 deletions(-) diff --git a/packages/0-framework/2-authoring/nextjs/src/__tests__/assemble.test.ts b/packages/0-framework/2-authoring/nextjs/src/__tests__/assemble.test.ts index 8c7378a5..82769ac7 100644 --- a/packages/0-framework/2-authoring/nextjs/src/__tests__/assemble.test.ts +++ b/packages/0-framework/2-authoring/nextjs/src/__tests__/assemble.test.ts @@ -207,6 +207,71 @@ describe('assemble()', () => { expect(result.watch).toContain(fs.realpathSync(source)); }, 20_000); + test('stages a traced sibling referenced by a relative link inside an absolute target', async () => { + const root = makeAppRoot(); + const { appRel } = writeNextBuild(root); + const standalone = path.join(root, '.next', 'standalone'); + const store = path.join(root, 'node_modules', '.pnpm', 'pkg@1.0.0', 'node_modules'); + const source = path.join(store, 'pkg'); + const sibling = path.join(store, 'helper'); + fs.mkdirSync(source, { recursive: true }); + fs.mkdirSync(sibling, { recursive: true }); + fs.writeFileSync(path.join(sibling, 'marker.txt'), 'traced sibling\n'); + fs.symlinkSync('../helper', path.join(source, 'helper'), 'dir'); + const linkDir = path.join(standalone, appRel, '.next', 'node_modules'); + fs.mkdirSync(linkDir, { recursive: true }); + fs.symlinkSync(source, path.join(linkDir, 'pkg-traced'), 'dir'); + + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'prisma-composer-nextjs-cwd-')); + tmpDirs.push(cwd); + const result = await assemble({ + address: 'storefront.web', + cwd, + build: nextjs({ module: moduleUrl(root), appDir: '..' }), + }); + + const bundle = path.join(cwd, '.prisma-composer', 'artifacts', 'storefront.web', 'bundle'); + const bundledLink = path.join(bundle, appRel, '.next', 'node_modules', 'pkg-traced'); + const bundledTarget = path.resolve(path.dirname(bundledLink), fs.readlinkSync(bundledLink)); + const nestedLink = path.join(bundledTarget, 'helper'); + const nestedTarget = path.resolve(path.dirname(nestedLink), fs.readlinkSync(nestedLink)); + expect(fs.lstatSync(nestedLink).isSymbolicLink()).toBe(true); + expect(nestedTarget.startsWith(`${bundle}${path.sep}`)).toBe(true); + expect(fs.readFileSync(path.join(nestedTarget, 'marker.txt'), 'utf8')).toContain( + 'traced sibling', + ); + expect(result.watch).toContain(fs.realpathSync(source)); + expect(result.watch).toContain(fs.realpathSync(sibling)); + }, 20_000); + + test('rejects an external nested link even when relocation would make it hit bundle content', async () => { + const root = makeAppRoot(); + const { appRel } = writeNextBuild(root); + const standalone = path.join(root, '.next', 'standalone'); + const source = path.join(root, 'pkg'); + fs.mkdirSync(source); + const outsideName = `${path.basename(root)}-outside`; + const outside = path.join(path.dirname(root), outsideName); + tmpDirs.push(outside); + fs.mkdirSync(outside); + fs.writeFileSync(path.join(outside, 'marker.txt'), 'outside trace\n'); + fs.symlinkSync(path.relative(source, outside), path.join(source, 'escaped'), 'dir'); + const collision = path.join(standalone, outsideName); + fs.mkdirSync(collision); + fs.writeFileSync(path.join(collision, 'marker.txt'), 'bundle collision\n'); + const linkDir = path.join(standalone, appRel, '.next', 'node_modules'); + fs.mkdirSync(linkDir, { recursive: true }); + fs.symlinkSync(source, path.join(linkDir, 'pkg-traced'), 'dir'); + + await expect( + assemble({ + address: 'storefront.web', + cwd: root, + build: nextjs({ module: moduleUrl(root), appDir: '..' }), + }), + ).rejects.toThrow(/symlink outside the declared tracing root/); + }, 20_000); + test('does not let an occupied bundle path shadow an absolute-link target', async () => { const root = makeAppRoot(); const { appRel } = writeNextBuild(root); diff --git a/packages/0-framework/2-authoring/nextjs/src/control/build.ts b/packages/0-framework/2-authoring/nextjs/src/control/build.ts index 71d79271..9fc03594 100644 --- a/packages/0-framework/2-authoring/nextjs/src/control/build.ts +++ b/packages/0-framework/2-authoring/nextjs/src/control/build.ts @@ -171,41 +171,76 @@ async function stageAbsoluteStandaloneLinkTargets( const stagedTargets = new Map(); let stagingRoot: string | undefined; let nextStagedTarget = 0; - let staged = true; - while (staged) { - staged = false; - for (const linkPath of await collectSymlinks(bundleDir)) { - const rawTarget = await fs.promises.readlink(linkPath); - if (!path.isAbsolute(rawTarget)) continue; - let sourceReal: string; + /** Copies one trusted target and repairs links whose meaning relocation would change. */ + async function stageSource(sourceReal: string): Promise { + const existing = stagedTargets.get(sourceReal); + if (existing !== undefined) return existing; + + stagingRoot ??= await createAbsoluteLinkStagingRoot(bundleDir); + const target = path.join(stagingRoot, String(nextStagedTarget)); + nextStagedTarget += 1; + stagedTargets.set(sourceReal, target); + + const sourceStat = await fs.promises.stat(sourceReal); + await fs.promises.cp(sourceReal, target, { recursive: true, verbatimSymlinks: true }); + stagedSources.add(sourceReal); + + if (!sourceStat.isDirectory()) return target; + for (const stagedLink of await collectSymlinks(target)) { + const sourceLink = path.join(sourceReal, path.relative(target, stagedLink)); + const rawTarget = await fs.promises.readlink(sourceLink); + const sourceTarget = path.isAbsolute(rawTarget) + ? rawTarget + : path.resolve(path.dirname(sourceLink), rawTarget); + if (!path.isAbsolute(rawTarget) && isWithin(sourceReal, sourceTarget)) continue; + + let nestedSourceReal: string; try { - sourceReal = await fs.promises.realpath(linkPath); + nestedSourceReal = await fs.promises.realpath(sourceLink); } catch { - continue; + throw new Error( + `cannot stage ${sourceReal}: it contains a dangling symlink (${sourceLink} -> ${rawTarget})`, + ); } - if (!isWithin(tracedRootReal, sourceReal)) continue; - - let target = stagedTargets.get(sourceReal); - if (target === undefined) { - stagingRoot ??= await createAbsoluteLinkStagingRoot(bundleDir); - target = path.join(stagingRoot, String(nextStagedTarget)); - nextStagedTarget += 1; - await fs.promises.cp(sourceReal, target, { recursive: true, verbatimSymlinks: true }); - stagedTargets.set(sourceReal, target); + if (!isWithin(tracedRootReal, nestedSourceReal)) { + throw new Error( + `cannot stage ${sourceReal}: it contains a symlink outside the declared tracing root (${sourceLink} -> ${rawTarget})`, + ); } - const sourceStat = await fs.promises.stat(sourceReal); - const relativeTarget = path.relative(path.dirname(linkPath), target); - await fs.promises.rm(linkPath, { recursive: true, force: true }); + const nestedTarget = await stageSource(nestedSourceReal); + const nestedStat = await fs.promises.stat(nestedSourceReal); + await fs.promises.rm(stagedLink, { recursive: true, force: true }); await fs.promises.symlink( - relativeTarget, - linkPath, - sourceStat.isDirectory() ? 'dir' : 'file', + path.relative(path.dirname(stagedLink), nestedTarget), + stagedLink, + nestedStat.isDirectory() ? 'dir' : 'file', ); - stagedSources.add(sourceReal); - staged = true; } + return target; + } + + for (const linkPath of await collectSymlinks(bundleDir)) { + const rawTarget = await fs.promises.readlink(linkPath); + if (!path.isAbsolute(rawTarget)) continue; + + let sourceReal: string; + try { + sourceReal = await fs.promises.realpath(linkPath); + } catch { + continue; + } + if (!isWithin(tracedRootReal, sourceReal)) continue; + + const target = await stageSource(sourceReal); + const sourceStat = await fs.promises.stat(sourceReal); + await fs.promises.rm(linkPath, { recursive: true, force: true }); + await fs.promises.symlink( + path.relative(path.dirname(linkPath), target), + linkPath, + sourceStat.isDirectory() ? 'dir' : 'file', + ); } return [...stagedSources]; } From a1bbdcc07c027d3e4f06741820ebcf02a66c34e7 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Thu, 10 Sep 2026 14:14:21 +0530 Subject: [PATCH 04/13] test(nextjs): verify current published CLI assembly on Windows Temporary diagnostic for published versus PR Composer using create-prisma 0.11.7. Remove after recording verification results. Signed-off-by: Aman Varshney --- .github/scripts/assemble-current-cli.mjs | 19 +++++++++ .../windows-current-cli-diagnostic.yml | 39 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 .github/scripts/assemble-current-cli.mjs create mode 100644 .github/workflows/windows-current-cli-diagnostic.yml diff --git a/.github/scripts/assemble-current-cli.mjs b/.github/scripts/assemble-current-cli.mjs new file mode 100644 index 00000000..2e10c0ec --- /dev/null +++ b/.github/scripts/assemble-current-cli.mjs @@ -0,0 +1,19 @@ +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; + +const cwd = process.cwd(); +const require = createRequire(path.join(cwd, 'package.json')); +const next = process.argv[2] === 'next'; +const { assemble } = await import( + pathToFileURL(require.resolve(`@prisma/composer/${next ? 'nextjs' : 'node'}/control`)).href +); +const module = pathToFileURL(path.join(cwd, 'service.ts')).href; +const build = next + ? { type: 'nextjs', module, appDir: '.' } + : { type: 'node', module, entry: './dist/server.mjs' }; +const result = await assemble({ build, address: 'app', cwd }); +const entry = path.join(result.dir, result.entry); +if (!fs.existsSync(entry)) throw new Error(`Missing assembled entry: ${entry}`); +console.log(JSON.stringify({ assembled: true, entry })); diff --git a/.github/workflows/windows-current-cli-diagnostic.yml b/.github/workflows/windows-current-cli-diagnostic.yml new file mode 100644 index 00000000..fd8a08d9 --- /dev/null +++ b/.github/workflows/windows-current-cli-diagnostic.yml @@ -0,0 +1,39 @@ +name: Windows current CLI diagnostic +on: + pull_request: + paths: ['.github/workflows/windows-current-cli-diagnostic.yml', '.github/scripts/assemble-current-cli.mjs'] +permissions: + contents: read +jobs: + assemble: + runs-on: windows-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + template: [next, minimal] + env: + DO_NOT_TRACK: '1' + CREATE_PRISMA_DISABLE_TELEMETRY: '1' + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + persist-credentials: false + - uses: ./.github/actions/setup + - run: pnpm install --frozen-lockfile --ignore-scripts + - run: pnpm turbo run build --filter=@prisma/composer... + - name: Scaffold published CLI with npm + run: npx --yes create-prisma@0.11.7 windows-app --json --no-deploy --template ${{ matrix.template }} --authoring typescript --package-manager npm + - name: Build + working-directory: windows-app + run: npm run build + - name: Assemble published Composer + continue-on-error: true + working-directory: windows-app + run: node ../.github/scripts/assemble-current-cli.mjs ${{ matrix.template }} + - name: Install Composer PR + working-directory: windows-app + run: npm install ../packages/9-public/composer + - name: Assemble Composer PR + working-directory: windows-app + run: node ../.github/scripts/assemble-current-cli.mjs ${{ matrix.template }} From 01803dbd9a6e513447d652ef0941e95ecd551b2a Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Thu, 10 Sep 2026 14:19:58 +0530 Subject: [PATCH 05/13] test(nextjs): remove completed Windows diagnostic Run 34456797756 reproduces published Composer escaping pg symlink failure on a real create-prisma 0.11.7 Next.js build; the PR build passes. Minimal passes both baselines. No permanent workflow added. Signed-off-by: Aman Varshney --- .github/scripts/assemble-current-cli.mjs | 19 --------- .../windows-current-cli-diagnostic.yml | 39 ------------------- 2 files changed, 58 deletions(-) delete mode 100644 .github/scripts/assemble-current-cli.mjs delete mode 100644 .github/workflows/windows-current-cli-diagnostic.yml diff --git a/.github/scripts/assemble-current-cli.mjs b/.github/scripts/assemble-current-cli.mjs deleted file mode 100644 index 2e10c0ec..00000000 --- a/.github/scripts/assemble-current-cli.mjs +++ /dev/null @@ -1,19 +0,0 @@ -import fs from 'node:fs'; -import { createRequire } from 'node:module'; -import path from 'node:path'; -import { pathToFileURL } from 'node:url'; - -const cwd = process.cwd(); -const require = createRequire(path.join(cwd, 'package.json')); -const next = process.argv[2] === 'next'; -const { assemble } = await import( - pathToFileURL(require.resolve(`@prisma/composer/${next ? 'nextjs' : 'node'}/control`)).href -); -const module = pathToFileURL(path.join(cwd, 'service.ts')).href; -const build = next - ? { type: 'nextjs', module, appDir: '.' } - : { type: 'node', module, entry: './dist/server.mjs' }; -const result = await assemble({ build, address: 'app', cwd }); -const entry = path.join(result.dir, result.entry); -if (!fs.existsSync(entry)) throw new Error(`Missing assembled entry: ${entry}`); -console.log(JSON.stringify({ assembled: true, entry })); diff --git a/.github/workflows/windows-current-cli-diagnostic.yml b/.github/workflows/windows-current-cli-diagnostic.yml deleted file mode 100644 index fd8a08d9..00000000 --- a/.github/workflows/windows-current-cli-diagnostic.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Windows current CLI diagnostic -on: - pull_request: - paths: ['.github/workflows/windows-current-cli-diagnostic.yml', '.github/scripts/assemble-current-cli.mjs'] -permissions: - contents: read -jobs: - assemble: - runs-on: windows-latest - timeout-minutes: 20 - strategy: - fail-fast: false - matrix: - template: [next, minimal] - env: - DO_NOT_TRACK: '1' - CREATE_PRISMA_DISABLE_TELEMETRY: '1' - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 - with: - persist-credentials: false - - uses: ./.github/actions/setup - - run: pnpm install --frozen-lockfile --ignore-scripts - - run: pnpm turbo run build --filter=@prisma/composer... - - name: Scaffold published CLI with npm - run: npx --yes create-prisma@0.11.7 windows-app --json --no-deploy --template ${{ matrix.template }} --authoring typescript --package-manager npm - - name: Build - working-directory: windows-app - run: npm run build - - name: Assemble published Composer - continue-on-error: true - working-directory: windows-app - run: node ../.github/scripts/assemble-current-cli.mjs ${{ matrix.template }} - - name: Install Composer PR - working-directory: windows-app - run: npm install ../packages/9-public/composer - - name: Assemble Composer PR - working-directory: windows-app - run: node ../.github/scripts/assemble-current-cli.mjs ${{ matrix.template }} From d5bba3e608cbb2aa5a4ae83c68b10c71298fe259 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Mon, 14 Sep 2026 15:04:44 +0530 Subject: [PATCH 06/13] fix(nextjs): normalize Windows watcher test paths and document staging Signed-off-by: Aman Varshney --- docs/guides/deploying.md | 6 ++++++ .../2-authoring/nextjs/src/__tests__/assemble.test.ts | 6 +++--- skills/prisma-composer-core-concepts/SKILL.md | 4 ++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/guides/deploying.md b/docs/guides/deploying.md index ce58a457..bce09613 100644 --- a/docs/guides/deploying.md +++ b/docs/guides/deploying.md @@ -34,6 +34,12 @@ for hoisted installations. On Windows it prefers `alchemy.exe`, then `alchemy.cmd`, then the extensionless shim; POSIX uses `alchemy`. An installed Windows shim must not be reported as a missing Alchemy dependency. +Next.js standalone builds on Windows may contain absolute package symlinks. +Composer stages their targets only when they are inside Next's declared +`outputFileTracingRoot`, then rewrites the links to relative paths inside the +artifact. Nested links must satisfy the same boundary. Dangling or external +targets still fail assembly; do not flatten or dereference the standalone tree. + `prisma-composer deploy` does not build for you — it assembles what your build produced: diff --git a/packages/0-framework/2-authoring/nextjs/src/__tests__/assemble.test.ts b/packages/0-framework/2-authoring/nextjs/src/__tests__/assemble.test.ts index bd4f2dbe..f9474e46 100644 --- a/packages/0-framework/2-authoring/nextjs/src/__tests__/assemble.test.ts +++ b/packages/0-framework/2-authoring/nextjs/src/__tests__/assemble.test.ts @@ -207,7 +207,7 @@ describe('assemble()', () => { expect(fs.lstatSync(bundledLink).isSymbolicLink()).toBe(true); expect(bundledTarget.startsWith(`${bundle}${path.sep}`)).toBe(true); expect(fs.readFileSync(path.join(bundledTarget, 'index.js'), 'utf8')).toContain('pg'); - expect(result.watch).toContain(fs.realpathSync(source)); + expect(result.watch).toContain(await fs.promises.realpath(source)); }, 20_000); test('stages a traced sibling referenced by a relative link inside an absolute target', async () => { @@ -243,8 +243,8 @@ describe('assemble()', () => { expect(fs.readFileSync(path.join(nestedTarget, 'marker.txt'), 'utf8')).toContain( 'traced sibling', ); - expect(result.watch).toContain(fs.realpathSync(source)); - expect(result.watch).toContain(fs.realpathSync(sibling)); + expect(result.watch).toContain(await fs.promises.realpath(source)); + expect(result.watch).toContain(await fs.promises.realpath(sibling)); }, 20_000); test('rejects an external nested link even when relocation would make it hit bundle content', async () => { diff --git a/skills/prisma-composer-core-concepts/SKILL.md b/skills/prisma-composer-core-concepts/SKILL.md index 7e5fec5b..8fbdd22f 100644 --- a/skills/prisma-composer-core-concepts/SKILL.md +++ b/skills/prisma-composer-core-concepts/SKILL.md @@ -214,6 +214,10 @@ deploy. Rules that bite: calls `load()` needs `export const dynamic = 'force-dynamic'`, because the runtime environment doesn't exist at build time and Next ignores runtime env for prerendered routes. + On Windows, absolute package links are staged from the declared + `outputFileTracingRoot` and rewritten as relative in-artifact links. + Targets outside that root, including nested links, and unresolved dangling + targets remain errors. Do not dereference the standalone tree. 4. **Always build before `deploy` or `dev`.** Neither builds for you. Deploy configuration lives in `prisma-composer.config.ts` (or `.mts`, `.mjs`, From 781e938bb9236e411281a259d55daa476d811785 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Mon, 14 Sep 2026 15:06:16 +0530 Subject: [PATCH 07/13] test(nextjs): verify published scaffold deploys from Windows Signed-off-by: Aman Varshney --- .../windows-current-cli-diagnostic.yml | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .github/workflows/windows-current-cli-diagnostic.yml diff --git a/.github/workflows/windows-current-cli-diagnostic.yml b/.github/workflows/windows-current-cli-diagnostic.yml new file mode 100644 index 00000000..15b18b3e --- /dev/null +++ b/.github/workflows/windows-current-cli-diagnostic.yml @@ -0,0 +1,68 @@ +name: Windows current CLI diagnostic +on: + pull_request: + paths: ['.github/workflows/windows-current-cli-diagnostic.yml'] +permissions: + contents: read +concurrency: + group: e2e-deploy + cancel-in-progress: false +jobs: + deploy: + if: github.event.pull_request.head.repo.full_name == github.repository + runs-on: windows-latest + timeout-minutes: 20 + env: + DO_NOT_TRACK: '1' + CREATE_PRISMA_DISABLE_TELEMETRY: '1' + PRISMA_REGION: us-east-1 + APP_NAME: windows-next-${{ github.run_id }} + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + persist-credentials: false + - uses: ./.github/actions/setup + - run: pnpm install --frozen-lockfile --ignore-scripts + - run: pnpm turbo run build --filter=@prisma/composer... + - name: Scaffold published CLI with npm + run: npx --yes create-prisma@0.12.0 $env:APP_NAME --json --no-deploy --template next --authoring typescript --package-manager npm + - name: Pack Composer PR + working-directory: packages/9-public/composer + run: npm pack --ignore-scripts + - name: Install Composer PR and build + run: | + Set-Location $env:APP_NAME + npm install ../packages/9-public/composer/prisma-composer-0.20.0.tgz + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + npm run build + - name: Deploy and verify seeded page + id: deploy + env: + PRISMA_SERVICE_TOKEN: ${{ secrets.PRISMA_SERVICE_TOKEN }} + PRISMA_WORKSPACE_ID: ${{ vars.PRISMA_WORKSPACE_ID }} + run: | + Set-Location $env:APP_NAME + $lines = npm exec --offline --yes=false -- prisma deploy module.ts --json + $lines + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $frames = $lines | ForEach-Object { $_ | ConvertFrom-Json } + $result = $frames | Where-Object kind -eq 'result' | Select-Object -Last 1 + if (-not $result.envelope.ok) { throw 'Deploy did not return success' } + $url = $result.envelope.result.summary.nodes.entities | Where-Object kind -eq 'compute-service' | Select-Object -ExpandProperty url -First 1 + if (-not $url) { throw 'No public service URL returned' } + $response = Invoke-WebRequest -Uri $url -TimeoutSec 60 + if ($response.StatusCode -ne 200 -or $response.Content -notmatch 'alice@prisma.io') { throw 'Deployed page did not return seeded data' } + Write-Output "HTTP $($response.StatusCode): deployed Next.js page contains seeded database data" + - name: Remove this diagnostic project + if: always() && steps.deploy.outcome != 'skipped' + env: + PRISMA_SERVICE_TOKEN: ${{ secrets.PRISMA_SERVICE_TOKEN }} + PRISMA_WORKSPACE_ID: ${{ vars.PRISMA_WORKSPACE_ID }} + run: | + Set-Location $env:APP_NAME + $lines = npm exec --offline --yes=false -- prisma project show $env:APP_NAME --json + if ($LASTEXITCODE -ne 0) { throw 'Could not resolve diagnostic project for cleanup' } + $result = $lines | ForEach-Object { $_ | ConvertFrom-Json } | Where-Object kind -eq 'result' | Select-Object -Last 1 + $project = $result.envelope.result.project + if (-not $project.id -or $project.name -ne $env:APP_NAME) { throw 'Refusing to delete an unexpected project' } + npm exec --offline --yes=false -- prisma project delete $project.id --confirm $project.id --json From c161ac0a5784b8f955ab73e64b04cd8d60671639 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Mon, 14 Sep 2026 15:15:49 +0530 Subject: [PATCH 08/13] test(nextjs): verify rendered seed data and clean deployment versions Signed-off-by: Aman Varshney --- .../workflows/windows-current-cli-diagnostic.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/windows-current-cli-diagnostic.yml b/.github/workflows/windows-current-cli-diagnostic.yml index 15b18b3e..14e21f5b 100644 --- a/.github/workflows/windows-current-cli-diagnostic.yml +++ b/.github/workflows/windows-current-cli-diagnostic.yml @@ -51,7 +51,7 @@ jobs: $url = $result.envelope.result.summary.nodes.entities | Where-Object kind -eq 'compute-service' | Select-Object -ExpandProperty url -First 1 if (-not $url) { throw 'No public service URL returned' } $response = Invoke-WebRequest -Uri $url -TimeoutSec 60 - if ($response.StatusCode -ne 200 -or $response.Content -notmatch 'alice@prisma.io') { throw 'Deployed page did not return seeded data' } + if ($response.StatusCode -ne 200 -or $response.Content -notmatch 'Alice' -or $response.Content -notmatch 'Bob' -or $response.Content -notmatch 'Carol') { throw 'Deployed page did not return seeded data' } Write-Output "HTTP $($response.StatusCode): deployed Next.js page contains seeded database data" - name: Remove this diagnostic project if: always() && steps.deploy.outcome != 'skipped' @@ -65,4 +65,18 @@ jobs: $result = $lines | ForEach-Object { $_ | ConvertFrom-Json } | Where-Object kind -eq 'result' | Select-Object -Last 1 $project = $result.envelope.result.project if (-not $project.id -or $project.name -ne $env:APP_NAME) { throw 'Refusing to delete an unexpected project' } + $lines = npm exec --offline --yes=false -- prisma service list --project $project.id --json + if ($LASTEXITCODE -ne 0) { throw 'Could not list diagnostic services' } + $result = $lines | ForEach-Object { $_ | ConvertFrom-Json } | Where-Object kind -eq 'result' | Select-Object -Last 1 + foreach ($service in $result.envelope.result.services) { + $lines = npm exec --offline --yes=false -- prisma service version list $service.id --project $project.id --json + if ($LASTEXITCODE -ne 0) { throw 'Could not list diagnostic versions' } + $versions = $lines | ForEach-Object { $_ | ConvertFrom-Json } | Where-Object kind -eq 'result' | Select-Object -Last 1 + foreach ($version in $versions.envelope.result.versions) { + npm exec --offline --yes=false -- prisma service version stop $version.id --json + if ($LASTEXITCODE -ne 0) { throw 'Could not stop diagnostic version' } + npm exec --offline --yes=false -- prisma service version delete $version.id --confirm $version.id --json + if ($LASTEXITCODE -ne 0) { throw 'Could not delete diagnostic version' } + } + } npm exec --offline --yes=false -- prisma project delete $project.id --confirm $project.id --json From 921be918441ebafd0d106eb0b8c6378d359d30ac Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Mon, 14 Sep 2026 15:24:56 +0530 Subject: [PATCH 09/13] test(nextjs): align Composer provider versions in Windows diagnostic Signed-off-by: Aman Varshney --- .github/workflows/windows-current-cli-diagnostic.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/windows-current-cli-diagnostic.yml b/.github/workflows/windows-current-cli-diagnostic.yml index 14e21f5b..2c2bc3b5 100644 --- a/.github/workflows/windows-current-cli-diagnostic.yml +++ b/.github/workflows/windows-current-cli-diagnostic.yml @@ -15,8 +15,10 @@ jobs: env: DO_NOT_TRACK: '1' CREATE_PRISMA_DISABLE_TELEMETRY: '1' + NEXT_TELEMETRY_DISABLED: '1' PRISMA_REGION: us-east-1 APP_NAME: windows-next-${{ github.run_id }} + APP_DIR: ${{ runner.temp }}/windows-next-${{ github.run_id }} steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 with: @@ -25,14 +27,15 @@ jobs: - run: pnpm install --frozen-lockfile --ignore-scripts - run: pnpm turbo run build --filter=@prisma/composer... - name: Scaffold published CLI with npm + working-directory: ${{ runner.temp }} run: npx --yes create-prisma@0.12.0 $env:APP_NAME --json --no-deploy --template next --authoring typescript --package-manager npm - name: Pack Composer PR working-directory: packages/9-public/composer run: npm pack --ignore-scripts - name: Install Composer PR and build run: | - Set-Location $env:APP_NAME - npm install ../packages/9-public/composer/prisma-composer-0.20.0.tgz + Set-Location $env:APP_DIR + npm install "$env:GITHUB_WORKSPACE/packages/9-public/composer/prisma-composer-0.20.0.tgz" @prisma/composer-prisma-cloud@0.20.0 @prisma/orm-postgres@8.0.0-rc.11 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } npm run build - name: Deploy and verify seeded page @@ -41,7 +44,7 @@ jobs: PRISMA_SERVICE_TOKEN: ${{ secrets.PRISMA_SERVICE_TOKEN }} PRISMA_WORKSPACE_ID: ${{ vars.PRISMA_WORKSPACE_ID }} run: | - Set-Location $env:APP_NAME + Set-Location $env:APP_DIR $lines = npm exec --offline --yes=false -- prisma deploy module.ts --json $lines if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } @@ -59,7 +62,7 @@ jobs: PRISMA_SERVICE_TOKEN: ${{ secrets.PRISMA_SERVICE_TOKEN }} PRISMA_WORKSPACE_ID: ${{ vars.PRISMA_WORKSPACE_ID }} run: | - Set-Location $env:APP_NAME + Set-Location $env:APP_DIR $lines = npm exec --offline --yes=false -- prisma project show $env:APP_NAME --json if ($LASTEXITCODE -ne 0) { throw 'Could not resolve diagnostic project for cleanup' } $result = $lines | ForEach-Object { $_ | ConvertFrom-Json } | Where-Object kind -eq 'result' | Select-Object -Last 1 From 4737bc5431e6272307c1643d8961c18abfd2b489 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Mon, 14 Sep 2026 15:25:43 +0530 Subject: [PATCH 10/13] test(nextjs): resolve the temporary app path on the runner Signed-off-by: Aman Varshney --- .github/workflows/windows-current-cli-diagnostic.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/windows-current-cli-diagnostic.yml b/.github/workflows/windows-current-cli-diagnostic.yml index 2c2bc3b5..c6cb50c7 100644 --- a/.github/workflows/windows-current-cli-diagnostic.yml +++ b/.github/workflows/windows-current-cli-diagnostic.yml @@ -18,7 +18,6 @@ jobs: NEXT_TELEMETRY_DISABLED: '1' PRISMA_REGION: us-east-1 APP_NAME: windows-next-${{ github.run_id }} - APP_DIR: ${{ runner.temp }}/windows-next-${{ github.run_id }} steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 with: @@ -34,7 +33,7 @@ jobs: run: npm pack --ignore-scripts - name: Install Composer PR and build run: | - Set-Location $env:APP_DIR + Set-Location "$env:RUNNER_TEMP/$env:APP_NAME" npm install "$env:GITHUB_WORKSPACE/packages/9-public/composer/prisma-composer-0.20.0.tgz" @prisma/composer-prisma-cloud@0.20.0 @prisma/orm-postgres@8.0.0-rc.11 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } npm run build @@ -44,7 +43,7 @@ jobs: PRISMA_SERVICE_TOKEN: ${{ secrets.PRISMA_SERVICE_TOKEN }} PRISMA_WORKSPACE_ID: ${{ vars.PRISMA_WORKSPACE_ID }} run: | - Set-Location $env:APP_DIR + Set-Location "$env:RUNNER_TEMP/$env:APP_NAME" $lines = npm exec --offline --yes=false -- prisma deploy module.ts --json $lines if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } @@ -62,7 +61,7 @@ jobs: PRISMA_SERVICE_TOKEN: ${{ secrets.PRISMA_SERVICE_TOKEN }} PRISMA_WORKSPACE_ID: ${{ vars.PRISMA_WORKSPACE_ID }} run: | - Set-Location $env:APP_DIR + Set-Location "$env:RUNNER_TEMP/$env:APP_NAME" $lines = npm exec --offline --yes=false -- prisma project show $env:APP_NAME --json if ($LASTEXITCODE -ne 0) { throw 'Could not resolve diagnostic project for cleanup' } $result = $lines | ForEach-Object { $_ | ConvertFrom-Json } | Where-Object kind -eq 'result' | Select-Object -Last 1 From cc2be6e786cc3de350b700b1b6fe76bd856a368b Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Mon, 14 Sep 2026 15:37:42 +0530 Subject: [PATCH 11/13] test: remove temporary Windows deployment diagnostic from the PR Signed-off-by: Aman Varshney --- .../windows-current-cli-diagnostic.yml | 84 ------------------- 1 file changed, 84 deletions(-) delete mode 100644 .github/workflows/windows-current-cli-diagnostic.yml diff --git a/.github/workflows/windows-current-cli-diagnostic.yml b/.github/workflows/windows-current-cli-diagnostic.yml deleted file mode 100644 index c6cb50c7..00000000 --- a/.github/workflows/windows-current-cli-diagnostic.yml +++ /dev/null @@ -1,84 +0,0 @@ -name: Windows current CLI diagnostic -on: - pull_request: - paths: ['.github/workflows/windows-current-cli-diagnostic.yml'] -permissions: - contents: read -concurrency: - group: e2e-deploy - cancel-in-progress: false -jobs: - deploy: - if: github.event.pull_request.head.repo.full_name == github.repository - runs-on: windows-latest - timeout-minutes: 20 - env: - DO_NOT_TRACK: '1' - CREATE_PRISMA_DISABLE_TELEMETRY: '1' - NEXT_TELEMETRY_DISABLED: '1' - PRISMA_REGION: us-east-1 - APP_NAME: windows-next-${{ github.run_id }} - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 - with: - persist-credentials: false - - uses: ./.github/actions/setup - - run: pnpm install --frozen-lockfile --ignore-scripts - - run: pnpm turbo run build --filter=@prisma/composer... - - name: Scaffold published CLI with npm - working-directory: ${{ runner.temp }} - run: npx --yes create-prisma@0.12.0 $env:APP_NAME --json --no-deploy --template next --authoring typescript --package-manager npm - - name: Pack Composer PR - working-directory: packages/9-public/composer - run: npm pack --ignore-scripts - - name: Install Composer PR and build - run: | - Set-Location "$env:RUNNER_TEMP/$env:APP_NAME" - npm install "$env:GITHUB_WORKSPACE/packages/9-public/composer/prisma-composer-0.20.0.tgz" @prisma/composer-prisma-cloud@0.20.0 @prisma/orm-postgres@8.0.0-rc.11 - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - npm run build - - name: Deploy and verify seeded page - id: deploy - env: - PRISMA_SERVICE_TOKEN: ${{ secrets.PRISMA_SERVICE_TOKEN }} - PRISMA_WORKSPACE_ID: ${{ vars.PRISMA_WORKSPACE_ID }} - run: | - Set-Location "$env:RUNNER_TEMP/$env:APP_NAME" - $lines = npm exec --offline --yes=false -- prisma deploy module.ts --json - $lines - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - $frames = $lines | ForEach-Object { $_ | ConvertFrom-Json } - $result = $frames | Where-Object kind -eq 'result' | Select-Object -Last 1 - if (-not $result.envelope.ok) { throw 'Deploy did not return success' } - $url = $result.envelope.result.summary.nodes.entities | Where-Object kind -eq 'compute-service' | Select-Object -ExpandProperty url -First 1 - if (-not $url) { throw 'No public service URL returned' } - $response = Invoke-WebRequest -Uri $url -TimeoutSec 60 - if ($response.StatusCode -ne 200 -or $response.Content -notmatch 'Alice' -or $response.Content -notmatch 'Bob' -or $response.Content -notmatch 'Carol') { throw 'Deployed page did not return seeded data' } - Write-Output "HTTP $($response.StatusCode): deployed Next.js page contains seeded database data" - - name: Remove this diagnostic project - if: always() && steps.deploy.outcome != 'skipped' - env: - PRISMA_SERVICE_TOKEN: ${{ secrets.PRISMA_SERVICE_TOKEN }} - PRISMA_WORKSPACE_ID: ${{ vars.PRISMA_WORKSPACE_ID }} - run: | - Set-Location "$env:RUNNER_TEMP/$env:APP_NAME" - $lines = npm exec --offline --yes=false -- prisma project show $env:APP_NAME --json - if ($LASTEXITCODE -ne 0) { throw 'Could not resolve diagnostic project for cleanup' } - $result = $lines | ForEach-Object { $_ | ConvertFrom-Json } | Where-Object kind -eq 'result' | Select-Object -Last 1 - $project = $result.envelope.result.project - if (-not $project.id -or $project.name -ne $env:APP_NAME) { throw 'Refusing to delete an unexpected project' } - $lines = npm exec --offline --yes=false -- prisma service list --project $project.id --json - if ($LASTEXITCODE -ne 0) { throw 'Could not list diagnostic services' } - $result = $lines | ForEach-Object { $_ | ConvertFrom-Json } | Where-Object kind -eq 'result' | Select-Object -Last 1 - foreach ($service in $result.envelope.result.services) { - $lines = npm exec --offline --yes=false -- prisma service version list $service.id --project $project.id --json - if ($LASTEXITCODE -ne 0) { throw 'Could not list diagnostic versions' } - $versions = $lines | ForEach-Object { $_ | ConvertFrom-Json } | Where-Object kind -eq 'result' | Select-Object -Last 1 - foreach ($version in $versions.envelope.result.versions) { - npm exec --offline --yes=false -- prisma service version stop $version.id --json - if ($LASTEXITCODE -ne 0) { throw 'Could not stop diagnostic version' } - npm exec --offline --yes=false -- prisma service version delete $version.id --confirm $version.id --json - if ($LASTEXITCODE -ne 0) { throw 'Could not delete diagnostic version' } - } - } - npm exec --offline --yes=false -- prisma project delete $project.id --confirm $project.id --json From 00f20571edc990f061f5a5b683463a20d109785a Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Mon, 14 Sep 2026 15:46:35 +0530 Subject: [PATCH 12/13] test: verify the npm minimum and real Next assembly on Windows Signed-off-by: Aman Varshney --- .github/scripts/assemble-current-cli.mjs | 15 ++++++ .../windows-npm-assembly-diagnostic.yml | 53 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 .github/scripts/assemble-current-cli.mjs create mode 100644 .github/workflows/windows-npm-assembly-diagnostic.yml diff --git a/.github/scripts/assemble-current-cli.mjs b/.github/scripts/assemble-current-cli.mjs new file mode 100644 index 00000000..f7288ce7 --- /dev/null +++ b/.github/scripts/assemble-current-cli.mjs @@ -0,0 +1,15 @@ +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; + +const cwd = process.cwd(); +const require = createRequire(path.join(cwd, 'package.json')); +const { assemble } = await import( + pathToFileURL(require.resolve('@prisma/composer/nextjs/control')).href +); +const module = pathToFileURL(path.join(cwd, 'service.ts')).href; +const result = await assemble({ build: { type: 'nextjs', module, appDir: '.' }, address: 'app', cwd }); +const entry = path.join(result.dir, result.entry); +if (!fs.existsSync(entry)) throw new Error(`Missing assembled entry: ${entry}`); +console.log(JSON.stringify({ assembled: true, entry })); diff --git a/.github/workflows/windows-npm-assembly-diagnostic.yml b/.github/workflows/windows-npm-assembly-diagnostic.yml new file mode 100644 index 00000000..b46f7e66 --- /dev/null +++ b/.github/workflows/windows-npm-assembly-diagnostic.yml @@ -0,0 +1,53 @@ +name: Windows npm assembly diagnostic +on: + pull_request: + paths: ['.github/workflows/windows-npm-assembly-diagnostic.yml', '.github/scripts/assemble-current-cli.mjs'] +permissions: + contents: read +jobs: + assemble: + runs-on: windows-latest + timeout-minutes: 20 + env: + DO_NOT_TRACK: '1' + CREATE_PRISMA_DISABLE_TELEMETRY: '1' + NEXT_TELEMETRY_DISABLED: '1' + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + persist-credentials: false + - uses: ./.github/actions/setup + - name: Record toolchains and install the supported npm minimum + run: | + node --version + npm --version + Get-Command node,npm,npx | Format-Table Name,Source + Set-Location $env:RUNNER_TEMP + node --version + npm --version + Get-Command node,npm,npx | Format-Table Name,Source + npm install --global npm@11.6.0 + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $prefix = npm prefix --global + $prefix | Out-File -FilePath $env:GITHUB_PATH -Append + - name: Scaffold published preview with npm 11.6.0 + working-directory: ${{ runner.temp }} + run: | + node --version + $npmVersion = npm --version + Write-Output "npm $npmVersion" + if ($npmVersion -ne '11.6.0') { throw 'The test must use npm 11.6.0' } + npx --yes create-prisma@0.12.0-pr.99.335.1 windows-supported-npm --json --no-deploy --template next --authoring typescript --package-manager npm + - run: pnpm install --frozen-lockfile --ignore-scripts + - run: pnpm turbo run build --filter=@prisma/composer... + - name: Pack Composer PR + working-directory: packages/9-public/composer + run: npm pack --ignore-scripts + - name: Build and assemble the actual scaffold + run: | + Set-Location "$env:RUNNER_TEMP/windows-supported-npm" + npm install "$env:GITHUB_WORKSPACE/packages/9-public/composer/prisma-composer-0.20.0.tgz" @prisma/composer-prisma-cloud@0.20.0 @prisma/orm-postgres@8.0.0-rc.11 + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + npm run build + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + node "$env:GITHUB_WORKSPACE/.github/scripts/assemble-current-cli.mjs" From 4f5c68dc00c4007cfc0e06de23bcdbf68c837541 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Mon, 14 Sep 2026 15:51:55 +0530 Subject: [PATCH 13/13] test: remove the one-off npm and assembly diagnostic Signed-off-by: Aman Varshney --- .github/scripts/assemble-current-cli.mjs | 15 ------ .../windows-npm-assembly-diagnostic.yml | 53 ------------------- 2 files changed, 68 deletions(-) delete mode 100644 .github/scripts/assemble-current-cli.mjs delete mode 100644 .github/workflows/windows-npm-assembly-diagnostic.yml diff --git a/.github/scripts/assemble-current-cli.mjs b/.github/scripts/assemble-current-cli.mjs deleted file mode 100644 index f7288ce7..00000000 --- a/.github/scripts/assemble-current-cli.mjs +++ /dev/null @@ -1,15 +0,0 @@ -import fs from 'node:fs'; -import { createRequire } from 'node:module'; -import path from 'node:path'; -import { pathToFileURL } from 'node:url'; - -const cwd = process.cwd(); -const require = createRequire(path.join(cwd, 'package.json')); -const { assemble } = await import( - pathToFileURL(require.resolve('@prisma/composer/nextjs/control')).href -); -const module = pathToFileURL(path.join(cwd, 'service.ts')).href; -const result = await assemble({ build: { type: 'nextjs', module, appDir: '.' }, address: 'app', cwd }); -const entry = path.join(result.dir, result.entry); -if (!fs.existsSync(entry)) throw new Error(`Missing assembled entry: ${entry}`); -console.log(JSON.stringify({ assembled: true, entry })); diff --git a/.github/workflows/windows-npm-assembly-diagnostic.yml b/.github/workflows/windows-npm-assembly-diagnostic.yml deleted file mode 100644 index b46f7e66..00000000 --- a/.github/workflows/windows-npm-assembly-diagnostic.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Windows npm assembly diagnostic -on: - pull_request: - paths: ['.github/workflows/windows-npm-assembly-diagnostic.yml', '.github/scripts/assemble-current-cli.mjs'] -permissions: - contents: read -jobs: - assemble: - runs-on: windows-latest - timeout-minutes: 20 - env: - DO_NOT_TRACK: '1' - CREATE_PRISMA_DISABLE_TELEMETRY: '1' - NEXT_TELEMETRY_DISABLED: '1' - steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 - with: - persist-credentials: false - - uses: ./.github/actions/setup - - name: Record toolchains and install the supported npm minimum - run: | - node --version - npm --version - Get-Command node,npm,npx | Format-Table Name,Source - Set-Location $env:RUNNER_TEMP - node --version - npm --version - Get-Command node,npm,npx | Format-Table Name,Source - npm install --global npm@11.6.0 - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - $prefix = npm prefix --global - $prefix | Out-File -FilePath $env:GITHUB_PATH -Append - - name: Scaffold published preview with npm 11.6.0 - working-directory: ${{ runner.temp }} - run: | - node --version - $npmVersion = npm --version - Write-Output "npm $npmVersion" - if ($npmVersion -ne '11.6.0') { throw 'The test must use npm 11.6.0' } - npx --yes create-prisma@0.12.0-pr.99.335.1 windows-supported-npm --json --no-deploy --template next --authoring typescript --package-manager npm - - run: pnpm install --frozen-lockfile --ignore-scripts - - run: pnpm turbo run build --filter=@prisma/composer... - - name: Pack Composer PR - working-directory: packages/9-public/composer - run: npm pack --ignore-scripts - - name: Build and assemble the actual scaffold - run: | - Set-Location "$env:RUNNER_TEMP/windows-supported-npm" - npm install "$env:GITHUB_WORKSPACE/packages/9-public/composer/prisma-composer-0.20.0.tgz" @prisma/composer-prisma-cloud@0.20.0 @prisma/orm-postgres@8.0.0-rc.11 - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - npm run build - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } - node "$env:GITHUB_WORKSPACE/.github/scripts/assemble-current-cli.mjs"