@cosmicdrift/kumiko-feature-ast 0.391.0 → 0.392.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.
- package/package.json +3 -3
- package/src/changes.json +12 -0
- package/src/scripts/codemod/codemod-support.ts +17 -5
- package/src/scripts/codemod/ctx-namespaces.ts +6 -11
- package/src/scripts/codemod/feature-layout.ts +4 -4
- package/src/scripts/codemod/object-form.ts +9 -6
- package/src/scripts/codemod/package-merge.ts +10 -10
- package/src/scripts/codemod/remove-engine-payload-casts.ts +3 -2
- package/src/scripts/codemod/testing-move.ts +9 -6
- package/src/scripts/codemod/type-aware-project.ts +47 -0
- package/src/scripts/codemod/unprocessable-error-details-reason.ts +5 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-feature-ast",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.392.0",
|
|
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.
|
|
26
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
25
|
+
"@cosmicdrift/kumiko-framework": "0.392.0",
|
|
26
|
+
"@cosmicdrift/kumiko-types": "0.392.0",
|
|
27
27
|
"ts-morph": "^28.0.0",
|
|
28
28
|
"zod": "^4.6.7"
|
|
29
29
|
},
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
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
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.391.1",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "Type-aware codemods no longer exhaust memory when the app pins another zod",
|
|
12
|
+
"detail": "ctx-namespaces and remove-engine-payload-casts build their ts-morph project with a resolution host that resolves zod and its subpaths from the app root, so the program holds one zod copy even when bun nests a second version under each framework package. Without zod at the app root, resolution is unchanged. A small app with zod 4.6.6 next to framework packages on 4.6.7 went from 17 GB and 36 s to 1.1 GB and 3 s for ctx-namespaces, with the same rewrites."
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.391.0",
|
|
4
16
|
"type": "fix",
|
|
@@ -286,13 +286,16 @@ export interface CodemodCliConfig {
|
|
|
286
286
|
readonly extraExtensions?: readonly string[];
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
-
function
|
|
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
|
-
|
|
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<
|
|
333
|
-
|
|
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
|
-
|
|
363
|
+
return options.mode === "check" && rewrites.length + manual.length > 0 ? 1 : 0;
|
|
352
364
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join, relative, resolve } from "node:path";
|
|
3
|
-
import { Project } from "ts-morph";
|
|
4
3
|
import { type CodemodSite, formatWithBiome } from "./codemod-support.ts";
|
|
5
4
|
import { collectSourceFiles } from "./collect-source-files.ts";
|
|
6
5
|
import { transformCtxNamespaces } from "./ctx-namespaces-transform.ts";
|
|
6
|
+
import { createTypeAwareProject } from "./type-aware-project.ts";
|
|
7
7
|
|
|
8
8
|
const NAME = "ctx-namespaces";
|
|
9
9
|
|
|
@@ -101,10 +101,7 @@ function runGroup(
|
|
|
101
101
|
mode: Mode,
|
|
102
102
|
totals: RunTotals,
|
|
103
103
|
): void {
|
|
104
|
-
const project =
|
|
105
|
-
...(tsconfig === undefined ? {} : { tsConfigFilePath: tsconfig }),
|
|
106
|
-
skipAddingFilesFromTsConfig: true,
|
|
107
|
-
});
|
|
104
|
+
const project = createTypeAwareProject(tsconfig);
|
|
108
105
|
totals.projects += 1;
|
|
109
106
|
// Receivers are found through references: a structural helper is only recognised as taking a
|
|
110
107
|
// handler context when its callers in other files are already part of the project.
|
|
@@ -127,14 +124,14 @@ function runGroup(
|
|
|
127
124
|
|
|
128
125
|
// kumiko-upgrade runs this as `bun ctx-namespaces.ts <repoDir> [--dry-run]` and treats a non-zero
|
|
129
126
|
// exit as a failed codemod, so manual sites alone must not fail write or dry-run.
|
|
130
|
-
async function main(argv: readonly string[]): Promise<
|
|
127
|
+
export async function main(argv: readonly string[]): Promise<number> {
|
|
131
128
|
let options: CliOptions;
|
|
132
129
|
try {
|
|
133
130
|
options = parseArgs(argv);
|
|
134
131
|
} catch (error) {
|
|
135
132
|
if (error instanceof CliError) {
|
|
136
133
|
console.error(`[${NAME}] ${error.message}`);
|
|
137
|
-
|
|
134
|
+
return 2;
|
|
138
135
|
}
|
|
139
136
|
throw error;
|
|
140
137
|
}
|
|
@@ -160,11 +157,9 @@ async function main(argv: readonly string[]): Promise<void> {
|
|
|
160
157
|
if (options.mode === "write" && options.format && totals.changedFiles.length > 0) {
|
|
161
158
|
await formatWithBiome(NAME, totals.changedFiles, resolve(options.paths[0] ?? "."));
|
|
162
159
|
}
|
|
163
|
-
|
|
164
|
-
options.mode === "check" && totals.rewrites.length + totals.manual.length > 0 ? 1 : 0,
|
|
165
|
-
);
|
|
160
|
+
return options.mode === "check" && totals.rewrites.length + totals.manual.length > 0 ? 1 : 0;
|
|
166
161
|
}
|
|
167
162
|
|
|
168
163
|
if (import.meta.main) {
|
|
169
|
-
await main(process.argv.slice(2));
|
|
164
|
+
process.exit(await main(process.argv.slice(2)));
|
|
170
165
|
}
|
|
@@ -389,13 +389,13 @@ function writeResult(result: FeatureLayoutResult): void {
|
|
|
389
389
|
}
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
-
async function main(argv: readonly string[]): Promise<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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<
|
|
726
|
-
const options =
|
|
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
|
-
|
|
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
|
}
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
// program, so the framework version the app compiles against decides the row type.
|
|
14
14
|
|
|
15
15
|
import { dirname, resolve } from "node:path";
|
|
16
|
-
import { type AsExpression, Node, Project, type SourceFile, SyntaxKind, ts } from "ts-morph";
|
|
16
|
+
import { type AsExpression, Node, type Project, type SourceFile, SyntaxKind, ts } from "ts-morph";
|
|
17
17
|
import { type CodemodResult, runCodemodCli } from "./codemod-support.ts";
|
|
18
|
+
import { createTypeAwareProject } from "./type-aware-project.ts";
|
|
18
19
|
|
|
19
20
|
const CAST_MARKER = "@cast-boundary";
|
|
20
21
|
const EXECUTOR_TYPES_FILE = "/event-store-executor-types.ts";
|
|
@@ -278,7 +279,7 @@ function projectFor(filePath: string): Project | undefined {
|
|
|
278
279
|
if (tsconfig === undefined) return undefined;
|
|
279
280
|
const cached = projectsByTsconfig.get(tsconfig);
|
|
280
281
|
if (cached !== undefined) return cached;
|
|
281
|
-
const project =
|
|
282
|
+
const project = createTypeAwareProject(tsconfig);
|
|
282
283
|
projectsByTsconfig.set(tsconfig, project);
|
|
283
284
|
return project;
|
|
284
285
|
}
|
|
@@ -506,20 +506,23 @@ function transformFiles(files: readonly string[], mode: Mode): RunSummary {
|
|
|
506
506
|
return summary;
|
|
507
507
|
}
|
|
508
508
|
|
|
509
|
-
function
|
|
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
|
-
|
|
517
|
+
return { exitCode: 2 };
|
|
516
518
|
}
|
|
517
519
|
throw err;
|
|
518
520
|
}
|
|
519
521
|
}
|
|
520
522
|
|
|
521
|
-
async function main(): Promise<
|
|
522
|
-
const options =
|
|
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
|
-
|
|
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
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { dirname, join, resolve } from "node:path";
|
|
2
|
+
import { Project, type ResolutionHostFactory, ts } from "ts-morph";
|
|
3
|
+
|
|
4
|
+
function isZodSpecifier(moduleName: string): boolean {
|
|
5
|
+
return moduleName === "zod" || moduleName.startsWith("zod/");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// A second zod nested under a package makes the checker compare schemas structurally across both
|
|
9
|
+
// copies at every framework call, which exhausts memory; one copy keeps them nominal.
|
|
10
|
+
function appRootZodResolution(appRoot: string): ResolutionHostFactory {
|
|
11
|
+
const appRootAnchor = join(appRoot, "codemod-resolution-anchor.ts");
|
|
12
|
+
return (moduleResolutionHost, getCompilerOptions) => {
|
|
13
|
+
const cache = ts.createModuleResolutionCache(
|
|
14
|
+
appRoot,
|
|
15
|
+
(fileName) => fileName,
|
|
16
|
+
getCompilerOptions(),
|
|
17
|
+
);
|
|
18
|
+
const resolveFrom = (moduleName: string, containingFile: string, options: ts.CompilerOptions) =>
|
|
19
|
+
ts.resolveModuleName(moduleName, containingFile, options, moduleResolutionHost, cache)
|
|
20
|
+
.resolvedModule;
|
|
21
|
+
return {
|
|
22
|
+
resolveModuleNames: (
|
|
23
|
+
moduleNames,
|
|
24
|
+
containingFile,
|
|
25
|
+
_reusedNames,
|
|
26
|
+
_redirectedReference,
|
|
27
|
+
options,
|
|
28
|
+
) =>
|
|
29
|
+
moduleNames.map(
|
|
30
|
+
(moduleName) =>
|
|
31
|
+
(isZodSpecifier(moduleName)
|
|
32
|
+
? resolveFrom(moduleName, appRootAnchor, options)
|
|
33
|
+
: undefined) ?? resolveFrom(moduleName, containingFile, options),
|
|
34
|
+
),
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createTypeAwareProject(tsconfig: string | undefined): Project {
|
|
40
|
+
if (tsconfig === undefined) return new Project({ skipAddingFilesFromTsConfig: true });
|
|
41
|
+
const tsConfigFilePath = resolve(tsconfig);
|
|
42
|
+
return new Project({
|
|
43
|
+
tsConfigFilePath,
|
|
44
|
+
skipAddingFilesFromTsConfig: true,
|
|
45
|
+
resolutionHost: appRootZodResolution(dirname(tsConfigFilePath)),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -365,9 +365,9 @@ function migrateFile(sourceFile: SourceFile, skips: SkipEntry[]): number {
|
|
|
365
365
|
return removedCount;
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
-
async function main(): Promise<
|
|
369
|
-
const positional =
|
|
370
|
-
const dryRun =
|
|
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)));
|