diff --git a/docs/guides/deploying.md b/docs/guides/deploying.md index 5eb7b181..c894d477 100644 --- a/docs/guides/deploying.md +++ b/docs/guides/deploying.md @@ -29,6 +29,11 @@ token is the only authentication. ## Build first +Composer resolves Alchemy from the nearest `node_modules/.bin`, walking up +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. + `prisma-composer deploy` does not build for you — it assembles what your build produced: diff --git a/packages/0-framework/3-tooling/cli/src/__tests__/run-alchemy.test.ts b/packages/0-framework/3-tooling/cli/src/__tests__/run-alchemy.test.ts index aeec0776..cf4e0e33 100644 --- a/packages/0-framework/3-tooling/cli/src/__tests__/run-alchemy.test.ts +++ b/packages/0-framework/3-tooling/cli/src/__tests__/run-alchemy.test.ts @@ -44,6 +44,17 @@ afterEach(() => { }); describe('resolveAlchemyBin()', () => { + test.skipIf(process.platform !== 'win32')('finds Windows-only package-manager shims', () => { + for (const filename of ['alchemy.cmd', 'alchemy.exe']) { + const dir = makeTmpDir(); + const binDir = path.join(dir, 'node_modules', '.bin'); + fs.mkdirSync(binDir, { recursive: true }); + const bin = path.join(binDir, filename); + fs.writeFileSync(bin, ''); + expect(resolveAlchemyBin(dir)).toBe(bin); + } + }); + test('finds node_modules/.bin/alchemy in the given directory', () => { const dir = makeTmpDir(); const bin = installFakeAlchemy(dir); @@ -177,6 +188,39 @@ describe('alchemyInvocation()', () => { }); describe('spawnAlchemy()', () => { + test.skipIf(process.platform !== 'win32')('runs a cmd shim with literal arguments', async () => { + const dir = makeTmpDir(); + const script = path.join(dir, 'capture.cjs'); + const captureFile = path.join(dir, 'capture.json'); + fs.writeFileSync( + script, + 'require("node:fs").writeFileSync(process.env.CAPTURE_FILE, JSON.stringify(process.argv.slice(2)));', + ); + const binDir = path.join(dir, 'node_modules', '.bin'); + fs.mkdirSync(binDir, { recursive: true }); + fs.writeFileSync( + path.join(binDir, 'alchemy.cmd'), + `@echo off\r\n"${process.execPath}" "${script}" %*\r\n`, + ); + const stage = 'spaces & symbols'; + expect( + await spawnAlchemy({ + action: 'deploy', + stackFileRelativePath: '.prisma-composer/alchemy.run.ts', + stage, + cwd: dir, + env: { CAPTURE_FILE: captureFile }, + }), + ).toEqual({ exitCode: 0, signal: null }); + expect(JSON.parse(fs.readFileSync(captureFile, 'utf8'))).toEqual([ + 'deploy', + '.prisma-composer/alchemy.run.ts', + '--yes', + '--stage', + stage, + ]); + }); + test('runs the invocation in its cwd with its env additions merged over the invoking environment', async () => { const dir = makeTmpDir(); const captureFile = path.join(dir, 'capture.json'); diff --git a/packages/0-framework/3-tooling/cli/src/run-alchemy.ts b/packages/0-framework/3-tooling/cli/src/run-alchemy.ts index d7f7e150..eed32389 100644 --- a/packages/0-framework/3-tooling/cli/src/run-alchemy.ts +++ b/packages/0-framework/3-tooling/cli/src/run-alchemy.ts @@ -16,12 +16,16 @@ import * as path from 'node:path'; import { CliStructuredError } from '@internal/foundation/errors'; import spawn from 'cross-spawn'; -/** Walks up from `startDir` looking for `node_modules/.bin/alchemy`. */ +/** Walks up from `startDir` looking for the installed Alchemy executable. */ export function resolveAlchemyBin(startDir: string): string { + const names = + process.platform === 'win32' ? ['alchemy.exe', 'alchemy.cmd', 'alchemy'] : ['alchemy']; let dir = startDir; while (true) { - const candidate = path.join(dir, 'node_modules', '.bin', 'alchemy'); - if (fs.existsSync(candidate)) return candidate; + for (const name of names) { + const candidate = path.join(dir, 'node_modules', '.bin', name); + if (fs.existsSync(candidate)) return candidate; + } const parent = path.dirname(dir); if (parent === dir) { throw new CliStructuredError( diff --git a/skills/prisma-composer-core-concepts/SKILL.md b/skills/prisma-composer-core-concepts/SKILL.md index 69d65a8c..74f79362 100644 --- a/skills/prisma-composer-core-concepts/SKILL.md +++ b/skills/prisma-composer-core-concepts/SKILL.md @@ -291,6 +291,11 @@ that arrives as an ordinary, exactly-pinned npm dependency of imports or configures it; consult alchemy's own docs for the engine itself. What matters operationally: +Alchemy is resolved from the nearest `node_modules/.bin`, including hoisted +ancestor directories. Windows resolves `alchemy.exe`, then `alchemy.cmd`, +then the extensionless shim; POSIX resolves `alchemy`. No global Alchemy +installation is needed. + 1. Deploy and destroy write the pipeline's results to a generated, gitignored stack file at `.prisma-composer/alchemy.run.ts`, then run the alchemy CLI against it as a child process; `dev` does the same at