@cosmicdrift/kumiko-feature-ast 0.391.1 → 0.392.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-feature-ast",
3
- "version": "0.391.1",
3
+ "version": "0.392.1",
4
4
  "description": "Feature-file AST for Kumiko: parse, patch and render feature definitions, the pattern library, and the codemods kumiko-upgrade runs.",
5
5
  "license": "MIT",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -22,8 +22,8 @@
22
22
  }
23
23
  },
24
24
  "dependencies": {
25
- "@cosmicdrift/kumiko-framework": "0.391.1",
26
- "@cosmicdrift/kumiko-types": "0.391.1",
25
+ "@cosmicdrift/kumiko-framework": "0.392.1",
26
+ "@cosmicdrift/kumiko-types": "0.392.1",
27
27
  "ts-morph": "^28.0.0",
28
28
  "zod": "^4.6.7"
29
29
  },
package/src/changes.json CHANGED
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.392.0",
4
+ "type": "improvement",
5
+ "title": "Codemod entries return their exit code",
6
+ "detail": "The feature-ast codemods export main(argv) and runCodemod next to runCodemodCli; they return an exit code instead of calling process.exit, so callers can run them in-process. Invocation from the shell is unchanged."
7
+ },
2
8
  {
3
9
  "version": "0.391.1",
4
10
  "type": "fix",
@@ -286,13 +286,16 @@ export interface CodemodCliConfig {
286
286
  readonly extraExtensions?: readonly string[];
287
287
  }
288
288
 
289
- function parseArgsOrExit(codemodName: string, argv: readonly string[]): CliOptions {
289
+ function parseArgsOrUsageError(
290
+ codemodName: string,
291
+ argv: readonly string[],
292
+ ): CliOptions | { readonly exitCode: number } {
290
293
  try {
291
294
  return parseArgs(argv);
292
295
  } catch (err) {
293
296
  if (err instanceof CliError) {
294
297
  console.error(`[${codemodName}] ${err.message}`);
295
- process.exit(2);
298
+ return { exitCode: 2 };
296
299
  }
297
300
  throw err;
298
301
  }
@@ -329,8 +332,17 @@ function transformFiles(
329
332
  export async function runCodemodCli(
330
333
  config: CodemodCliConfig,
331
334
  argv: readonly string[],
332
- ): Promise<void> {
333
- const options = parseArgsOrExit(config.name, argv);
335
+ ): Promise<never> {
336
+ process.exit(await runCodemod(config, argv));
337
+ }
338
+
339
+ // Exit code instead of process.exit so the CLI logic can run in-process (tests).
340
+ export async function runCodemod(
341
+ config: CodemodCliConfig,
342
+ argv: readonly string[],
343
+ ): Promise<number> {
344
+ const options = parseArgsOrUsageError(config.name, argv);
345
+ if ("exitCode" in options) return options.exitCode;
334
346
  const files = collectSourceFiles(options.paths, {
335
347
  isMigratableFile: (name) =>
336
348
  isSourceFileName(name) ||
@@ -348,5 +360,5 @@ export async function runCodemodCli(
348
360
  resolve(options.paths[0] ?? "."),
349
361
  );
350
362
  }
351
- process.exit(options.mode === "check" && rewrites.length + manual.length > 0 ? 1 : 0);
363
+ return options.mode === "check" && rewrites.length + manual.length > 0 ? 1 : 0;
352
364
  }
@@ -124,14 +124,14 @@ function runGroup(
124
124
 
125
125
  // kumiko-upgrade runs this as `bun ctx-namespaces.ts <repoDir> [--dry-run]` and treats a non-zero
126
126
  // exit as a failed codemod, so manual sites alone must not fail write or dry-run.
127
- async function main(argv: readonly string[]): Promise<void> {
127
+ export async function main(argv: readonly string[]): Promise<number> {
128
128
  let options: CliOptions;
129
129
  try {
130
130
  options = parseArgs(argv);
131
131
  } catch (error) {
132
132
  if (error instanceof CliError) {
133
133
  console.error(`[${NAME}] ${error.message}`);
134
- process.exit(2);
134
+ return 2;
135
135
  }
136
136
  throw error;
137
137
  }
@@ -157,11 +157,9 @@ async function main(argv: readonly string[]): Promise<void> {
157
157
  if (options.mode === "write" && options.format && totals.changedFiles.length > 0) {
158
158
  await formatWithBiome(NAME, totals.changedFiles, resolve(options.paths[0] ?? "."));
159
159
  }
160
- process.exit(
161
- options.mode === "check" && totals.rewrites.length + totals.manual.length > 0 ? 1 : 0,
162
- );
160
+ return options.mode === "check" && totals.rewrites.length + totals.manual.length > 0 ? 1 : 0;
163
161
  }
164
162
 
165
163
  if (import.meta.main) {
166
- await main(process.argv.slice(2));
164
+ process.exit(await main(process.argv.slice(2)));
167
165
  }
@@ -389,13 +389,13 @@ function writeResult(result: FeatureLayoutResult): void {
389
389
  }
390
390
  }
391
391
 
392
- async function main(argv: readonly string[]): Promise<void> {
392
+ export async function main(argv: readonly string[]): Promise<number> {
393
393
  let options: CliOptions;
394
394
  try {
395
395
  options = parseArgs(argv);
396
396
  } catch (err) {
397
397
  console.error(`[feature-layout] ${err instanceof Error ? err.message : String(err)}`);
398
- process.exit(2);
398
+ return 2;
399
399
  }
400
400
  const files = readTargetFiles(options.paths);
401
401
  const result = applyFeatureLayout(files, options);
@@ -418,7 +418,7 @@ async function main(argv: readonly string[]): Promise<void> {
418
418
  }
419
419
  }
420
420
  const pending = result.markersRemoved.length + result.manual.length;
421
- process.exit(options.mode === "check" && pending > 0 ? 1 : 0);
421
+ return options.mode === "check" && pending > 0 ? 1 : 0;
422
422
  }
423
423
 
424
- if (import.meta.main) await main(process.argv.slice(2));
424
+ if (import.meta.main) process.exit(await main(process.argv.slice(2)));
@@ -683,13 +683,15 @@ function countByMethod(rewrites: readonly ObjectFormRewrite[]): string {
683
683
  .join(", ");
684
684
  }
685
685
 
686
- function parseArgsOrExit(argv: readonly string[]): CliOptions {
686
+ function parseArgsOrUsageError(
687
+ argv: readonly string[],
688
+ ): CliOptions | { readonly exitCode: number } {
687
689
  try {
688
690
  return parseArgs(argv);
689
691
  } catch (err) {
690
692
  if (err instanceof CliError) {
691
693
  console.error(`[object-form] ${err.message}`);
692
- process.exit(2);
694
+ return { exitCode: 2 };
693
695
  }
694
696
  throw err;
695
697
  }
@@ -722,8 +724,9 @@ function transformFiles(files: readonly string[], mode: Mode): RunSummary {
722
724
  return summary;
723
725
  }
724
726
 
725
- async function main(): Promise<void> {
726
- const options = parseArgsOrExit(process.argv.slice(2));
727
+ export async function main(argv: readonly string[]): Promise<number> {
728
+ const options = parseArgsOrUsageError(argv);
729
+ if ("exitCode" in options) return options.exitCode;
727
730
  const files = collectSourceFiles(options.paths, { isMigratableFile }).filter(
728
731
  (file) => !isFixturePath(file),
729
732
  );
@@ -738,9 +741,9 @@ async function main(): Promise<void> {
738
741
  }
739
742
 
740
743
  const hasOpenSites = rewrites.length + manual.length > 0;
741
- process.exit(options.mode === "check" && hasOpenSites ? 1 : 0);
744
+ return options.mode === "check" && hasOpenSites ? 1 : 0;
742
745
  }
743
746
 
744
747
  if (import.meta.main) {
745
- await main();
748
+ process.exit(await main(process.argv.slice(2)));
746
749
  }
@@ -15,6 +15,7 @@
15
15
  import { Node, Project } from "ts-morph";
16
16
  import {
17
17
  applyEdits,
18
+ type CodemodCliConfig,
18
19
  type CodemodResult,
19
20
  detectIndent,
20
21
  isSortedKeys,
@@ -270,15 +271,14 @@ function isSkippedPath(file: string): boolean {
270
271
  return SKIPPED_PATH_SUFFIXES.some((suffix) => file.endsWith(suffix));
271
272
  }
272
273
 
274
+ export const PACKAGE_MERGE_CLI: CodemodCliConfig = {
275
+ name: "package-merge",
276
+ transform: (source, filePath) => transformPackageMergeSource(source, filePath),
277
+ isSkippedPath,
278
+ extraFileNames: ["package.json"],
279
+ extraExtensions: [".css"],
280
+ };
281
+
273
282
  if (import.meta.main) {
274
- await runCodemodCli(
275
- {
276
- name: "package-merge",
277
- transform: (source, filePath) => transformPackageMergeSource(source, filePath),
278
- isSkippedPath,
279
- extraFileNames: ["package.json"],
280
- extraExtensions: [".css"],
281
- },
282
- process.argv.slice(2),
283
- );
283
+ await runCodemodCli(PACKAGE_MERGE_CLI, process.argv.slice(2));
284
284
  }
@@ -506,20 +506,23 @@ function transformFiles(files: readonly string[], mode: Mode): RunSummary {
506
506
  return summary;
507
507
  }
508
508
 
509
- function parseArgsOrExit(argv: readonly string[]): CliOptions {
509
+ function parseArgsOrUsageError(
510
+ argv: readonly string[],
511
+ ): CliOptions | { readonly exitCode: number } {
510
512
  try {
511
513
  return parseArgs(argv);
512
514
  } catch (err) {
513
515
  if (err instanceof CliError) {
514
516
  console.error(`[testing-move] ${err.message}`);
515
- process.exit(2);
517
+ return { exitCode: 2 };
516
518
  }
517
519
  throw err;
518
520
  }
519
521
  }
520
522
 
521
- async function main(): Promise<void> {
522
- const options = parseArgsOrExit(process.argv.slice(2));
523
+ export async function main(argv: readonly string[]): Promise<number> {
524
+ const options = parseArgsOrUsageError(argv);
525
+ if ("exitCode" in options) return options.exitCode;
523
526
  const files = collectSourceFiles(options.paths, { isMigratableFile }).filter(
524
527
  (file) => !isSkippedPath(file),
525
528
  );
@@ -534,9 +537,9 @@ async function main(): Promise<void> {
534
537
  }
535
538
 
536
539
  const hasOpenSites = rewrites.length + manual.length > 0;
537
- process.exit(options.mode === "check" && hasOpenSites ? 1 : 0);
540
+ return options.mode === "check" && hasOpenSites ? 1 : 0;
538
541
  }
539
542
 
540
543
  if (import.meta.main) {
541
- await main();
544
+ process.exit(await main(process.argv.slice(2)));
542
545
  }
@@ -365,9 +365,9 @@ function migrateFile(sourceFile: SourceFile, skips: SkipEntry[]): number {
365
365
  return removedCount;
366
366
  }
367
367
 
368
- async function main(): Promise<void> {
369
- const positional = process.argv.slice(2).filter((a) => !a.startsWith("--"));
370
- const dryRun = process.argv.includes("--dry-run");
368
+ export async function main(argv: readonly string[]): Promise<number> {
369
+ const positional = argv.filter((a) => !a.startsWith("--"));
370
+ const dryRun = argv.includes("--dry-run");
371
371
  const rootDir = resolve(positional[0] ?? process.cwd());
372
372
 
373
373
  const files = findTargetFiles(rootDir);
@@ -406,6 +406,7 @@ async function main(): Promise<void> {
406
406
  }
407
407
  }
408
408
  console.log();
409
+ return 0;
409
410
  }
410
411
 
411
- await main();
412
+ if (import.meta.main) process.exit(await main(process.argv.slice(2)));