@cosmicdrift/kumiko-feature-ast 0.390.0 → 0.391.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-feature-ast",
3
- "version": "0.390.0",
3
+ "version": "0.391.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,10 +22,10 @@
22
22
  }
23
23
  },
24
24
  "dependencies": {
25
- "@cosmicdrift/kumiko-framework": "0.390.0",
26
- "@cosmicdrift/kumiko-types": "0.390.0",
25
+ "@cosmicdrift/kumiko-framework": "0.391.0",
26
+ "@cosmicdrift/kumiko-types": "0.391.0",
27
27
  "ts-morph": "^28.0.0",
28
- "zod": "^4.4.3"
28
+ "zod": "^4.6.7"
29
29
  },
30
30
  "repository": {
31
31
  "type": "git",
package/src/changes.json CHANGED
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "version": "0.391.0",
4
+ "type": "fix",
5
+ "title": "Codemods no longer fail when git is not installed",
6
+ "detail": "collectSourceFiles asks git check-ignore which files the repository's .gitignore excludes. Without a git executable Bun.spawnSync threw ENOENT and kumiko-upgrade --apply aborted at the first codemod, for example ctx-namespaces in the customer-app builder image. A missing git is now treated like a directory outside a repository: no file is filtered."
7
+ },
8
+ {
9
+ "version": "0.390.1",
10
+ "type": "fix",
11
+ "title": "Codemods skip gitignored files, ctx-namespaces finds structural helpers across files",
12
+ "detail": "collectSourceFiles, used by the codemods, leaves out files the repository's .gitignore excludes, such as generated output and local worktrees. The ctx-namespaces codemod loads all files of a tsconfig project before rewriting, so a helper with a structural `{ query }` context type is migrated even when its handler caller sits in a file processed later."
13
+ },
2
14
  {
3
15
  "version": "0.390.0",
4
16
  "type": "fix",
@@ -1,5 +1,6 @@
1
1
  import { lstatSync, readdirSync, statSync } from "node:fs";
2
- import { join, resolve } from "node:path";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { errorCodeOf } from "@cosmicdrift/kumiko-types/type-guards";
3
4
 
4
5
  export const BUILD_AND_VENDOR_DIR_NAMES: readonly string[] = [
5
6
  "node_modules",
@@ -41,8 +42,53 @@ export function collectSourceFiles(
41
42
  const files: string[] = [];
42
43
  for (const p of paths) {
43
44
  const abs = resolve(p);
44
- if (statSync(abs).isDirectory()) walkDir(abs, files);
45
- else if (options.isMigratableFile(abs)) files.push(abs);
45
+ const found: string[] = [];
46
+ const isDirectory = statSync(abs).isDirectory();
47
+ if (isDirectory) walkDir(abs, found);
48
+ else if (options.isMigratableFile(abs)) found.push(abs);
49
+ files.push(...withoutGitIgnored(isDirectory ? abs : dirname(abs), found));
46
50
  }
47
51
  return files;
48
52
  }
53
+
54
+ // git check-ignore exits 0 when it printed ignored paths, 1 when none is ignored and 128 outside a
55
+ // repository; only 0 filters anything.
56
+ const GIT_CHECK_IGNORE_SOME_IGNORED = 0;
57
+
58
+ // A git hook exports GIT_DIR and friends, and git prefers them over cwd: inherited, check-ignore
59
+ // would ask the hook's repository about paths outside it.
60
+ export function envWithoutGitRepositoryOverrides(): Record<string, string> {
61
+ return Object.fromEntries(
62
+ Object.entries(process.env).filter(
63
+ (entry): entry is [string, string] => !entry[0].startsWith("GIT_") && entry[1] !== undefined,
64
+ ),
65
+ );
66
+ }
67
+
68
+ // Generated output, worktrees and local scratch dirs are usually only excluded through .gitignore;
69
+ // a codemod must not rewrite them.
70
+ function withoutGitIgnored(cwd: string, files: readonly string[]): readonly string[] {
71
+ if (files.length === 0) return files;
72
+ const result = runGitCheckIgnore(cwd, files);
73
+ if (result === undefined || result.exitCode !== GIT_CHECK_IGNORE_SOME_IGNORED) return files;
74
+ const ignored = new Set(result.stdout.toString().split("\0"));
75
+ return files.filter((file) => !ignored.has(file));
76
+ }
77
+
78
+ function runGitCheckIgnore(cwd: string, files: readonly string[]) {
79
+ try {
80
+ return Bun.spawnSync({
81
+ cmd: ["git", "check-ignore", "--stdin", "-z"],
82
+ cwd,
83
+ env: envWithoutGitRepositoryOverrides(),
84
+ stdin: Buffer.from(`${files.join("\0")}\0`),
85
+ stdout: "pipe",
86
+ stderr: "pipe",
87
+ });
88
+ } catch (error) {
89
+ // Build images such as the customer-app builder ship without git; like outside a repository,
90
+ // there is no .gitignore to honour then.
91
+ if (errorCodeOf(error) === "ENOENT") return undefined;
92
+ throw error;
93
+ }
94
+ }
@@ -106,8 +106,10 @@ function runGroup(
106
106
  skipAddingFilesFromTsConfig: true,
107
107
  });
108
108
  totals.projects += 1;
109
- for (const file of files) {
110
- const sourceFile = project.addSourceFileAtPath(file);
109
+ // Receivers are found through references: a structural helper is only recognised as taking a
110
+ // handler context when its callers in other files are already part of the project.
111
+ const staged = files.map((file) => ({ file, sourceFile: project.addSourceFileAtPath(file) }));
112
+ for (const { file, sourceFile } of staged) {
111
113
  const before = readFileSync(file, "utf8");
112
114
  const result = transformCtxNamespaces(sourceFile);
113
115
  reportSites(result.rewrites, "rewritten");