@warlock.js/core 4.11.0 → 4.12.0

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.
@@ -2,22 +2,93 @@ import { toCamelCase } from "@mongez/reinforcements";
2
2
 
3
3
  //#region ../core/src/cli/parse-cli-args.ts
4
4
  /**
5
+ * Thrown when a declared boolean option is given a value that is neither
6
+ * true-ish nor false-ish.
7
+ *
8
+ * We refuse to guess. Guessing is exactly what produced #21: `--rollback=false`
9
+ * was kept as the truthy string `"false"` and dropped every table. An option
10
+ * that gates a destructive action must fail loudly on input it cannot read.
11
+ */
12
+ var CliOptionValueError = class extends Error {
13
+ constructor(optionToken, value) {
14
+ super(`Invalid value ${JSON.stringify(value)} for boolean option ${optionToken}. Expected one of: true, false, 1, 0, yes, no — or pass ${optionToken} on its own for true.`);
15
+ this.optionToken = optionToken;
16
+ this.value = value;
17
+ this.name = "CliOptionValueError";
18
+ }
19
+ };
20
+ const TRUE_VALUES = new Set([
21
+ "true",
22
+ "1",
23
+ "yes"
24
+ ]);
25
+ const FALSE_VALUES = new Set([
26
+ "false",
27
+ "0",
28
+ "no"
29
+ ]);
30
+ /**
31
+ * Collect the camelCased keys (names AND aliases) of every option the command
32
+ * declared as `type: "boolean"`.
33
+ *
34
+ * Both sides are camelCased because declared names keep their raw kebab form
35
+ * (`"pending-only"`) while the parser emits camelCase keys (`pendingOnly`).
36
+ */
37
+ function collectBooleanKeys(schema) {
38
+ const keys = /* @__PURE__ */ new Set();
39
+ for (const option of schema) {
40
+ if (option.type !== "boolean") continue;
41
+ if (option.name) keys.add(toCamelCase(option.name));
42
+ if (option.alias) keys.add(toCamelCase(option.alias));
43
+ }
44
+ return keys;
45
+ }
46
+ /**
47
+ * Turn the value written after `=` into a real boolean, or throw.
48
+ */
49
+ function toBoolean(optionToken, value) {
50
+ const normalized = value.toLowerCase();
51
+ if (TRUE_VALUES.has(normalized)) return true;
52
+ if (FALSE_VALUES.has(normalized)) return false;
53
+ throw new CliOptionValueError(optionToken, value);
54
+ }
55
+ /**
5
56
  * Parse CLI arguments from process.argv
6
57
  *
58
+ * Called with a `schema` (the resolved command's declared options), every
59
+ * option declared `type: "boolean"` is resolved as a boolean:
60
+ * `--flag` → true, `--flag=false|0|no` → false, `--flag=true|1|yes` → true,
61
+ * and `--flag <token>` leaves `<token>` as a POSITIONAL instead of swallowing
62
+ * it as the flag's value. Anything else throws {@link CliOptionValueError}.
63
+ *
64
+ * Called without a schema — which is how the command name is discovered,
65
+ * before any command object exists — behaviour is unchanged: every value stays
66
+ * a raw string and a bare `--flag` still consumes the following non-dash token.
67
+ * Nothing is blanket-coerced here on purpose: `--name=false` on a string-typed
68
+ * option must survive as the string `"false"`.
69
+ *
70
+ * Options the schema does not declare keep the schema-less behaviour.
71
+ *
7
72
  * @example
8
73
  * parseCliArgs(["node", "warlock", "migrate", "--rollback", "file.ts"])
9
74
  * // Returns: { name: "migrate", args: [], options: { rollback: "file.ts" } }
10
75
  *
11
76
  * @example
77
+ * // `rollback` declared as type: "boolean" — the file stays a positional
78
+ * parseCliArgs(["node", "warlock", "migrate", "--rollback", "file.ts"], migrateOptions)
79
+ * // Returns: { name: "migrate", args: ["file.ts"], options: { rollback: true } }
80
+ *
81
+ * @example
12
82
  * parseCliArgs(["node", "warlock", "dev", "--port=3000", "--fresh"])
13
83
  * // Returns: { name: "dev", args: [], options: { port: "3000", fresh: true } }
14
84
  */
