@prisma-next/cli 0.5.0-dev.21 → 0.5.0-dev.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/migration-cli.d.mts.map +1 -1
- package/dist/migration-cli.mjs +21 -13
- package/dist/migration-cli.mjs.map +1 -1
- package/package.json +14 -14
- package/src/migration-cli.ts +21 -13
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migration-cli.d.mts","names":[],"sources":["../src/migration-cli.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"migration-cli.d.mts","names":[],"sources":["../src/migration-cli.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;;;KAsEY,oBAAA,2BAA+C;;;;;;;;;;;;;cAiE9C,YAAA;;;;;;;;;;;;;oDAa6C,uBAAuB"}
|
package/dist/migration-cli.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { dirname, join } from "pathe";
|
|
|
4
4
|
import { createControlStack } from "@prisma-next/framework-components/control";
|
|
5
5
|
import { errorMigrationTargetMismatch } from "@prisma-next/errors/migration";
|
|
6
6
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { MigrationToolsError, errorInvalidJson } from "@prisma-next/migration-tools/errors";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
8
9
|
import { buildMigrationArtifacts, isDirectEntrypoint, printMigrationHelp } from "@prisma-next/migration-tools/migration";
|
|
9
10
|
|
|
@@ -125,7 +126,7 @@ var MigrationCLI = class {
|
|
|
125
126
|
});
|
|
126
127
|
serializeMigrationToDisk(new MigrationClass(createControlStack(config)), migrationDir, args.dryRun);
|
|
127
128
|
} catch (err) {
|
|
128
|
-
if (CliStructuredError.is(err)) process.stderr.write(`${err.message}: ${err.why}\n`);
|
|
129
|
+
if (CliStructuredError.is(err) || MigrationToolsError.is(err)) process.stderr.write(`${err.message}: ${err.why}\n`);
|
|
129
130
|
else process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
130
131
|
process.exitCode = 1;
|
|
131
132
|
}
|
|
@@ -133,17 +134,24 @@ var MigrationCLI = class {
|
|
|
133
134
|
};
|
|
134
135
|
/**
|
|
135
136
|
* Read a previously-scaffolded `migration.json` from disk, returning
|
|
136
|
-
* `null` when the file is missing
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
137
|
+
* `null` when the file is missing and throwing `MIGRATION.INVALID_JSON`
|
|
138
|
+
* when the file is present but cannot be parsed as JSON. The CLI feeds
|
|
139
|
+
* this into `buildMigrationArtifacts` so the pure builder can preserve
|
|
140
|
+
* fields owned by `migration plan` (contract bookends, hints, labels,
|
|
141
|
+
* `createdAt`) across re-emits.
|
|
140
142
|
*
|
|
141
|
-
* Author-time path: this loader
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
143
|
+
* Author-time path: this loader still does not verify the manifest hash
|
|
144
|
+
* or schema — that is the apply-time loader's job. Hash mismatch is the
|
|
145
|
+
* *expected* outcome of a re-author (the developer's source changes
|
|
146
|
+
* invalidate the prior hash by construction), and verification here
|
|
147
|
+
* would block legitimate regenerations. Syntactic JSON-parse failure,
|
|
148
|
+
* however, is now surfaced rather than swallowed: a malformed
|
|
149
|
+
* `migration.json` indicates either a hand-edit gone wrong or partial
|
|
150
|
+
* write, and silently rebuilding from `describe()` would discard the
|
|
151
|
+
* user's on-disk content (preserved bookends, hints, labels,
|
|
152
|
+
* `createdAt`) without any indication something was wrong on disk.
|
|
153
|
+
* Apply-time consumers always route through the verifying
|
|
154
|
+
* `readMigrationPackage` in `@prisma-next/migration-tools/io` instead.
|
|
147
155
|
*/
|
|
148
156
|
function readExistingMetadata(metadataPath) {
|
|
149
157
|
let raw;
|
|
@@ -154,8 +162,8 @@ function readExistingMetadata(metadataPath) {
|
|
|
154
162
|
}
|
|
155
163
|
try {
|
|
156
164
|
return JSON.parse(raw);
|
|
157
|
-
} catch {
|
|
158
|
-
|
|
165
|
+
} catch (e) {
|
|
166
|
+
throw errorInvalidJson(metadataPath, e instanceof Error ? e.message : String(e));
|
|
159
167
|
}
|
|
160
168
|
}
|
|
161
169
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migration-cli.mjs","names":["configPath: string | undefined","raw: string"],"sources":["../src/migration-cli.ts"],"sourcesContent":["/**\n * The migration-file CLI interface: the actor invoked when the author runs\n * `node migration.ts` directly.\n *\n * Naming: this is *not* a \"migration runner\" in the apply-time sense. The\n * apply-time runner is the thing `prisma-next migration apply` uses to\n * execute migration JSON ops against a database. `MigrationCLI` is the\n * tiny CLI surface owned by an authored `migration.ts` file: parse the\n * file's argv, load the project's `prisma-next.config.ts`, assemble a\n * `ControlStack`, instantiate the migration class, and serialize.\n *\n * The user authors a migration class, then calls\n * `MigrationCLI.run(import.meta.url, MigrationClass)` at module scope\n * after the class definition. When the file is invoked as a node\n * entrypoint (`node migration.ts`), the CLI:\n *\n * 1. Detects whether the file is the direct entrypoint (no-op when imported).\n * 2. Parses CLI args (`--help`, `--dry-run`, `--config <path>`).\n * 3. Loads the project's `prisma-next.config.ts` via the same `loadConfig`\n * the CLI commands use, walking up from the migration file's directory.\n * 4. Probe-instantiates the migration class without a stack so it can read\n * `targetId` and verify it matches `config.target.targetId`\n * (`PN-MIG-2006` on mismatch) before any stack-driven adapter\n * construction runs.\n * 5. Assembles a `ControlStack` from the loaded config descriptors and\n * constructs the migration with that stack.\n * 6. Reads any previously-scaffolded `migration.json`, then calls\n * `buildMigrationArtifacts` from `@prisma-next/migration-tools` to\n * produce in-memory `ops.json` + `migration.json` content. Persists\n * the result to disk (or prints in dry-run mode).\n *\n * File I/O lives here, in `@prisma-next/cli`: this is the only place\n * that legitimately combines config loading, stack assembly, and\n * on-disk persistence. `@prisma-next/migration-tools` owns the pure\n * conversion from a `Migration` instance to artifact strings; `Migration`\n * stays a pure abstract class.\n */\n\nimport { readFileSync, writeFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { CliStructuredError, errorMigrationCliInvalidConfigArg } from '@prisma-next/errors/control';\nimport { errorMigrationTargetMismatch } from '@prisma-next/errors/migration';\nimport { createControlStack } from '@prisma-next/framework-components/control';\nimport type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';\nimport {\n buildMigrationArtifacts,\n isDirectEntrypoint,\n type Migration,\n printMigrationHelp,\n} from '@prisma-next/migration-tools/migration';\nimport { dirname, join } from 'pathe';\nimport { loadConfig } from './config-loader';\n\n/**\n * Constructor shape accepted by `MigrationCLI.run`. `Migration` subclasses\n * accept an optional `ControlStack` in their constructor (each subclass\n * narrows the stack to its own family/target generics); the CLI always\n * passes one assembled from the loaded config. We use a rest-args `any[]`\n * constructor signature so that subclass constructors with narrower\n * parameter types remain assignable - constructor type compatibility in\n * TS is contravariant in the parameter, and a wider `unknown` parameter\n * on the alias side would reject any narrower subclass signature.\n *\n * The CLI only ever passes one argument (`new MigrationClass(stack)`);\n * the rest-arity is purely a type-compatibility concession for subclass\n * constructors that declare narrower parameter types, not an extension\n * point for additional construction arguments.\n */\n// biome-ignore lint/suspicious/noExplicitAny: see JSDoc - rest args with any are the idiomatic TS pattern for accepting arbitrary subclass constructor signatures\nexport type MigrationConstructor = new (...args: any[]) => Migration;\n\ninterface ParsedArgs {\n readonly help: boolean;\n readonly dryRun: boolean;\n readonly configPath: string | undefined;\n}\n\n/**\n * Parse the subset of `process.argv` that `MigrationCLI.run` cares about.\n * Recognised flags: `--help`, `--dry-run`, `--config <path>` /\n * `--config=<path>`. Unknown flags are ignored to keep the surface\n * forgiving for ad-hoc tooling that wraps a migration file.\n *\n * Throws `errorMigrationCliInvalidConfigArg` (`PN-CLI-4012`) when\n * `--config` is missing its path argument or is followed by another flag\n * (e.g. `--config --dry-run`); silently consuming the next flag would\n * either drop dry-run handling or serialize against the wrong project.\n *\n * NOTE: this hand-rolled parser is a known wart, tracked separately by\n * TML-2318 (\"Migration CLI: replace handrolled arg parser with shared\n * CLI library\"). Until that lands the surface is intentionally tiny.\n */\nfunction parseArgs(argv: readonly string[]): ParsedArgs {\n let help = false;\n let dryRun = false;\n let configPath: string | undefined;\n\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i]!;\n if (arg === '--help' || arg === '-h') {\n help = true;\n } else if (arg === '--dry-run') {\n dryRun = true;\n } else if (arg === '--config') {\n const next = argv[i + 1];\n if (next === undefined) {\n throw errorMigrationCliInvalidConfigArg();\n }\n if (next.startsWith('-')) {\n throw errorMigrationCliInvalidConfigArg({ nextToken: next });\n }\n configPath = next;\n i++;\n } else if (arg.startsWith('--config=')) {\n configPath = arg.slice('--config='.length);\n }\n }\n\n return { help, dryRun, configPath };\n}\n\n/**\n * The CLI surface invoked by an authored `migration.ts` file. Exposed as\n * a class with a static `run` method (rather than a free function) to\n * give the concept a stable identity in the ubiquitous language: this is\n * the \"migration-file CLI\", distinct from the apply-time runner that\n * executes migration JSON ops.\n *\n * Currently a single static method. Future surface (e.g. a programmatic\n * `MigrationCLI.serializeOnly(...)` for tests, or extra subcommands) can\n * land here without changing the import shape used by every authored\n * migration.\n */\n// biome-ignore lint/complexity/noStaticOnlyClass: see JSDoc - intentional class facade for the migration-file CLI surface; future methods will share state derived from argv/config.\nexport class MigrationCLI {\n /**\n * Orchestrates a class-flow `migration.ts` script run. Awaitable:\n * callers may `await MigrationCLI.run(...)` to surface async failures\n * from config loading, but the typical usage pattern (top-level call\n * after the class definition) does not require awaiting because\n * node's module evaluation keeps the promise alive until completion.\n *\n * Any throwable inside this function must surface through the internal\n * try/catch — script callers do not await, so an unhandled rejection\n * would silently exit 0. Treat the try/catch as load-bearing for the\n * no-await usage pattern.\n */\n static async run(importMetaUrl: string, MigrationClass: MigrationConstructor): Promise<void> {\n if (!importMetaUrl) return;\n if (!isDirectEntrypoint(importMetaUrl)) return;\n\n try {\n const args = parseArgs(process.argv.slice(2));\n\n if (args.help) {\n printMigrationHelp();\n return;\n }\n\n const migrationFile = fileURLToPath(importMetaUrl);\n const migrationDir = dirname(migrationFile);\n\n const config = await loadConfig(args.configPath);\n\n // Probe-instantiate without a stack so we can read `targetId` before\n // any target-specific constructor side effects (e.g.\n // `PostgresMigration`'s `stack.adapter.create(stack)`) run. Concrete\n // subclasses are required to accept the no-arg form; the abstract\n // `Migration` constructor declares `stack?` and target subclasses\n // (Postgres, Mongo) propagate that optionality. This makes the\n // target-mismatch guard fail fast with `PN-MIG-2006` before any\n // stack-driven adapter construction begins, even if the wrong-target\n // adapter's `create` would otherwise succeed and silently misshapen\n // the stored adapter cast.\n const probe = new MigrationClass();\n\n if (probe.targetId !== config.target.targetId) {\n throw errorMigrationTargetMismatch({\n migrationTargetId: probe.targetId,\n configTargetId: config.target.targetId,\n });\n }\n\n const stack = createControlStack(config);\n const instance = new MigrationClass(stack);\n\n serializeMigrationToDisk(instance, migrationDir, args.dryRun);\n } catch (err) {\n if (CliStructuredError.is(err)) {\n process.stderr.write(`${err.message}: ${err.why}\\n`);\n } else {\n process.stderr.write(`${err instanceof Error ? err.message : String(err)}\\n`);\n }\n process.exitCode = 1;\n }\n }\n}\n\n/**\n * Read a previously-scaffolded `migration.json` from disk, returning\n * `null` when the file is missing or unparseable. The CLI feeds this into\n * `buildMigrationArtifacts` so the pure builder can preserve fields owned\n * by `migration plan` (contract bookends, hints, labels, `createdAt`)\n * across re-emits.\n *\n * Author-time path: this loader is non-verifying by design. Hash mismatch\n * is the *expected* outcome of a re-author (the developer's source\n * changes invalidate the prior hash by construction), and verification\n * here would block legitimate regenerations. Apply-time consumers always\n * route through the verifying `readMigrationPackage` in\n * `@prisma-next/migration-tools/io` instead.\n */\nfunction readExistingMetadata(metadataPath: string): Partial<MigrationMetadata> | null {\n let raw: string;\n try {\n raw = readFileSync(metadataPath, 'utf-8');\n } catch {\n return null;\n }\n try {\n return JSON.parse(raw) as Partial<MigrationMetadata>;\n } catch {\n return null;\n }\n}\n\n/**\n * Persist a migration instance's artifacts to `migrationDir`. In\n * `dryRun` mode the artifacts are printed to stdout (with the same\n * `--- migration.json --- / --- ops.json ---` framing the legacy\n * `serializeMigration` helper used) and no files are written. Otherwise\n * `ops.json` and `migration.json` are written next to `migration.ts` and\n * a confirmation line is printed.\n *\n * File I/O lives in the CLI rather than `@prisma-next/migration-tools`\n * so the migration-tools package stays focused on the pure\n * `Migration` → in-memory artifact conversion. The CLI is the only\n * legitimate site for combining config loading, stack assembly, and\n * filesystem persistence.\n */\nfunction serializeMigrationToDisk(\n instance: Migration,\n migrationDir: string,\n dryRun: boolean,\n): void {\n const metadataPath = join(migrationDir, 'migration.json');\n const existing = readExistingMetadata(metadataPath);\n const { opsJson, metadataJson } = buildMigrationArtifacts(instance, existing);\n\n if (dryRun) {\n process.stdout.write(`--- migration.json ---\\n${metadataJson}\\n`);\n process.stdout.write('--- ops.json ---\\n');\n process.stdout.write(`${opsJson}\\n`);\n return;\n }\n\n writeFileSync(join(migrationDir, 'ops.json'), opsJson);\n writeFileSync(metadataPath, metadataJson);\n\n process.stdout.write(`Wrote ops.json + migration.json to ${migrationDir}\\n`);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4FA,SAAS,UAAU,MAAqC;CACtD,IAAI,OAAO;CACX,IAAI,SAAS;CACb,IAAIA;AAEJ,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,MAAM,KAAK;AACjB,MAAI,QAAQ,YAAY,QAAQ,KAC9B,QAAO;WACE,QAAQ,YACjB,UAAS;WACA,QAAQ,YAAY;GAC7B,MAAM,OAAO,KAAK,IAAI;AACtB,OAAI,SAAS,OACX,OAAM,mCAAmC;AAE3C,OAAI,KAAK,WAAW,IAAI,CACtB,OAAM,kCAAkC,EAAE,WAAW,MAAM,CAAC;AAE9D,gBAAa;AACb;aACS,IAAI,WAAW,YAAY,CACpC,cAAa,IAAI,MAAM,EAAmB;;AAI9C,QAAO;EAAE;EAAM;EAAQ;EAAY;;;;;;;;;;;;;;AAgBrC,IAAa,eAAb,MAA0B;;;;;;;;;;;;;CAaxB,aAAa,IAAI,eAAuB,gBAAqD;AAC3F,MAAI,CAAC,cAAe;AACpB,MAAI,CAAC,mBAAmB,cAAc,CAAE;AAExC,MAAI;GACF,MAAM,OAAO,UAAU,QAAQ,KAAK,MAAM,EAAE,CAAC;AAE7C,OAAI,KAAK,MAAM;AACb,wBAAoB;AACpB;;GAIF,MAAM,eAAe,QADC,cAAc,cAAc,CACP;GAE3C,MAAM,SAAS,MAAM,WAAW,KAAK,WAAW;GAYhD,MAAM,QAAQ,IAAI,gBAAgB;AAElC,OAAI,MAAM,aAAa,OAAO,OAAO,SACnC,OAAM,6BAA6B;IACjC,mBAAmB,MAAM;IACzB,gBAAgB,OAAO,OAAO;IAC/B,CAAC;AAMJ,4BAFiB,IAAI,eADP,mBAAmB,OAAO,CACE,EAEP,cAAc,KAAK,OAAO;WACtD,KAAK;AACZ,OAAI,mBAAmB,GAAG,IAAI,CAC5B,SAAQ,OAAO,MAAM,GAAG,IAAI,QAAQ,IAAI,IAAI,IAAI,IAAI;OAEpD,SAAQ,OAAO,MAAM,GAAG,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,CAAC,IAAI;AAE/E,WAAQ,WAAW;;;;;;;;;;;;;;;;;;AAmBzB,SAAS,qBAAqB,cAAyD;CACrF,IAAIC;AACJ,KAAI;AACF,QAAM,aAAa,cAAc,QAAQ;SACnC;AACN,SAAO;;AAET,KAAI;AACF,SAAO,KAAK,MAAM,IAAI;SAChB;AACN,SAAO;;;;;;;;;;;;;;;;;AAkBX,SAAS,yBACP,UACA,cACA,QACM;CACN,MAAM,eAAe,KAAK,cAAc,iBAAiB;CAEzD,MAAM,EAAE,SAAS,iBAAiB,wBAAwB,UADzC,qBAAqB,aAAa,CAC0B;AAE7E,KAAI,QAAQ;AACV,UAAQ,OAAO,MAAM,2BAA2B,aAAa,IAAI;AACjE,UAAQ,OAAO,MAAM,qBAAqB;AAC1C,UAAQ,OAAO,MAAM,GAAG,QAAQ,IAAI;AACpC;;AAGF,eAAc,KAAK,cAAc,WAAW,EAAE,QAAQ;AACtD,eAAc,cAAc,aAAa;AAEzC,SAAQ,OAAO,MAAM,sCAAsC,aAAa,IAAI"}
|
|
1
|
+
{"version":3,"file":"migration-cli.mjs","names":["configPath: string | undefined","raw: string"],"sources":["../src/migration-cli.ts"],"sourcesContent":["/**\n * The migration-file CLI interface: the actor invoked when the author runs\n * `node migration.ts` directly.\n *\n * Naming: this is *not* a \"migration runner\" in the apply-time sense. The\n * apply-time runner is the thing `prisma-next migration apply` uses to\n * execute migration JSON ops against a database. `MigrationCLI` is the\n * tiny CLI surface owned by an authored `migration.ts` file: parse the\n * file's argv, load the project's `prisma-next.config.ts`, assemble a\n * `ControlStack`, instantiate the migration class, and serialize.\n *\n * The user authors a migration class, then calls\n * `MigrationCLI.run(import.meta.url, MigrationClass)` at module scope\n * after the class definition. When the file is invoked as a node\n * entrypoint (`node migration.ts`), the CLI:\n *\n * 1. Detects whether the file is the direct entrypoint (no-op when imported).\n * 2. Parses CLI args (`--help`, `--dry-run`, `--config <path>`).\n * 3. Loads the project's `prisma-next.config.ts` via the same `loadConfig`\n * the CLI commands use, walking up from the migration file's directory.\n * 4. Probe-instantiates the migration class without a stack so it can read\n * `targetId` and verify it matches `config.target.targetId`\n * (`PN-MIG-2006` on mismatch) before any stack-driven adapter\n * construction runs.\n * 5. Assembles a `ControlStack` from the loaded config descriptors and\n * constructs the migration with that stack.\n * 6. Reads any previously-scaffolded `migration.json`, then calls\n * `buildMigrationArtifacts` from `@prisma-next/migration-tools` to\n * produce in-memory `ops.json` + `migration.json` content. Persists\n * the result to disk (or prints in dry-run mode).\n *\n * File I/O lives here, in `@prisma-next/cli`: this is the only place\n * that legitimately combines config loading, stack assembly, and\n * on-disk persistence. `@prisma-next/migration-tools` owns the pure\n * conversion from a `Migration` instance to artifact strings; `Migration`\n * stays a pure abstract class.\n */\n\nimport { readFileSync, writeFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { CliStructuredError, errorMigrationCliInvalidConfigArg } from '@prisma-next/errors/control';\nimport { errorMigrationTargetMismatch } from '@prisma-next/errors/migration';\nimport { createControlStack } from '@prisma-next/framework-components/control';\nimport { errorInvalidJson, MigrationToolsError } from '@prisma-next/migration-tools/errors';\nimport type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';\nimport {\n buildMigrationArtifacts,\n isDirectEntrypoint,\n type Migration,\n printMigrationHelp,\n} from '@prisma-next/migration-tools/migration';\nimport { dirname, join } from 'pathe';\nimport { loadConfig } from './config-loader';\n\n/**\n * Constructor shape accepted by `MigrationCLI.run`. `Migration` subclasses\n * accept an optional `ControlStack` in their constructor (each subclass\n * narrows the stack to its own family/target generics); the CLI always\n * passes one assembled from the loaded config. We use a rest-args `any[]`\n * constructor signature so that subclass constructors with narrower\n * parameter types remain assignable - constructor type compatibility in\n * TS is contravariant in the parameter, and a wider `unknown` parameter\n * on the alias side would reject any narrower subclass signature.\n *\n * The CLI only ever passes one argument (`new MigrationClass(stack)`);\n * the rest-arity is purely a type-compatibility concession for subclass\n * constructors that declare narrower parameter types, not an extension\n * point for additional construction arguments.\n */\n// biome-ignore lint/suspicious/noExplicitAny: see JSDoc - rest args with any are the idiomatic TS pattern for accepting arbitrary subclass constructor signatures\nexport type MigrationConstructor = new (...args: any[]) => Migration;\n\ninterface ParsedArgs {\n readonly help: boolean;\n readonly dryRun: boolean;\n readonly configPath: string | undefined;\n}\n\n/**\n * Parse the subset of `process.argv` that `MigrationCLI.run` cares about.\n * Recognised flags: `--help`, `--dry-run`, `--config <path>` /\n * `--config=<path>`. Unknown flags are ignored to keep the surface\n * forgiving for ad-hoc tooling that wraps a migration file.\n *\n * Throws `errorMigrationCliInvalidConfigArg` (`PN-CLI-4012`) when\n * `--config` is missing its path argument or is followed by another flag\n * (e.g. `--config --dry-run`); silently consuming the next flag would\n * either drop dry-run handling or serialize against the wrong project.\n *\n * NOTE: this hand-rolled parser is a known wart, tracked separately by\n * TML-2318 (\"Migration CLI: replace handrolled arg parser with shared\n * CLI library\"). Until that lands the surface is intentionally tiny.\n */\nfunction parseArgs(argv: readonly string[]): ParsedArgs {\n let help = false;\n let dryRun = false;\n let configPath: string | undefined;\n\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i]!;\n if (arg === '--help' || arg === '-h') {\n help = true;\n } else if (arg === '--dry-run') {\n dryRun = true;\n } else if (arg === '--config') {\n const next = argv[i + 1];\n if (next === undefined) {\n throw errorMigrationCliInvalidConfigArg();\n }\n if (next.startsWith('-')) {\n throw errorMigrationCliInvalidConfigArg({ nextToken: next });\n }\n configPath = next;\n i++;\n } else if (arg.startsWith('--config=')) {\n configPath = arg.slice('--config='.length);\n }\n }\n\n return { help, dryRun, configPath };\n}\n\n/**\n * The CLI surface invoked by an authored `migration.ts` file. Exposed as\n * a class with a static `run` method (rather than a free function) to\n * give the concept a stable identity in the ubiquitous language: this is\n * the \"migration-file CLI\", distinct from the apply-time runner that\n * executes migration JSON ops.\n *\n * Currently a single static method. Future surface (e.g. a programmatic\n * `MigrationCLI.serializeOnly(...)` for tests, or extra subcommands) can\n * land here without changing the import shape used by every authored\n * migration.\n */\n// biome-ignore lint/complexity/noStaticOnlyClass: see JSDoc - intentional class facade for the migration-file CLI surface; future methods will share state derived from argv/config.\nexport class MigrationCLI {\n /**\n * Orchestrates a class-flow `migration.ts` script run. Awaitable:\n * callers may `await MigrationCLI.run(...)` to surface async failures\n * from config loading, but the typical usage pattern (top-level call\n * after the class definition) does not require awaiting because\n * node's module evaluation keeps the promise alive until completion.\n *\n * Any throwable inside this function must surface through the internal\n * try/catch — script callers do not await, so an unhandled rejection\n * would silently exit 0. Treat the try/catch as load-bearing for the\n * no-await usage pattern.\n */\n static async run(importMetaUrl: string, MigrationClass: MigrationConstructor): Promise<void> {\n if (!importMetaUrl) return;\n if (!isDirectEntrypoint(importMetaUrl)) return;\n\n try {\n const args = parseArgs(process.argv.slice(2));\n\n if (args.help) {\n printMigrationHelp();\n return;\n }\n\n const migrationFile = fileURLToPath(importMetaUrl);\n const migrationDir = dirname(migrationFile);\n\n const config = await loadConfig(args.configPath);\n\n // Probe-instantiate without a stack so we can read `targetId` before\n // any target-specific constructor side effects (e.g.\n // `PostgresMigration`'s `stack.adapter.create(stack)`) run. Concrete\n // subclasses are required to accept the no-arg form; the abstract\n // `Migration` constructor declares `stack?` and target subclasses\n // (Postgres, Mongo) propagate that optionality. This makes the\n // target-mismatch guard fail fast with `PN-MIG-2006` before any\n // stack-driven adapter construction begins, even if the wrong-target\n // adapter's `create` would otherwise succeed and silently misshapen\n // the stored adapter cast.\n const probe = new MigrationClass();\n\n if (probe.targetId !== config.target.targetId) {\n throw errorMigrationTargetMismatch({\n migrationTargetId: probe.targetId,\n configTargetId: config.target.targetId,\n });\n }\n\n const stack = createControlStack(config);\n const instance = new MigrationClass(stack);\n\n serializeMigrationToDisk(instance, migrationDir, args.dryRun);\n } catch (err) {\n if (CliStructuredError.is(err) || MigrationToolsError.is(err)) {\n process.stderr.write(`${err.message}: ${err.why}\\n`);\n } else {\n process.stderr.write(`${err instanceof Error ? err.message : String(err)}\\n`);\n }\n process.exitCode = 1;\n }\n }\n}\n\n/**\n * Read a previously-scaffolded `migration.json` from disk, returning\n * `null` when the file is missing and throwing `MIGRATION.INVALID_JSON`\n * when the file is present but cannot be parsed as JSON. The CLI feeds\n * this into `buildMigrationArtifacts` so the pure builder can preserve\n * fields owned by `migration plan` (contract bookends, hints, labels,\n * `createdAt`) across re-emits.\n *\n * Author-time path: this loader still does not verify the manifest hash\n * or schema — that is the apply-time loader's job. Hash mismatch is the\n * *expected* outcome of a re-author (the developer's source changes\n * invalidate the prior hash by construction), and verification here\n * would block legitimate regenerations. Syntactic JSON-parse failure,\n * however, is now surfaced rather than swallowed: a malformed\n * `migration.json` indicates either a hand-edit gone wrong or partial\n * write, and silently rebuilding from `describe()` would discard the\n * user's on-disk content (preserved bookends, hints, labels,\n * `createdAt`) without any indication something was wrong on disk.\n * Apply-time consumers always route through the verifying\n * `readMigrationPackage` in `@prisma-next/migration-tools/io` instead.\n */\nfunction readExistingMetadata(metadataPath: string): Partial<MigrationMetadata> | null {\n let raw: string;\n try {\n raw = readFileSync(metadataPath, 'utf-8');\n } catch {\n return null;\n }\n try {\n return JSON.parse(raw) as Partial<MigrationMetadata>;\n } catch (e) {\n throw errorInvalidJson(metadataPath, e instanceof Error ? e.message : String(e));\n }\n}\n\n/**\n * Persist a migration instance's artifacts to `migrationDir`. In\n * `dryRun` mode the artifacts are printed to stdout (with the same\n * `--- migration.json --- / --- ops.json ---` framing the legacy\n * `serializeMigration` helper used) and no files are written. Otherwise\n * `ops.json` and `migration.json` are written next to `migration.ts` and\n * a confirmation line is printed.\n *\n * File I/O lives in the CLI rather than `@prisma-next/migration-tools`\n * so the migration-tools package stays focused on the pure\n * `Migration` → in-memory artifact conversion. The CLI is the only\n * legitimate site for combining config loading, stack assembly, and\n * filesystem persistence.\n */\nfunction serializeMigrationToDisk(\n instance: Migration,\n migrationDir: string,\n dryRun: boolean,\n): void {\n const metadataPath = join(migrationDir, 'migration.json');\n const existing = readExistingMetadata(metadataPath);\n const { opsJson, metadataJson } = buildMigrationArtifacts(instance, existing);\n\n if (dryRun) {\n process.stdout.write(`--- migration.json ---\\n${metadataJson}\\n`);\n process.stdout.write('--- ops.json ---\\n');\n process.stdout.write(`${opsJson}\\n`);\n return;\n }\n\n writeFileSync(join(migrationDir, 'ops.json'), opsJson);\n writeFileSync(metadataPath, metadataJson);\n\n process.stdout.write(`Wrote ops.json + migration.json to ${migrationDir}\\n`);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6FA,SAAS,UAAU,MAAqC;CACtD,IAAI,OAAO;CACX,IAAI,SAAS;CACb,IAAIA;AAEJ,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,MAAM,KAAK;AACjB,MAAI,QAAQ,YAAY,QAAQ,KAC9B,QAAO;WACE,QAAQ,YACjB,UAAS;WACA,QAAQ,YAAY;GAC7B,MAAM,OAAO,KAAK,IAAI;AACtB,OAAI,SAAS,OACX,OAAM,mCAAmC;AAE3C,OAAI,KAAK,WAAW,IAAI,CACtB,OAAM,kCAAkC,EAAE,WAAW,MAAM,CAAC;AAE9D,gBAAa;AACb;aACS,IAAI,WAAW,YAAY,CACpC,cAAa,IAAI,MAAM,EAAmB;;AAI9C,QAAO;EAAE;EAAM;EAAQ;EAAY;;;;;;;;;;;;;;AAgBrC,IAAa,eAAb,MAA0B;;;;;;;;;;;;;CAaxB,aAAa,IAAI,eAAuB,gBAAqD;AAC3F,MAAI,CAAC,cAAe;AACpB,MAAI,CAAC,mBAAmB,cAAc,CAAE;AAExC,MAAI;GACF,MAAM,OAAO,UAAU,QAAQ,KAAK,MAAM,EAAE,CAAC;AAE7C,OAAI,KAAK,MAAM;AACb,wBAAoB;AACpB;;GAIF,MAAM,eAAe,QADC,cAAc,cAAc,CACP;GAE3C,MAAM,SAAS,MAAM,WAAW,KAAK,WAAW;GAYhD,MAAM,QAAQ,IAAI,gBAAgB;AAElC,OAAI,MAAM,aAAa,OAAO,OAAO,SACnC,OAAM,6BAA6B;IACjC,mBAAmB,MAAM;IACzB,gBAAgB,OAAO,OAAO;IAC/B,CAAC;AAMJ,4BAFiB,IAAI,eADP,mBAAmB,OAAO,CACE,EAEP,cAAc,KAAK,OAAO;WACtD,KAAK;AACZ,OAAI,mBAAmB,GAAG,IAAI,IAAI,oBAAoB,GAAG,IAAI,CAC3D,SAAQ,OAAO,MAAM,GAAG,IAAI,QAAQ,IAAI,IAAI,IAAI,IAAI;OAEpD,SAAQ,OAAO,MAAM,GAAG,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,CAAC,IAAI;AAE/E,WAAQ,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;AA0BzB,SAAS,qBAAqB,cAAyD;CACrF,IAAIC;AACJ,KAAI;AACF,QAAM,aAAa,cAAc,QAAQ;SACnC;AACN,SAAO;;AAET,KAAI;AACF,SAAO,KAAK,MAAM,IAAI;UACf,GAAG;AACV,QAAM,iBAAiB,cAAc,aAAa,QAAQ,EAAE,UAAU,OAAO,EAAE,CAAC;;;;;;;;;;;;;;;;;AAkBpF,SAAS,yBACP,UACA,cACA,QACM;CACN,MAAM,eAAe,KAAK,cAAc,iBAAiB;CAEzD,MAAM,EAAE,SAAS,iBAAiB,wBAAwB,UADzC,qBAAqB,aAAa,CAC0B;AAE7E,KAAI,QAAQ;AACV,UAAQ,OAAO,MAAM,2BAA2B,aAAa,IAAI;AACjE,UAAQ,OAAO,MAAM,qBAAqB;AAC1C,UAAQ,OAAO,MAAM,GAAG,QAAQ,IAAI;AACpC;;AAGF,eAAc,KAAK,cAAc,WAAW,EAAE,QAAQ;AACtD,eAAc,cAAc,aAAa;AAEzC,SAAQ,OAAO,MAAM,sCAAsC,aAAa,IAAI"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/cli",
|
|
3
|
-
"version": "0.5.0-dev.
|
|
3
|
+
"version": "0.5.0-dev.23",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"files": [
|
|
@@ -25,26 +25,26 @@
|
|
|
25
25
|
"string-width": "^8.2.0",
|
|
26
26
|
"strip-ansi": "^7.1.2",
|
|
27
27
|
"wrap-ansi": "^10.0.0",
|
|
28
|
-
"@prisma-next/config": "0.5.0-dev.
|
|
29
|
-
"@prisma-next/contract": "0.5.0-dev.
|
|
30
|
-
"@prisma-next/emitter": "0.5.0-dev.
|
|
31
|
-
"@prisma-next/
|
|
32
|
-
"@prisma-next/
|
|
33
|
-
"@prisma-next/
|
|
34
|
-
"@prisma-next/
|
|
35
|
-
"@prisma-next/
|
|
28
|
+
"@prisma-next/config": "0.5.0-dev.23",
|
|
29
|
+
"@prisma-next/contract": "0.5.0-dev.23",
|
|
30
|
+
"@prisma-next/emitter": "0.5.0-dev.23",
|
|
31
|
+
"@prisma-next/errors": "0.5.0-dev.23",
|
|
32
|
+
"@prisma-next/psl-printer": "0.5.0-dev.23",
|
|
33
|
+
"@prisma-next/utils": "0.5.0-dev.23",
|
|
34
|
+
"@prisma-next/migration-tools": "0.5.0-dev.23",
|
|
35
|
+
"@prisma-next/framework-components": "0.5.0-dev.23"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "24.10.4",
|
|
39
39
|
"tsdown": "0.18.4",
|
|
40
40
|
"typescript": "5.9.3",
|
|
41
41
|
"vitest": "4.0.17",
|
|
42
|
-
"@prisma-next/sql-contract": "0.5.0-dev.
|
|
43
|
-
"@prisma-next/sql-contract-emitter": "0.5.0-dev.
|
|
44
|
-
"@prisma-next/sql-contract-ts": "0.5.0-dev.
|
|
45
|
-
"@prisma-next/sql-operations": "0.5.0-dev.
|
|
46
|
-
"@prisma-next/sql-runtime": "0.5.0-dev.21",
|
|
42
|
+
"@prisma-next/sql-contract": "0.5.0-dev.23",
|
|
43
|
+
"@prisma-next/sql-contract-emitter": "0.5.0-dev.23",
|
|
44
|
+
"@prisma-next/sql-contract-ts": "0.5.0-dev.23",
|
|
45
|
+
"@prisma-next/sql-operations": "0.5.0-dev.23",
|
|
47
46
|
"@prisma-next/test-utils": "0.0.1",
|
|
47
|
+
"@prisma-next/sql-runtime": "0.5.0-dev.23",
|
|
48
48
|
"@prisma-next/tsconfig": "0.0.0",
|
|
49
49
|
"@prisma-next/tsdown": "0.0.0"
|
|
50
50
|
},
|
package/src/migration-cli.ts
CHANGED
|
@@ -41,6 +41,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
41
41
|
import { CliStructuredError, errorMigrationCliInvalidConfigArg } from '@prisma-next/errors/control';
|
|
42
42
|
import { errorMigrationTargetMismatch } from '@prisma-next/errors/migration';
|
|
43
43
|
import { createControlStack } from '@prisma-next/framework-components/control';
|
|
44
|
+
import { errorInvalidJson, MigrationToolsError } from '@prisma-next/migration-tools/errors';
|
|
44
45
|
import type { MigrationMetadata } from '@prisma-next/migration-tools/metadata';
|
|
45
46
|
import {
|
|
46
47
|
buildMigrationArtifacts,
|
|
@@ -186,7 +187,7 @@ export class MigrationCLI {
|
|
|
186
187
|
|
|
187
188
|
serializeMigrationToDisk(instance, migrationDir, args.dryRun);
|
|
188
189
|
} catch (err) {
|
|
189
|
-
if (CliStructuredError.is(err)) {
|
|
190
|
+
if (CliStructuredError.is(err) || MigrationToolsError.is(err)) {
|
|
190
191
|
process.stderr.write(`${err.message}: ${err.why}\n`);
|
|
191
192
|
} else {
|
|
192
193
|
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
@@ -198,17 +199,24 @@ export class MigrationCLI {
|
|
|
198
199
|
|
|
199
200
|
/**
|
|
200
201
|
* Read a previously-scaffolded `migration.json` from disk, returning
|
|
201
|
-
* `null` when the file is missing
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
202
|
+
* `null` when the file is missing and throwing `MIGRATION.INVALID_JSON`
|
|
203
|
+
* when the file is present but cannot be parsed as JSON. The CLI feeds
|
|
204
|
+
* this into `buildMigrationArtifacts` so the pure builder can preserve
|
|
205
|
+
* fields owned by `migration plan` (contract bookends, hints, labels,
|
|
206
|
+
* `createdAt`) across re-emits.
|
|
205
207
|
*
|
|
206
|
-
* Author-time path: this loader
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
208
|
+
* Author-time path: this loader still does not verify the manifest hash
|
|
209
|
+
* or schema — that is the apply-time loader's job. Hash mismatch is the
|
|
210
|
+
* *expected* outcome of a re-author (the developer's source changes
|
|
211
|
+
* invalidate the prior hash by construction), and verification here
|
|
212
|
+
* would block legitimate regenerations. Syntactic JSON-parse failure,
|
|
213
|
+
* however, is now surfaced rather than swallowed: a malformed
|
|
214
|
+
* `migration.json` indicates either a hand-edit gone wrong or partial
|
|
215
|
+
* write, and silently rebuilding from `describe()` would discard the
|
|
216
|
+
* user's on-disk content (preserved bookends, hints, labels,
|
|
217
|
+
* `createdAt`) without any indication something was wrong on disk.
|
|
218
|
+
* Apply-time consumers always route through the verifying
|
|
219
|
+
* `readMigrationPackage` in `@prisma-next/migration-tools/io` instead.
|
|
212
220
|
*/
|
|
213
221
|
function readExistingMetadata(metadataPath: string): Partial<MigrationMetadata> | null {
|
|
214
222
|
let raw: string;
|
|
@@ -219,8 +227,8 @@ function readExistingMetadata(metadataPath: string): Partial<MigrationMetadata>
|
|
|
219
227
|
}
|
|
220
228
|
try {
|
|
221
229
|
return JSON.parse(raw) as Partial<MigrationMetadata>;
|
|
222
|
-
} catch {
|
|
223
|
-
|
|
230
|
+
} catch (e) {
|
|
231
|
+
throw errorInvalidJson(metadataPath, e instanceof Error ? e.message : String(e));
|
|
224
232
|
}
|
|
225
233
|
}
|
|
226
234
|
|