15
- function parseCliArgs(argv) {
85
+ function parseCliArgs(argv, schema = []) {
16
86
  const potentialCommand = argv[2] || "";
17
87
  const isFirstArgOption = potentialCommand.startsWith("-");
18
88
  const command = isFirstArgOption ? "" : potentialCommand;
19
89
  const args = [];
20
90
  const options = {};
91
+ const booleanKeys = collectBooleanKeys(schema);
21
92
  const startIndex = isFirstArgOption ? 2 : 3;
22
93
  for (let i = startIndex; i < argv.length; i++) {
23
94
  const arg = argv[i];
@@ -26,11 +97,13 @@ function parseCliArgs(argv) {
26
97
  const equalIndex = withoutDashes.indexOf("=");
27
98
  if (equalIndex !== -1) {
28
99
  const key = toCamelCase(withoutDashes.slice(0, equalIndex));
29
- options[key] = withoutDashes.slice(equalIndex + 1);
100
+ const value = withoutDashes.slice(equalIndex + 1);
101
+ options[key] = booleanKeys.has(key) ? toBoolean(`--${withoutDashes.slice(0, equalIndex)}`, value) : value;
30
102
  } else {
31
103
  const key = toCamelCase(withoutDashes);
32
104
  const nextArg = argv[i + 1];
33
- if (nextArg && !nextArg.startsWith("-")) {
105
+ if (booleanKeys.has(key)) options[key] = true;
106
+ else if (nextArg && !nextArg.startsWith("-")) {
34
107
  options[key] = nextArg;
35
108
  i++;
36
109
  } else options[key] = true;
@@ -39,14 +112,18 @@ function parseCliArgs(argv) {
39
112
  const flags = arg.slice(1);
40
113
  const equalIndex = flags.indexOf("=");
41
114
  if (equalIndex !== -1) {
42
- const key = toCamelCase(flags.slice(0, equalIndex));
43
- options[key] = flags.slice(equalIndex + 1);
115
+ const rawKey = flags.slice(0, equalIndex);
116
+ const key = toCamelCase(rawKey);
117
+ const value = flags.slice(equalIndex + 1);
118
+ options[key] = booleanKeys.has(key) ? toBoolean(`-${rawKey}`, value) : value;
44
119
  } else if (flags.length === 1) {
120
+ const key = toCamelCase(flags);
45
121
  const nextArg = argv[i + 1];
46
- if (nextArg && !nextArg.startsWith("-")) {
47
- options[toCamelCase(flags)] = nextArg;
122
+ if (booleanKeys.has(key)) options[key] = true;
123
+ else if (nextArg && !nextArg.startsWith("-")) {
124
+ options[key] = nextArg;
48
125
  i++;
49
- } else options[toCamelCase(flags)] = true;
126
+ } else options[key] = true;
50
127
  } else for (const flag of flags) options[toCamelCase(flag)] = true;
51
128
  } else args.push(arg);
52
129
  }
@@ -58,5 +135,5 @@ function parseCliArgs(argv) {
58
135
  }
59
136
 
60
137
  //#endregion
61
- export { parseCliArgs };
138
+ export { CliOptionValueError, parseCliArgs };
62
139
  //# sourceMappingURL=parse-cli-args.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"parse-cli-args.mjs","names":[],"sources":["../../../../../../../core/src/cli/parse-cli-args.ts"],"sourcesContent":["import { toCamelCase } from \"@mongez/reinforcements\";\n\nexport type ParsedCliArgs = {\n name: string;\n args: string[];\n options: Record<string, string | boolean>;\n};\n\n/**\n * Parse CLI arguments from process.argv\n *\n * @example\n * parseCliArgs([\"node\", \"warlock\", \"migrate\", \"--rollback\", \"file.ts\"])\n * // Returns: { name: \"migrate\", args: [], options: { rollback: \"file.ts\" } }\n *\n * @example\n * parseCliArgs([\"node\", \"warlock\", \"dev\", \"--port=3000\", \"--fresh\"])\n * // Returns: { name: \"dev\", args: [], options: { port: \"3000\", fresh: true } }\n */\nexport function parseCliArgs(argv: string[]): ParsedCliArgs {\n // Command is at index 2 (after \"node\" and script path)\n // But if index 2 starts with \"-\", it's an option, not a command\n const potentialCommand = argv[2] || \"\";\n const isFirstArgOption = potentialCommand.startsWith(\"-\");\n const command = isFirstArgOption ? \"\" : potentialCommand;\n const args: string[] = [];\n const options: Record<string, string | boolean> = {};\n\n // Parse arguments starting from index 3 (or 2 if first arg was an option)\n const startIndex = isFirstArgOption ? 2 : 3;\n for (let i = startIndex; i < argv.length; i++) {\n const arg = argv[i];\n\n if (arg.startsWith(\"--\")) {\n // Long option: --key or --key=value\n const withoutDashes = arg.slice(2);\n const equalIndex = withoutDashes.indexOf(\"=\");\n\n if (equalIndex !== -1) {\n // --key=value format\n const key = toCamelCase(withoutDashes.slice(0, equalIndex));\n const value = withoutDashes.slice(equalIndex + 1);\n options[key] = value;\n } else {\n // --key format (check if next arg is a value)\n const key = toCamelCase(withoutDashes);\n const nextArg = argv[i + 1];\n\n // If next arg exists and doesn't start with -, treat it as value\n if (nextArg && !nextArg.startsWith(\"-\")) {\n options[key] = nextArg;\n i++; // Skip next arg\n } else {\n options[key] = true;\n }\n }\n } else if (arg.startsWith(\"-\") && arg.length > 1) {\n // Short option: -f, -t=value, or -abc (multiple flags)\n const flags = arg.slice(1);\n const equalIndex = flags.indexOf(\"=\");\n\n if (equalIndex !== -1) {\n // -t=value format\n const key = toCamelCase(flags.slice(0, equalIndex));\n const value = flags.slice(equalIndex + 1);\n options[key] = value;\n } else if (flags.length === 1) {\n // Single flag: -f\n const nextArg = argv[i + 1];\n\n if (nextArg && !nextArg.startsWith(\"-\")) {\n options[toCamelCase(flags)] = nextArg;\n i++; // Skip next arg\n } else {\n options[toCamelCase(flags)] = true;\n }\n } else {\n // Multiple flags: -abc becomes { a: true, b: true, c: true }\n for (const flag of flags) {\n options[toCamelCase(flag)] = true;\n }\n }\n } else {\n // Positional argument\n args.push(arg);\n }\n }\n\n return { name: command, args, options };\n}\n"],"mappings":";;;;;;;;;;;;;;AAmBA,SAAgB,aAAa,MAA+B;CAG1D,MAAM,mBAAmB,KAAK,MAAM;CACpC,MAAM,mBAAmB,iBAAiB,WAAW,GAAG;CACxD,MAAM,UAAU,mBAAmB,KAAK;CACxC,MAAM,OAAiB,CAAC;CACxB,MAAM,UAA4C,CAAC;CAGnD,MAAM,aAAa,mBAAmB,IAAI;CAC1C,KAAK,IAAI,IAAI,YAAY,IAAI,KAAK,QAAQ,KAAK;EAC7C,MAAM,MAAM,KAAK;EAEjB,IAAI,IAAI,WAAW,IAAI,GAAG;GAExB,MAAM,gBAAgB,IAAI,MAAM,CAAC;GACjC,MAAM,aAAa,cAAc,QAAQ,GAAG;GAE5C,IAAI,eAAe,IAAI;IAErB,MAAM,MAAM,YAAY,cAAc,MAAM,GAAG,UAAU,CAAC;IAE1D,QAAQ,OADM,cAAc,MAAM,aAAa,CAC5B;GACrB,OAAO;IAEL,MAAM,MAAM,YAAY,aAAa;IACrC,MAAM,UAAU,KAAK,IAAI;IAGzB,IAAI,WAAW,CAAC,QAAQ,WAAW,GAAG,GAAG;KACvC,QAAQ,OAAO;KACf;IACF,OACE,QAAQ,OAAO;GAEnB;EACF,OAAO,IAAI,IAAI,WAAW,GAAG,KAAK,IAAI,SAAS,GAAG;GAEhD,MAAM,QAAQ,IAAI,MAAM,CAAC;GACzB,MAAM,aAAa,MAAM,QAAQ,GAAG;GAEpC,IAAI,eAAe,IAAI;IAErB,MAAM,MAAM,YAAY,MAAM,MAAM,GAAG,UAAU,CAAC;IAElD,QAAQ,OADM,MAAM,MAAM,aAAa,CACpB;GACrB,OAAO,IAAI,MAAM,WAAW,GAAG;IAE7B,MAAM,UAAU,KAAK,IAAI;IAEzB,IAAI,WAAW,CAAC,QAAQ,WAAW,GAAG,GAAG;KACvC,QAAQ,YAAY,KAAK,KAAK;KAC9B;IACF,OACE,QAAQ,YAAY,KAAK,KAAK;GAElC,OAEE,KAAK,MAAM,QAAQ,OACjB,QAAQ,YAAY,IAAI,KAAK;EAGnC,OAEE,KAAK,KAAK,GAAG;CAEjB;CAEA,OAAO;EAAE,MAAM;EAAS;EAAM;CAAQ;AACxC"}
1
+ {"version":3,"file":"parse-cli-args.mjs","names":[],"sources":["../../../../../../../core/src/cli/parse-cli-args.ts"],"sourcesContent":["import { toCamelCase } from \"@mongez/reinforcements\";\n\nexport type ParsedCliArgs = {\n name: string;\n args: string[];\n options: Record<string, string | boolean>;\n};\n\n/**\n * The slice of a declared command option this parser needs.\n *\n * `CLICommand.commandOptions` (`ResolvedCLICommandOption[]`) satisfies it, but\n * the parser stays independent of the command classes so it can be unit-tested\n * and reused with a hand-written schema.\n */\nexport type CliOptionSchema = {\n name?: string;\n alias?: string;\n type?: \"string\" | \"boolean\" | \"number\";\n};\n\n/**\n * Thrown when a declared boolean option is given a value that is neither\n * true-ish nor false-ish.\n *\n * We refuse to guess. Guessing is exactly what produced #21: `--rollback=false`\n * was kept as the truthy string `\"false\"` and dropped every table. An option\n * that gates a destructive action must fail loudly on input it cannot read.\n */\nexport class CliOptionValueError extends Error {\n public constructor(\n public readonly optionToken: string,\n public readonly value: string,\n ) {\n super(\n `Invalid value ${JSON.stringify(value)} for boolean option ${optionToken}. ` +\n `Expected one of: true, false, 1, 0, yes, no — or pass ${optionToken} on its own for true.`,\n );\n\n this.name = \"CliOptionValueError\";\n }\n}\n\nconst TRUE_VALUES = new Set([\"true\", \"1\", \"yes\"]);\nconst FALSE_VALUES = new Set([\"false\", \"0\", \"no\"]);\n\n/**\n * Collect the camelCased keys (names AND aliases) of every option the command\n * declared as `type: \"boolean\"`.\n *\n * Both sides are camelCased because declared names keep their raw kebab form\n * (`\"pending-only\"`) while the parser emits camelCase keys (`pendingOnly`).\n */\nfunction collectBooleanKeys(schema: CliOptionSchema[]): Set<string> {\n const keys = new Set<string>();\n\n for (const option of schema) {\n if (option.type !== \"boolean\") continue;\n\n if (option.name) {\n keys.add(toCamelCase(option.name));\n }\n\n if (option.alias) {\n keys.add(toCamelCase(option.alias));\n }\n }\n\n return keys;\n}\n\n/**\n * Turn the value written after `=` into a real boolean, or throw.\n */\nfunction toBoolean(optionToken: string, value: string): boolean {\n const normalized = value.toLowerCase();\n\n if (TRUE_VALUES.has(normalized)) return true;\n if (FALSE_VALUES.has(normalized)) return false;\n\n throw new CliOptionValueError(optionToken, value);\n}\n\n/**\n * Parse CLI arguments from process.argv\n *\n * Called with a `schema` (the resolved command's declared options), every\n * option declared `type: \"boolean\"` is resolved as a boolean:\n * `--flag` → true, `--flag=false|0|no` → false, `--flag=true|1|yes` → true,\n * and `--flag <token>` leaves `<token>` as a POSITIONAL instead of swallowing\n * it as the flag's value. Anything else throws {@link CliOptionValueError}.\n *\n * Called without a schema — which is how the command name is discovered,\n * before any command object exists — behaviour is unchanged: every value stays\n * a raw string and a bare `--flag` still consumes the following non-dash token.\n * Nothing is blanket-coerced here on purpose: `--name=false` on a string-typed\n * option must survive as the string `\"false\"`.\n *\n * Options the schema does not declare keep the schema-less behaviour.\n *\n * @example\n * parseCliArgs([\"node\", \"warlock\", \"migrate\", \"--rollback\", \"file.ts\"])\n * // Returns: { name: \"migrate\", args: [], options: { rollback: \"file.ts\" } }\n *\n * @example\n * // `rollback` declared as type: \"boolean\" — the file stays a positional\n * parseCliArgs([\"node\", \"warlock\", \"migrate\", \"--rollback\", \"file.ts\"], migrateOptions)\n * // Returns: { name: \"migrate\", args: [\"file.ts\"], options: { rollback: true } }\n *\n * @example\n * parseCliArgs([\"node\", \"warlock\", \"dev\", \"--port=3000\", \"--fresh\"])\n * // Returns: { name: \"dev\", args: [], options: { port: \"3000\", fresh: true } }\n */\nexport function parseCliArgs(argv: string[], schema: CliOptionSchema[] = []): ParsedCliArgs {\n // Command is at index 2 (after \"node\" and script path)\n // But if index 2 starts with \"-\", it's an option, not a command\n const potentialCommand = argv[2] || \"\";\n const isFirstArgOption = potentialCommand.startsWith(\"-\");\n const command = isFirstArgOption ? \"\" : potentialCommand;\n const args: string[] = [];\n const options: Record<string, string | boolean> = {};\n const booleanKeys = collectBooleanKeys(schema);\n\n // Parse arguments starting from index 3 (or 2 if first arg was an option)\n const startIndex = isFirstArgOption ? 2 : 3;\n for (let i = startIndex; i < argv.length; i++) {\n const arg = argv[i];\n\n if (arg.startsWith(\"--\")) {\n // Long option: --key or --key=value\n const withoutDashes = arg.slice(2);\n const equalIndex = withoutDashes.indexOf(\"=\");\n\n if (equalIndex !== -1) {\n // --key=value format\n const key = toCamelCase(withoutDashes.slice(0, equalIndex));\n const value = withoutDashes.slice(equalIndex + 1);\n options[key] = booleanKeys.has(key)\n ? toBoolean(`--${withoutDashes.slice(0, equalIndex)}`, value)\n : value;\n } else {\n // --key format (check if next arg is a value)\n const key = toCamelCase(withoutDashes);\n const nextArg = argv[i + 1];\n\n // A declared boolean NEVER consumes the next token: `--rollback\n // users.ts` means \"rollback, and here is a positional\", not \"rollback\n // the file users.ts\". Swallowing it silently discarded the operator's\n // only stated scope and rolled back everything.\n if (booleanKeys.has(key)) {\n options[key] = true;\n } else if (nextArg && !nextArg.startsWith(\"-\")) {\n // If next arg exists and doesn't start with -, treat it as value\n options[key] = nextArg;\n i++; // Skip next arg\n } else {\n options[key] = true;\n }\n }\n } else if (arg.startsWith(\"-\") && arg.length > 1) {\n // Short option: -f, -t=value, or -abc (multiple flags)\n const flags = arg.slice(1);\n const equalIndex = flags.indexOf(\"=\");\n\n if (equalIndex !== -1) {\n // -t=value format\n const rawKey = flags.slice(0, equalIndex);\n const key = toCamelCase(rawKey);\n const value = flags.slice(equalIndex + 1);\n options[key] = booleanKeys.has(key) ? toBoolean(`-${rawKey}`, value) : value;\n } else if (flags.length === 1) {\n // Single flag: -f\n const key = toCamelCase(flags);\n const nextArg = argv[i + 1];\n\n if (booleanKeys.has(key)) {\n options[key] = true;\n } else if (nextArg && !nextArg.startsWith(\"-\")) {\n options[key] = nextArg;\n i++; // Skip next arg\n } else {\n options[key] = true;\n }\n } else {\n // Multiple flags: -abc becomes { a: true, b: true, c: true }\n // A bundle carries no value and never consumes the next token, so a\n // declared boolean already resolves to `true` here.\n for (const flag of flags) {\n options[toCamelCase(flag)] = true;\n }\n }\n } else {\n // Positional argument\n args.push(arg);\n }\n }\n\n return { name: command, args, options };\n}\n"],"mappings":"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"}
@@ -4,13 +4,30 @@ import { Path } from "../dev-server/path.mjs";
4
4
  import { warlockConfigManager } from "../warlock-config/warlock-config.manager.mjs";
5
5
  import { getFilesFromDirectory } from "../dev-server/utils.mjs";
6
6
  import { filesOrchestrator } from "../dev-server/files-orchestrator.mjs";
7
+ import { PENDING_EXIT_CODE, exitCodeFor } from "./pending-exit-code.mjs";
8
+ import { resolvePendingMigrations } from "./resolve-pending-migrations.mjs";
7
9
  import { exportMigrationsSQL, freshMigrate, listExecutedMigrations, migrationRunner, rollbackMigrations, runMigrations } from "@warlock.js/cascade";
8
10
  import path from "path";
9
11
  import { colors } from "@mongez/copper";
10
12
  import dayjs from "dayjs";
11
13
 
12
14
  //#region ../core/src/database/migrate-action.ts
15
+ /**
16
+ * `--list` — print the migration state: what has run, then what will run next.
17
+ *
18
+ * The executed section is printed FIRST and unconditionally, before anything
19
+ * touches disk or config. That ordering is the contract: reading migrations
20
+ * from the table cannot fail because of a broken migration file, and `--list`
21
+ * is the command an operator reaches for *during* an incident. The pending
22
+ * section is best-effort on top of an answer that is already on screen.
23
+ *
24
+ * Always exits 0. It is a report; `--pending` is the gate.
25
+ */
13
26
  async function listMigrationsAction() {
27
+ await printExecutedMigrations();
28
+ printPendingMigrations(await resolvePendingMigrations(loadAllMigrations));
29
+ }
30
+ async function printExecutedMigrations() {
14
31
  const createdMigrations = await listExecutedMigrations();
15
32
  console.log(`\nTotal Executed Migrations: ${colors.green(createdMigrations.length)}\n`);
16
33
  if (createdMigrations.length === 0) {
@@ -26,6 +43,40 @@ async function listMigrationsAction() {
26
43
  console.log("");
27
44
  }
28
45
  }
46
+ /**
47
+ * Print the pending section shared by `--list` and `--pending`.
48
+ *
49
+ * An unavailable result prints an explicit line and NEVER a count. "0 pending"
50
+ * over a tree we failed to read is the one output this whole feature exists to
51
+ * prevent.
52
+ */
53
+ function printPendingMigrations(result) {
54
+ if (result.type === "unavailable") {
55
+ console.log(`${colors.yellowBright("Pending: unavailable")} — ${result.reason}\n` + colors.gray(" The executed list above is still accurate.\n"));
56
+ return;
57
+ }
58
+ const { migrations } = result;
59
+ console.log(`Total Pending Migrations: ${colors.green(migrations.length)}\n`);
60
+ if (migrations.length === 0) {
61
+ console.log(colors.gray(" Nothing pending — the database is up to date.\n"));
62
+ return;
63
+ }
64
+ migrations.forEach((migration, index) => {
65
+ console.log(` ${colors.gray(`${index + 1}.`)} ${colors.cyanBright(migration.name)}`);
66
+ });
67
+ console.log("");
68
+ }
69
+ /**
70
+ * `--pending` — the scriptable half. Same computation as `--list`'s pending
71
+ * section, different exit contract: see {@link PENDING_EXIT_CODE}.
72
+ */
73
+ async function pendingMigrationsAction() {
74
+ const result = await resolvePendingMigrations(loadAllMigrations);
75
+ printPendingMigrations(result);
76
+ const exitCode = exitCodeFor(result);
77
+ if (exitCode === PENDING_EXIT_CODE.unavailable) console.error(colors.redBright("Could not determine the pending set — exiting 2 rather than reporting 0."));
78
+ process.exit(exitCode);
79
+ }
29
80
  async function allMigrationsFilesAction() {
30
81
  const files = (await migrationFiles()).map((path) => Path.toRelative(path));
31
82
  console.log(`Total Migration Files: ${colors.green(files.length)}`);
@@ -37,8 +88,9 @@ async function allMigrationsFilesAction() {
37
88
  * If rollback is provided, then run the migration runner against all files in reverse order
38
89
  */
39
90
  async function migrateAction(options) {
40
- const { fresh, path, rollback, all, list, sql, pendingOnly, compact } = options.options;
91
+ const { fresh, path, rollback, all, list, pending, sql, pendingOnly, compact } = options.options;
41
92
  if (list) return await listMigrationsAction();
93
+ if (pending) return await pendingMigrationsAction();
42
94
  if (all) return await allMigrationsFilesAction();
43
95
  if (path) await loadMigrationFile(Path.toAbsolute(path));
44
96
  else await loadAllMigrations();
@@ -1 +1 @@
1
- {"version":3,"file":"migrate-action.mjs","names":[],"sources":["../../../../../../../core/src/database/migrate-action.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport {\r\n exportMigrationsSQL,\r\n freshMigrate,\r\n listExecutedMigrations,\r\n Migration,\r\n migrationRunner,\r\n rollbackMigrations,\r\n runMigrations,\r\n} from \"@warlock.js/cascade\";\r\nimport dayjs from \"dayjs\";\r\nimport path from \"path\";\r\nimport { CommandActionData } from \"../cli/types\";\r\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\r\nimport { Path } from \"../dev-server/path\";\r\nimport { getFilesFromDirectory } from \"../dev-server/utils\";\r\nimport { srcPath } from \"../utils\";\r\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\r\n\r\nasync function listMigrationsAction() {\r\n const createdMigrations = await listExecutedMigrations();\r\n\r\n console.log(`\\nTotal Executed Migrations: ${colors.green(createdMigrations.length)}\\n`);\r\n\r\n if (createdMigrations.length === 0) {\r\n console.log(colors.gray(\" No migrations have been executed yet.\\n\"));\r\n return;\r\n }\r\n\r\n // Display each migration as a block\r\n for (const migration of createdMigrations) {\r\n const executedAt = dayjs(migration.executedAt).format(\"DD-MM-YYYY hh:mm:ss A\");\r\n const createdAt = migration.createdAt\r\n ? dayjs(migration.createdAt).format(\"DD-MM-YYYY hh:mm:ss A\")\r\n : null;\r\n\r\n // Migration name with checkmark icon\r\n console.log(` ${colors.green(\"✔\")} ${colors.cyanBright(migration.name)}`);\r\n\r\n // Executed date\r\n console.log(` ${colors.gray(\"Executed:\")} ${colors.white(executedAt)}`);\r\n\r\n // Created date (if available)\r\n if (createdAt) {\r\n console.log(` ${colors.gray(\"Created:\")} ${colors.yellow(createdAt)}`);\r\n }\r\n\r\n console.log(\"\"); // Empty line between migrations\r\n }\r\n}\r\n\r\nasync function allMigrationsFilesAction() {\r\n // get all available migration files in the project\r\n const files = (await migrationFiles()).map((path) => Path.toRelative(path));\r\n console.log(`Total Migration Files: ${colors.green(files.length)}`);\r\n\r\n for (const file of files) {\r\n console.log(colors.yellowBright(file));\r\n }\r\n}\r\n\r\n/**\r\n * If path is provided, then run the migration runner against that file only\r\n * If fresh is provided, then rollback all migrations and run all migrations\r\n * If rollback is provided, then run the migration runner against all files in reverse order\r\n */\r\nexport async function migrateAction(options: CommandActionData) {\r\n const { fresh, path, rollback, all, list, sql, pendingOnly, compact } = options.options;\r\n\r\n if (list) {\r\n return await listMigrationsAction();\r\n }\r\n\r\n if (all) {\r\n return await allMigrationsFilesAction();\r\n }\r\n\r\n if (path) {\r\n await loadMigrationFile(Path.toAbsolute(path as string));\r\n } else {\r\n await loadAllMigrations();\r\n }\r\n\r\n if (fresh && rollback) {\r\n console.log(colors.redBright(\"You can't use --fresh and --rollback together\"));\r\n process.exit(1);\r\n }\r\n\r\n if (rollback) {\r\n await rollbackMigrations({ all: true });\r\n return;\r\n }\r\n\r\n if (sql) {\r\n await exportMigrationsSQL({\r\n pendingOnly: pendingOnly as boolean,\r\n compact: compact as boolean,\r\n });\r\n return;\r\n }\r\n\r\n if (fresh) {\r\n await freshMigrate();\r\n return;\r\n }\r\n\r\n await runMigrations();\r\n}\r\n\r\nasync function loadMigrationFile(absPath: string) {\r\n const relativePath = Path.toRelative(absPath);\r\n\r\n const loadedModule = await filesOrchestrator.load<{ default: typeof Migration }>(relativePath);\r\n\r\n if (!loadedModule?.default) {\r\n throw new Error(`${Path.toRelative(absPath)} must have a default export`);\r\n }\r\n\r\n const MigrationClass = loadedModule.default;\r\n\r\n if (!MigrationClass.migrationName) {\r\n MigrationClass.migrationName = path\r\n .basename(absPath)\r\n .split(\".\")[0]\r\n .replace(\"-migration\", \"\")\r\n .replace(\"_migration\", \"\");\r\n }\r\n\r\n // Extract createdAt timestamp from filename if not already set\r\n // Expected format: MM-DD-YYYY_HH-MM-SS-name.migration.ts\r\n if (!MigrationClass.createdAt) {\r\n const filename = path.basename(absPath);\r\n const timestampMatch = filename.match(/^(\\d{2}-\\d{2}-\\d{4}_\\d{2}-\\d{2}-\\d{2})/);\r\n if (timestampMatch) {\r\n MigrationClass.createdAt = timestampMatch[1];\r\n }\r\n }\r\n\r\n migrationRunner.register(MigrationClass);\r\n}\r\n\r\n/**\r\n *\r\n * @returns List of absolute paths to migration files\r\n */\r\nasync function migrationFiles() {\r\n const migrationFiles = await getFilesFromDirectory(srcPath(\"app\"), \"*/models/*/migrations/*\");\r\n const separateMigrationsFolderFIles = await getFilesFromDirectory(\r\n srcPath(\"app\"),\r\n \"*/migrations/*\",\r\n );\r\n\r\n const migrations = [...migrationFiles, ...separateMigrationsFolderFIles];\r\n\r\n return migrations;\r\n}\r\n\r\nasync function loadAllMigrations() {\r\n // Load config-registered migrations (from packages like @warlock.js/auth)\r\n const configMigrations = warlockConfigManager.get(\"database\")?.migrations || [];\r\n\r\n for (const MigrationClass of configMigrations) {\r\n // Use class name as migration name if not set\r\n if (!MigrationClass.migrationName) {\r\n MigrationClass.migrationName = MigrationClass.name;\r\n }\r\n migrationRunner.register(MigrationClass);\r\n }\r\n\r\n // Always load file-based migrations from src/app, regardless of config\r\n const migrations = await migrationFiles();\r\n for (const migrationFile of migrations) {\r\n await loadMigrationFile(migrationFile);\r\n }\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"migrate-action.mjs","names":[],"sources":["../../../../../../../core/src/database/migrate-action.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport {\r\n exportMigrationsSQL,\r\n freshMigrate,\r\n listExecutedMigrations,\r\n Migration,\r\n migrationRunner,\r\n rollbackMigrations,\r\n runMigrations,\r\n} from \"@warlock.js/cascade\";\r\nimport dayjs from \"dayjs\";\r\nimport path from \"path\";\r\nimport { CommandActionData } from \"../cli/types\";\r\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\r\nimport { Path } from \"../dev-server/path\";\r\nimport { getFilesFromDirectory } from \"../dev-server/utils\";\r\nimport { srcPath } from \"../utils\";\r\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\r\nimport { exitCodeFor, PENDING_EXIT_CODE } from \"./pending-exit-code\";\r\nimport {\r\n resolvePendingMigrations,\r\n type PendingMigrationsResult,\r\n} from \"./resolve-pending-migrations\";\r\n\r\n/**\r\n * `--list` — print the migration state: what has run, then what will run next.\r\n *\r\n * The executed section is printed FIRST and unconditionally, before anything\r\n * touches disk or config. That ordering is the contract: reading migrations\r\n * from the table cannot fail because of a broken migration file, and `--list`\r\n * is the command an operator reaches for *during* an incident. The pending\r\n * section is best-effort on top of an answer that is already on screen.\r\n *\r\n * Always exits 0. It is a report; `--pending` is the gate.\r\n */\r\nasync function listMigrationsAction() {\r\n await printExecutedMigrations();\r\n\r\n const pending = await resolvePendingMigrations(loadAllMigrations);\r\n\r\n printPendingMigrations(pending);\r\n}\r\n\r\nasync function printExecutedMigrations() {\r\n const createdMigrations = await listExecutedMigrations();\r\n\r\n console.log(`\\nTotal Executed Migrations: ${colors.green(createdMigrations.length)}\\n`);\r\n\r\n if (createdMigrations.length === 0) {\r\n console.log(colors.gray(\" No migrations have been executed yet.\\n\"));\r\n return;\r\n }\r\n\r\n // Display each migration as a block\r\n for (const migration of createdMigrations) {\r\n const executedAt = dayjs(migration.executedAt).format(\"DD-MM-YYYY hh:mm:ss A\");\r\n const createdAt = migration.createdAt\r\n ? dayjs(migration.createdAt).format(\"DD-MM-YYYY hh:mm:ss A\")\r\n : null;\r\n\r\n // Migration name with checkmark icon\r\n console.log(` ${colors.green(\"✔\")} ${colors.cyanBright(migration.name)}`);\r\n\r\n // Executed date\r\n console.log(` ${colors.gray(\"Executed:\")} ${colors.white(executedAt)}`);\r\n\r\n // Created date (if available)\r\n if (createdAt) {\r\n console.log(` ${colors.gray(\"Created:\")} ${colors.yellow(createdAt)}`);\r\n }\r\n\r\n console.log(\"\"); // Empty line between migrations\r\n }\r\n}\r\n\r\n/**\r\n * Print the pending section shared by `--list` and `--pending`.\r\n *\r\n * An unavailable result prints an explicit line and NEVER a count. \"0 pending\"\r\n * over a tree we failed to read is the one output this whole feature exists to\r\n * prevent.\r\n */\r\nfunction printPendingMigrations(result: PendingMigrationsResult) {\r\n if (result.type === \"unavailable\") {\r\n console.log(\r\n `${colors.yellowBright(\"Pending: unavailable\")} — ${result.reason}\\n` +\r\n colors.gray(\" The executed list above is still accurate.\\n\"),\r\n );\r\n return;\r\n }\r\n\r\n const { migrations } = result;\r\n\r\n console.log(`Total Pending Migrations: ${colors.green(migrations.length)}\\n`);\r\n\r\n if (migrations.length === 0) {\r\n console.log(colors.gray(\" Nothing pending — the database is up to date.\\n\"));\r\n return;\r\n }\r\n\r\n // Numbered because the ORDER is the answer: this is what the next\r\n // `warlock migrate` will do, in the sequence it will do it.\r\n migrations.forEach((migration, index) => {\r\n console.log(` ${colors.gray(`${index + 1}.`)} ${colors.cyanBright(migration.name)}`);\r\n });\r\n\r\n console.log(\"\");\r\n}\r\n\r\n/**\r\n * `--pending` — the scriptable half. Same computation as `--list`'s pending\r\n * section, different exit contract: see {@link PENDING_EXIT_CODE}.\r\n */\r\nasync function pendingMigrationsAction() {\r\n const result = await resolvePendingMigrations(loadAllMigrations);\r\n\r\n printPendingMigrations(result);\r\n\r\n const exitCode = exitCodeFor(result);\r\n\r\n if (exitCode === PENDING_EXIT_CODE.unavailable) {\r\n console.error(\r\n colors.redBright(\"Could not determine the pending set — exiting 2 rather than reporting 0.\"),\r\n );\r\n }\r\n\r\n process.exit(exitCode);\r\n}\r\n\r\nasync function allMigrationsFilesAction() {\r\n // get all available migration files in the project\r\n const files = (await migrationFiles()).map((path) => Path.toRelative(path));\r\n console.log(`Total Migration Files: ${colors.green(files.length)}`);\r\n\r\n for (const file of files) {\r\n console.log(colors.yellowBright(file));\r\n }\r\n}\r\n\r\n/**\r\n * If path is provided, then run the migration runner against that file only\r\n * If fresh is provided, then rollback all migrations and run all migrations\r\n * If rollback is provided, then run the migration runner against all files in reverse order\r\n */\r\nexport async function migrateAction(options: CommandActionData) {\r\n const { fresh, path, rollback, all, list, pending, sql, pendingOnly, compact } = options.options;\r\n\r\n if (list) {\r\n return await listMigrationsAction();\r\n }\r\n\r\n if (pending) {\r\n return await pendingMigrationsAction();\r\n }\r\n\r\n if (all) {\r\n return await allMigrationsFilesAction();\r\n }\r\n\r\n if (path) {\r\n await loadMigrationFile(Path.toAbsolute(path as string));\r\n } else {\r\n await loadAllMigrations();\r\n }\r\n\r\n if (fresh && rollback) {\r\n console.log(colors.redBright(\"You can't use --fresh and --rollback together\"));\r\n process.exit(1);\r\n }\r\n\r\n if (rollback) {\r\n await rollbackMigrations({ all: true });\r\n return;\r\n }\r\n\r\n if (sql) {\r\n await exportMigrationsSQL({\r\n pendingOnly: pendingOnly as boolean,\r\n compact: compact as boolean,\r\n });\r\n return;\r\n }\r\n\r\n if (fresh) {\r\n await freshMigrate();\r\n return;\r\n }\r\n\r\n await runMigrations();\r\n}\r\n\r\nasync function loadMigrationFile(absPath: string) {\r\n const relativePath = Path.toRelative(absPath);\r\n\r\n const loadedModule = await filesOrchestrator.load<{ default: typeof Migration }>(relativePath);\r\n\r\n if (!loadedModule?.default) {\r\n throw new Error(`${Path.toRelative(absPath)} must have a default export`);\r\n }\r\n\r\n const MigrationClass = loadedModule.default;\r\n\r\n if (!MigrationClass.migrationName) {\r\n MigrationClass.migrationName = path\r\n .basename(absPath)\r\n .split(\".\")[0]\r\n .replace(\"-migration\", \"\")\r\n .replace(\"_migration\", \"\");\r\n }\r\n\r\n // Extract createdAt timestamp from filename if not already set\r\n // Expected format: MM-DD-YYYY_HH-MM-SS-name.migration.ts\r\n if (!MigrationClass.createdAt) {\r\n const filename = path.basename(absPath);\r\n const timestampMatch = filename.match(/^(\\d{2}-\\d{2}-\\d{4}_\\d{2}-\\d{2}-\\d{2})/);\r\n if (timestampMatch) {\r\n MigrationClass.createdAt = timestampMatch[1];\r\n }\r\n }\r\n\r\n migrationRunner.register(MigrationClass);\r\n}\r\n\r\n/**\r\n *\r\n * @returns List of absolute paths to migration files\r\n */\r\nasync function migrationFiles() {\r\n const migrationFiles = await getFilesFromDirectory(srcPath(\"app\"), \"*/models/*/migrations/*\");\r\n const separateMigrationsFolderFIles = await getFilesFromDirectory(\r\n srcPath(\"app\"),\r\n \"*/migrations/*\",\r\n );\r\n\r\n const migrations = [...migrationFiles, ...separateMigrationsFolderFIles];\r\n\r\n return migrations;\r\n}\r\n\r\nasync function loadAllMigrations() {\r\n // Load config-registered migrations (from packages like @warlock.js/auth)\r\n const configMigrations = warlockConfigManager.get(\"database\")?.migrations || [];\r\n\r\n for (const MigrationClass of configMigrations) {\r\n // Use class name as migration name if not set\r\n if (!MigrationClass.migrationName) {\r\n MigrationClass.migrationName = MigrationClass.name;\r\n }\r\n migrationRunner.register(MigrationClass);\r\n }\r\n\r\n // Always load file-based migrations from src/app, regardless of config\r\n const migrations = await migrationFiles();\r\n for (const migrationFile of migrations) {\r\n await loadMigrationFile(migrationFile);\r\n }\r\n}\r\n"],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ //#region ../core/src/database/pending-exit-code.ts
2
+ /**
3
+ * Exit codes for `warlock migrate --pending`.
4
+ *
5
+ * `--pending` is a GATE, not a report: it is written into `migrate --pending &&
6
+ * deploy` and into CI steps asserting nothing is outstanding. So it must
7
+ * distinguish three outcomes, not two.
8
+ *
9
+ * Two codes would fold `pending` and `unavailable` into a single non-zero, and
10
+ * a script could no longer tell "three migrations are waiting" from "I could
11
+ * not work out what is waiting". Those demand opposite responses — the first is
12
+ * "run them", the second is "stop and get a human".
13
+ *
14
+ * `warlock migrate --list` is the report half and always exits `0`, whatever
15
+ * the pending section says.
16
+ */
17
+ const PENDING_EXIT_CODE = {
18
+ /** Computed, and nothing is pending. */
19
+ clear: 0,
20
+ /** Computed, and at least one migration is pending. */
21
+ pending: 1,
22
+ /** Could not be computed. Never conflate this with `clear`. */
23
+ unavailable: 2
24
+ };
25
+ /**
26
+ * Map a resolution outcome onto the exit code a gate should report.
27
+ *
28
+ * @example
29
+ * exitCodeFor({ type: "resolved", migrations: [] }); // 0
30
+ * exitCodeFor({ type: "unavailable", reason: "…" }); // 2
31
+ */
32
+ function exitCodeFor(result) {
33
+ if (result.type === "unavailable") return PENDING_EXIT_CODE.unavailable;
34
+ return result.migrations.length > 0 ? PENDING_EXIT_CODE.pending : PENDING_EXIT_CODE.clear;
35
+ }
36
+
37
+ //#endregion
38
+ export { PENDING_EXIT_CODE, exitCodeFor };
39
+ //# sourceMappingURL=pending-exit-code.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pending-exit-code.mjs","names":[],"sources":["../../../../../../../core/src/database/pending-exit-code.ts"],"sourcesContent":["import type { PendingMigrationsResult } from \"./resolve-pending-migrations\";\n\n/**\n * Exit codes for `warlock migrate --pending`.\n *\n * `--pending` is a GATE, not a report: it is written into `migrate --pending &&\n * deploy` and into CI steps asserting nothing is outstanding. So it must\n * distinguish three outcomes, not two.\n *\n * Two codes would fold `pending` and `unavailable` into a single non-zero, and\n * a script could no longer tell \"three migrations are waiting\" from \"I could\n * not work out what is waiting\". Those demand opposite responses — the first is\n * \"run them\", the second is \"stop and get a human\".\n *\n * `warlock migrate --list` is the report half and always exits `0`, whatever\n * the pending section says.\n */\nexport const PENDING_EXIT_CODE = {\n /** Computed, and nothing is pending. */\n clear: 0,\n /** Computed, and at least one migration is pending. */\n pending: 1,\n /** Could not be computed. Never conflate this with `clear`. */\n unavailable: 2,\n} as const;\n\nexport type PendingExitCode = (typeof PENDING_EXIT_CODE)[keyof typeof PENDING_EXIT_CODE];\n\n/**\n * Map a resolution outcome onto the exit code a gate should report.\n *\n * @example\n * exitCodeFor({ type: \"resolved\", migrations: [] }); // 0\n * exitCodeFor({ type: \"unavailable\", reason: \"…\" }); // 2\n */\nexport function exitCodeFor(result: PendingMigrationsResult): PendingExitCode {\n if (result.type === \"unavailable\") {\n return PENDING_EXIT_CODE.unavailable;\n }\n\n return result.migrations.length > 0 ? PENDING_EXIT_CODE.pending : PENDING_EXIT_CODE.clear;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAiBA,MAAa,oBAAoB;;CAE/B,OAAO;;CAEP,SAAS;;CAET,aAAa;AACf;;;;;;;;AAWA,SAAgB,YAAY,QAAkD;CAC5E,IAAI,OAAO,SAAS,eAClB,OAAO,kBAAkB;CAG3B,OAAO,OAAO,WAAW,SAAS,IAAI,kBAAkB,UAAU,kBAAkB;AACtF"}
@@ -0,0 +1,49 @@
1
+ import { listPendingMigrations } from "@warlock.js/cascade";
2
+
3
+ //#region ../core/src/database/resolve-pending-migrations.ts
4
+ /**
5
+ * Load the project's migrations, then compute which of them have not run yet.
6
+ *
7
+ * `loadMigrations` must be the SAME registration path a real `warlock migrate`
8
+ * takes. Skipping it does not fail — it silently yields an empty pending set,
9
+ * because the pending set is computed from the runner's registry.
10
+ *
11
+ * Loading executes project code (a migration file is imported, and a broken one
12
+ * throws), so every failure is converted into an `unavailable` result rather
13
+ * than propagating. Callers decide what an unknown means for them: a listing
14
+ * prints it and carries on, a gate refuses to pass.
15
+ *
16
+ * @example
17
+ * const result = await resolvePendingMigrations(loadAllMigrations);
18
+ *
19
+ * if (result.type === "unavailable") {
20
+ * console.error(`Pending: unavailable — ${result.reason}`);
21
+ * }
22
+ */
23
+ async function resolvePendingMigrations(loadMigrations) {
24
+ try {
25
+ await loadMigrations();
26
+ return {
27
+ type: "resolved",
28
+ migrations: await listPendingMigrations()
29
+ };
30
+ } catch (error) {
31
+ return {
32
+ type: "unavailable",
33
+ reason: describeFailure(error)
34
+ };
35
+ }
36
+ }
37
+ /**
38
+ * Reduce a thrown value to a single line an operator can act on. The message is
39
+ * the actionable part — a stack trace in the middle of a listing buries the
40
+ * executed section the reader still needs.
41
+ */
42
+ function describeFailure(error) {
43
+ if (error instanceof Error) return error.message;
44
+ return String(error);
45
+ }
46
+
47
+ //#endregion
48
+ export { resolvePendingMigrations };
49
+ //# sourceMappingURL=resolve-pending-migrations.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-pending-migrations.mjs","names":[],"sources":["../../../../../../../core/src/database/resolve-pending-migrations.ts"],"sourcesContent":["import { listPendingMigrations, type PendingMigration } from \"@warlock.js/cascade\";\n\n/**\n * Outcome of trying to compute the pending migration set.\n *\n * The \"unavailable\" arm exists because the two failure modes are NOT the same\n * answer: a database with nothing pending and a tree we could not read both\n * produce an empty list, and only one of them means it is safe to proceed.\n * Collapsing them into `[]` is how a pre-flight check reports a confident\n * all-clear over an unknown.\n */\nexport type PendingMigrationsResult =\n | {\n readonly type: \"resolved\";\n readonly migrations: PendingMigration[];\n }\n | {\n readonly type: \"unavailable\";\n readonly reason: string;\n };\n\n/**\n * Load the project's migrations, then compute which of them have not run yet.\n *\n * `loadMigrations` must be the SAME registration path a real `warlock migrate`\n * takes. Skipping it does not fail — it silently yields an empty pending set,\n * because the pending set is computed from the runner's registry.\n *\n * Loading executes project code (a migration file is imported, and a broken one\n * throws), so every failure is converted into an `unavailable` result rather\n * than propagating. Callers decide what an unknown means for them: a listing\n * prints it and carries on, a gate refuses to pass.\n *\n * @example\n * const result = await resolvePendingMigrations(loadAllMigrations);\n *\n * if (result.type === \"unavailable\") {\n * console.error(`Pending: unavailable — ${result.reason}`);\n * }\n */\nexport async function resolvePendingMigrations(\n loadMigrations: () => Promise<void>,\n): Promise<PendingMigrationsResult> {\n try {\n await loadMigrations();\n\n const migrations = await listPendingMigrations();\n\n return { type: \"resolved\", migrations };\n } catch (error) {\n return { type: \"unavailable\", reason: describeFailure(error) };\n }\n}\n\n/**\n * Reduce a thrown value to a single line an operator can act on. The message is\n * the actionable part — a stack trace in the middle of a listing buries the\n * executed section the reader still needs.\n */\nfunction describeFailure(error: unknown): string {\n if (error instanceof Error) {\n return error.message;\n }\n\n return String(error);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAwCA,eAAsB,yBACpB,gBACkC;CAClC,IAAI;EACF,MAAM,eAAe;EAIrB,OAAO;GAAE,MAAM;GAAY,kBAFF,sBAAsB;EAET;CACxC,SAAS,OAAO;EACd,OAAO;GAAE,MAAM;GAAe,QAAQ,gBAAgB,KAAK;EAAE;CAC/D;AACF;;;;;;AAOA,SAAS,gBAAgB,OAAwB;CAC/C,IAAI,iBAAiB,OACnB,OAAO,MAAM;CAGf,OAAO,OAAO,KAAK;AACrB"}
@@ -153,8 +153,9 @@ type InternalOptions = {
153
153
  * **Important:** This class requires the `sharp` package to be installed.
154
154
  * Install it with: `warlock add image` or `npm install sharp`
155
155
  *
156
- * Sharp is lazy-loaded on the first async operation (save, toBuffer, etc.),
157
- * so the constructor and all chainable methods remain synchronous.
156
+ * Sharp is resolved synchronously on the first construction that needs it, so
157
+ * the constructor and all chainable methods remain synchronous, and a missing
158
+ * sharp throws at construction rather than at some later await.
158
159
  *
159
160
  * All operations are synchronous and stored as descriptors.
160
161
  * The pipeline is executed only when calling output methods:
@@ -1 +1 @@
1
- {"version":3,"file":"image.d.mts","names":[],"sources":["../../../../../../../core/src/image/image.ts"],"mappings":"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"}
1
+ {"version":3,"file":"image.d.mts","names":[],"sources":["../../../../../../../core/src/image/image.ts"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { createRequire } from "node:module";
1
2
  import { http } from "@mongez/http";
2
3
 
3
4
  //#region ../core/src/image/image.ts
@@ -17,33 +18,79 @@ Or manually:
17
18
  yarn add sharp
18
19
  `.trim();
19
20
  /**
20
- * Module availability flag
21
+ * Cached sharp function, populated by the first `resolveSharp()` call
21
22
  */
22
- let moduleExists = null;
23
+ let sharpFn = null;
23
24
  /**
24
- * Cached sharp function (loaded at import time)
25
+ * Whether the resolution attempt already ran (success or failure)
25
26
  */
26
- let sharpFn;
27
+ let sharpResolved = false;
27
28
  /**
28
- * Eagerly load sharp module at import time
29
+ * Why the resolution failed, when it failed for a reason other than absence.
30
+ *
31
+ * Cached and re-thrown on every later call: the resolution attempt runs exactly
32
+ * once, so without this the second construction would fall through to the
33
+ * "not installed" branch and report a different, wrong cause.
34
+ */
35
+ let sharpLoadError = null;
36
+ const ErrorWithCause = Error;
37
+ /**
38
+ * Whether `error` says sharp itself is absent, as opposed to present but broken.
39
+ *
40
+ * Both halves are load-bearing. `MODULE_NOT_FOUND` alone is not enough: a
41
+ * dependency missing *inside* sharp raises the very same code (`Cannot find
42
+ * module 'color'`), and treating that as absence would tell the operator to
43
+ * install a package they already have. So the message has to name the specifier
44
+ * `'sharp'` exactly — quoted, which is also what keeps `'sharp-cli'` and friends
45
+ * from matching.
46
+ */
47
+ function isSharpMissing(error) {
48
+ if (!(error instanceof Error)) return false;
49
+ return error.code === "MODULE_NOT_FOUND" && error.message.includes("Cannot find module 'sharp'");
50
+ }
51
+ /**
52
+ * Resolve sharp synchronously, on first use.
53
+ *
54
+ * Resolution is deliberately *lazy* and *synchronous*:
55
+ *
56
+ * - Lazy, because a top-level require would drag sharp's native binary into
57
+ * every `import "@warlock.js/core"`, including apps that never touch images.
58
+ * - Synchronous, because the `Image` constructor is synchronous. An async
59
+ * import kicked off at module load leaves a window in which the module is
60
+ * neither loaded nor known to be missing, and a constructor running inside
61
+ * that window has no correct answer to give.
62
+ *
63
+ * `createRequire` gives an ESM-safe `require`, and the outcome — module, absence
64
+ * or load failure — is cached, so the resolution runs exactly once per process.
65
+ * The failure *reason* is cached too, not just the fact of it: every call after
66
+ * a failed load has to report the same cause as the first one.
67
+ *
68
+ * @throws when sharp is not installed, or is installed but cannot be loaded
29
69
  */
30
- async function loadSharpModule() {
31
- try {
32
- sharpFn = (await import("sharp")).default;
33
- moduleExists = true;
34
- } catch {
35
- moduleExists = false;
70
+ function resolveSharp() {
71
+ if (!sharpResolved) {
72
+ sharpResolved = true;
73
+ try {
74
+ const module = createRequire(import.meta.url)("sharp");
75
+ sharpFn = module.default ?? module;
76
+ } catch (error) {
77
+ sharpFn = null;
78
+ if (!isSharpMissing(error)) sharpLoadError = new ErrorWithCause(`Failed to load "sharp": ${error.message}`, { cause: error });
79
+ }
36
80
  }
81
+ if (sharpLoadError) throw sharpLoadError;
82
+ if (!sharpFn) throw new Error(`sharp is not installed.\n\n${SHARP_INSTALL_INSTRUCTIONS}`);
83
+ return sharpFn;
37
84
  }
38
- loadSharpModule();
39
85
  /**
40
86
  * Image manipulation class with deferred pipeline execution.
41
87
  *
42
88
  * **Important:** This class requires the `sharp` package to be installed.
43
89
  * Install it with: `warlock add image` or `npm install sharp`
44
90
  *
45
- * Sharp is lazy-loaded on the first async operation (save, toBuffer, etc.),
46
- * so the constructor and all chainable methods remain synchronous.
91
+ * Sharp is resolved synchronously on the first construction that needs it, so
92
+ * the constructor and all chainable methods remain synchronous, and a missing
93
+ * sharp throws at construction rather than at some later await.
47
94
  *
48
95
  * All operations are synchronous and stored as descriptors.
49
96
  * The pipeline is executed only when calling output methods:
@@ -81,9 +128,8 @@ var Image = class Image {
81
128
  this.operations = [];
82
129
  this.cachedMetadata = null;
83
130
  this.pipelineExecuted = false;
84
- if (moduleExists === false) throw new Error(`sharp is not installed.\n\n${SHARP_INSTALL_INSTRUCTIONS}`);
85
131
  if (image instanceof Object && "clone" in image && typeof image.clone === "function") this.image = image;
86
- else this.image = sharpFn(image);
132
+ else this.image = resolveSharp()(image);
87
133
  }
88
134
  /**
89
135
  * Create image instance from file path
@@ -321,7 +367,7 @@ var Image = class Image {
321
367
  */
322
368
  async resolveImageBuffer(input) {
323
369
  if (input instanceof Image) return input.image.toBuffer();
324
- return sharpFn(input).toBuffer();
370
+ return resolveSharp()(input).toBuffer();
325
371
  }
326
372
  /**
327
373
  * Apply format and quality options.