@cosmicdrift/kumiko-framework 0.374.0 → 0.377.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.
Files changed (65) hide show
  1. package/AGENTS.md +2 -2
  2. package/agents/examples/apex-landing.md +1 -1
  3. package/agents/examples/apex-surface-auth.md +3 -3
  4. package/agents/examples/compose-stacks.md +15 -8
  5. package/agents/examples/embedded-entity-form.md +1 -1
  6. package/agents/examples/notes-history-basic.md +1 -1
  7. package/agents/examples/tags-basic.md +1 -1
  8. package/agents/examples/user-profile.md +1 -1
  9. package/agents/examples/wizard-form.md +4 -4
  10. package/agents/widgets.md +3 -3
  11. package/dist/api/index.d.ts +1 -1
  12. package/dist/api/index.js +1 -1
  13. package/dist/api/routes.js +2 -2
  14. package/dist/api/server.js +2 -1
  15. package/dist/config/index.d.ts +4 -0
  16. package/dist/db/index.d.ts +2 -2
  17. package/dist/db/index.js +1 -1
  18. package/dist/db/migrate-runner.js +12 -4
  19. package/dist/db/money.js +1 -1
  20. package/dist/db/query.js +14 -14
  21. package/dist/db/replay-migration-sql.d.ts +1 -0
  22. package/dist/db/replay-migration-sql.js +115 -11
  23. package/dist/db/tenant-db.d.ts +7 -0
  24. package/dist/db/tenant-db.js +17 -5
  25. package/dist/engine/boot-validator/screens.js +7 -1
  26. package/dist/engine/index.d.ts +2 -1
  27. package/dist/engine/index.js +1 -0
  28. package/dist/engine/parse-ref-target.js +1 -1
  29. package/dist/engine/provider-plugin-context.d.ts +3 -0
  30. package/dist/engine/provider-plugin-context.js +28 -0
  31. package/dist/engine/schema-builder.js +7 -0
  32. package/dist/engine/types/index.d.ts +1 -1
  33. package/dist/env/index.js +1 -1
  34. package/dist/errors/zod-bridge.js +1 -1
  35. package/dist/files/file-ref-entity.d.ts +6 -6
  36. package/dist/files/file-ref-table.d.ts +6 -6
  37. package/dist/files/index.d.ts +1 -1
  38. package/dist/files/index.js +1 -1
  39. package/dist/files/provider-resolver.d.ts +17 -17
  40. package/dist/files/provider-resolver.js +1 -1
  41. package/dist/http/egress.d.ts +14 -0
  42. package/dist/http/egress.js +144 -0
  43. package/dist/http/index.d.ts +3 -2
  44. package/dist/http/index.js +2 -1
  45. package/dist/http/policy.d.ts +30 -0
  46. package/dist/http/policy.js +299 -0
  47. package/dist/i18n/required-surface-keys.js +2 -0
  48. package/dist/jobs/job-runner.js +4 -2
  49. package/dist/pipeline/config-accessor-factory.d.ts +3 -0
  50. package/dist/pipeline/config-accessor-factory.js +3 -0
  51. package/dist/pipeline/dispatch-shared.js +7 -5
  52. package/dist/pipeline/index.d.ts +1 -0
  53. package/dist/pipeline/index.js +1 -0
  54. package/dist/ui-types/index.d.ts +1 -1
  55. package/dist/upgrade-changelog.d.ts +25 -0
  56. package/dist/upgrade-changelog.js +48 -0
  57. package/dist/upgrade-cli.js +78 -13
  58. package/package.json +3 -5
  59. package/presets/biome-app.json +8 -0
  60. package/src/changes.json +71 -0
  61. package/src/scripts/codemod/README.md +25 -0
  62. package/src/scripts/codemod/codemod-support.ts +64 -6
  63. package/src/scripts/codemod/package-merge.ts +260 -0
  64. package/src/scripts/codemod/s3-imports.ts +4 -42
  65. package/src/scripts/codemod/testing-injection.ts +135 -0
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env bun
2
+ // package-merge — packages folded into another package keep working as shims for one wave and are
3
+ // then removed; this rewrites the consumer side of each move:
4
+ // import/export/import()/mock.module specifiers (incl. subpaths) -> the new specifier
5
+ // CSS @import/@plugin/@config specifiers -> the new specifier
6
+ // package.json dependencies/devDependencies/peerDependencies -> old entry out, new entry in
7
+ // (same version range, same block, skipped when the block already has it)
8
+ // New moves are rows in PACKAGE_MERGE_MAPPINGS; nothing else changes.
9
+ //
10
+ // Usage:
11
+ // bun scripts/codemod/package-merge.ts <targetDir...> [--check | --dry-run | --write] [--no-format]
12
+ // Without a mode flag it writes (kumiko-upgrade --apply calls it that way). --check exits 1
13
+ // while any rewritable site is left; --dry-run only reports.
14
+
15
+ import { Node, Project } from "ts-morph";
16
+ import {
17
+ applyEdits,
18
+ type CodemodResult,
19
+ moduleSpecifierLiterals,
20
+ runCodemodCli,
21
+ type SiteLists,
22
+ type TextEdit,
23
+ } from "./codemod-support.ts";
24
+
25
+ export interface PackageMergeMapping {
26
+ readonly from: string;
27
+ readonly to: string;
28
+ }
29
+
30
+ // A `from` also matches its subpaths: `<from>/x` becomes `<to>/x`. Longer rows win over shorter ones.
31
+ export const PACKAGE_MERGE_MAPPINGS: readonly PackageMergeMapping[] = [
32
+ { from: "@cosmicdrift/kumiko-http", to: "@cosmicdrift/kumiko-framework/http" },
33
+ {
34
+ from: "@cosmicdrift/kumiko-repo-manifest",
35
+ to: "@cosmicdrift/kumiko-guards/repo-manifest",
36
+ },
37
+ { from: "@cosmicdrift/kumiko-headless", to: "@cosmicdrift/kumiko-renderer/headless" },
38
+ { from: "@cosmicdrift/kumiko-dispatcher-live", to: "@cosmicdrift/kumiko-renderer/live" },
39
+ { from: "@cosmicdrift/kumiko-renderer-web", to: "@cosmicdrift/kumiko-renderer/web" },
40
+ ];
41
+
42
+ const DEPENDENCY_BLOCKS = ["dependencies", "devDependencies", "peerDependencies"] as const;
43
+
44
+ const SKIPPED_PATH_SUFFIXES: readonly string[] = [
45
+ "packages/http/src/index.ts",
46
+ "packages/repo-manifest/src/index.ts",
47
+ "packages/repo-manifest/src/app-ports/index.ts",
48
+ "packages/headless/src/index.ts",
49
+ "packages/headless/src/apex/index.ts",
50
+ "packages/headless/src/dispatcher/index.ts",
51
+ "packages/headless/src/locale-routing/index.ts",
52
+ "packages/dispatcher-live/src/index.ts",
53
+ "packages/renderer-web/src/index.ts",
54
+ "packages/renderer-web/src/theme-plugin.ts",
55
+ "packages/renderer-web/src/styles.css",
56
+ "packages/framework/src/scripts/codemod/package-merge.ts",
57
+ "packages/framework/src/__tests__/package-merge-codemod.test.ts",
58
+ ];
59
+
60
+ function packageNameOf(specifier: string): string {
61
+ const [scope, name] = specifier.split("/");
62
+ return specifier.startsWith("@") ? `${scope}/${name}` : (scope ?? specifier);
63
+ }
64
+
65
+ function mappingsLongestFirst(mappings: readonly PackageMergeMapping[]): PackageMergeMapping[] {
66
+ return [...mappings].sort((a, b) => b.from.length - a.from.length);
67
+ }
68
+
69
+ export function mergedSpecifier(
70
+ specifier: string,
71
+ mappings: readonly PackageMergeMapping[] = PACKAGE_MERGE_MAPPINGS,
72
+ ): string | undefined {
73
+ for (const { from, to } of mappingsLongestFirst(mappings)) {
74
+ if (specifier === from) return to;
75
+ if (specifier.startsWith(`${from}/`)) return `${to}${specifier.slice(from.length)}`;
76
+ }
77
+ return undefined;
78
+ }
79
+
80
+ function movedPackages(mappings: readonly PackageMergeMapping[]): Map<string, string> {
81
+ const moved = new Map<string, string>();
82
+ for (const { from, to } of mappings) {
83
+ if (packageNameOf(from) === from) moved.set(from, packageNameOf(to));
84
+ }
85
+ return moved;
86
+ }
87
+
88
+ function transformTypeScriptSource(
89
+ source: string,
90
+ filePath: string,
91
+ mappings: readonly PackageMergeMapping[],
92
+ result: SiteLists,
93
+ ): string {
94
+ if (!mappings.some(({ from }) => source.includes(from))) return source;
95
+ const project = new Project({
96
+ useInMemoryFileSystem: true,
97
+ skipAddingFilesFromTsConfig: true,
98
+ skipFileDependencyResolution: true,
99
+ });
100
+ const sourceFile = project.createSourceFile(filePath, source);
101
+ const edits: TextEdit[] = [];
102
+ for (const literal of moduleSpecifierLiterals(sourceFile)) {
103
+ if (!Node.isStringLiteral(literal)) continue;
104
+ const current = literal.getLiteralValue();
105
+ const next = mergedSpecifier(current, mappings);
106
+ if (next === undefined) continue;
107
+ result.rewrites.push({
108
+ file: filePath,
109
+ line: literal.getStartLineNumber(),
110
+ description: `${current} -> ${next}`,
111
+ });
112
+ edits.push({ start: literal.getStart(), end: literal.getEnd(), text: JSON.stringify(next) });
113
+ }
114
+ return applyEdits(source, edits);
115
+ }
116
+
117
+ const CSS_AT_RULE_STRING = /@(import|plugin|config|source)(\s+(?:url\(\s*)?)(["'])([^"'\n]+)\3/g;
118
+
119
+ // A stylesheet specifier is rewritten like an import. A path into the package's files (`@source
120
+ // ".../node_modules/<from>/src/..."`) has no mechanical target, so it is reported as manual.
121
+ function transformCssSource(
122
+ source: string,
123
+ filePath: string,
124
+ mappings: readonly PackageMergeMapping[],
125
+ result: SiteLists,
126
+ ): string {
127
+ if (!mappings.some(({ from }) => source.includes(from))) return source;
128
+ const lineOf = (offset: number): number => source.slice(0, offset).split("\n").length;
129
+ return source.replace(
130
+ CSS_AT_RULE_STRING,
131
+ (match, rule: string, between: string, quote: string, value: string, offset: number) => {
132
+ const next = rule === "source" ? undefined : mergedSpecifier(value, mappings);
133
+ if (next !== undefined) {
134
+ result.rewrites.push({
135
+ file: filePath,
136
+ line: lineOf(offset),
137
+ description: `${value} -> ${next}`,
138
+ });
139
+ return `@${rule}${between}${quote}${next}${quote}`;
140
+ }
141
+ if (mappings.some(({ from }) => value.includes(`/${from}/`))) {
142
+ result.manual.push({
143
+ file: filePath,
144
+ line: lineOf(offset),
145
+ description: `@${rule} path into a merged package: ${value}`,
146
+ });
147
+ }
148
+ return match;
149
+ },
150
+ );
151
+ }
152
+
153
+ function detectIndent(source: string): string {
154
+ return /^([ \t]+)"/m.exec(source)?.[1] ?? " ";
155
+ }
156
+
157
+ function isStringRecord(value: unknown): value is Record<string, string> {
158
+ return (
159
+ typeof value === "object" &&
160
+ value !== null &&
161
+ !Array.isArray(value) &&
162
+ Object.values(value).every((entry) => typeof entry === "string")
163
+ );
164
+ }
165
+
166
+ function isSortedKeys(record: Record<string, string>): boolean {
167
+ const keys = Object.keys(record);
168
+ return keys.every((key, index) => index === 0 || (keys[index - 1] ?? "") <= key);
169
+ }
170
+
171
+ function mergeDependencyBlock(
172
+ block: Record<string, string>,
173
+ moved: ReadonlyMap<string, string>,
174
+ ownName: unknown,
175
+ describe: (description: string) => void,
176
+ ): Record<string, string> | undefined {
177
+ const next: Record<string, string> = {};
178
+ let changed = false;
179
+ for (const [name, range] of Object.entries(block)) {
180
+ const target = moved.get(name);
181
+ if (target === undefined) {
182
+ next[name] = range;
183
+ continue;
184
+ }
185
+ changed = true;
186
+ describe(`${name} -> ${target}`);
187
+ if (target !== ownName && !(target in block)) next[target] = range;
188
+ }
189
+ if (!changed) return undefined;
190
+ return isSortedKeys(block)
191
+ ? Object.fromEntries(Object.entries(next).sort(([a], [b]) => a.localeCompare(b)))
192
+ : next;
193
+ }
194
+
195
+ function transformPackageJson(
196
+ source: string,
197
+ filePath: string,
198
+ mappings: readonly PackageMergeMapping[],
199
+ result: SiteLists,
200
+ ): string {
201
+ let manifest: unknown;
202
+ try {
203
+ manifest = JSON.parse(source);
204
+ } catch {
205
+ result.manual.push({ file: filePath, line: 1, description: "package.json is not valid JSON" });
206
+ return source;
207
+ }
208
+ if (typeof manifest !== "object" || manifest === null || Array.isArray(manifest)) return source;
209
+ const moved = movedPackages(mappings);
210
+ const record: Record<string, unknown> = { ...manifest };
211
+ let changed = false;
212
+ for (const blockName of DEPENDENCY_BLOCKS) {
213
+ const block = record[blockName];
214
+ if (!isStringRecord(block)) continue;
215
+ const merged = mergeDependencyBlock(block, moved, record["name"], (description) => {
216
+ result.rewrites.push({
217
+ file: filePath,
218
+ line: 1,
219
+ description: `${blockName}: ${description}`,
220
+ });
221
+ });
222
+ if (merged === undefined) continue;
223
+ record[blockName] = merged;
224
+ changed = true;
225
+ }
226
+ if (!changed) return source;
227
+ const trailingNewline = source.endsWith("\n") ? "\n" : "";
228
+ return `${JSON.stringify(record, null, detectIndent(source))}${trailingNewline}`;
229
+ }
230
+
231
+ export function transformPackageMergeSource(
232
+ source: string,
233
+ filePath = "source.ts",
234
+ mappings: readonly PackageMergeMapping[] = PACKAGE_MERGE_MAPPINGS,
235
+ ): CodemodResult {
236
+ const result: SiteLists = { rewrites: [], manual: [] };
237
+ const output = filePath.endsWith("package.json")
238
+ ? transformPackageJson(source, filePath, mappings, result)
239
+ : filePath.endsWith(".css")
240
+ ? transformCssSource(source, filePath, mappings, result)
241
+ : transformTypeScriptSource(source, filePath, mappings, result);
242
+ return { output, ...result };
243
+ }
244
+
245
+ function isSkippedPath(file: string): boolean {
246
+ return SKIPPED_PATH_SUFFIXES.some((suffix) => file.endsWith(suffix));
247
+ }
248
+
249
+ if (import.meta.main) {
250
+ await runCodemodCli(
251
+ {
252
+ name: "package-merge",
253
+ transform: (source, filePath) => transformPackageMergeSource(source, filePath),
254
+ isSkippedPath,
255
+ extraFileNames: ["package.json"],
256
+ extraExtensions: [".css"],
257
+ },
258
+ process.argv.slice(2),
259
+ );
260
+ }
@@ -10,17 +10,20 @@
10
10
  // Without a mode flag it writes (kumiko-upgrade --apply calls it that way). --check exits 1
11
11
  // while any rewritable or manual site is left; --dry-run only reports.
12
12
 
13
- import { type ImportDeclaration, Node, Project, type SourceFile, SyntaxKind } from "ts-morph";
13
+ import { type ImportDeclaration, Node, Project, type SourceFile } from "ts-morph";
14
14
  import {
15
+ applyEdits,
15
16
  type CodemodResult,
16
17
  type CodemodSite,
17
18
  ensureNamedImport,
18
19
  hasImportBinding,
20
+ moduleSpecifierLiterals,
19
21
  relativeSpecifier,
20
22
  removeNamedImport,
21
23
  runCodemodCli,
22
24
  type SiteLists,
23
25
  sourceRelativeModule,
26
+ type TextEdit,
24
27
  } from "./codemod-support.ts";
25
28
 
26
29
  const BUNDLED_PACKAGE = "@cosmicdrift/kumiko-bundled-features";
@@ -79,47 +82,6 @@ function site(file: string, node: Node, description: string): CodemodSite {
79
82
  return { file, line: node.getStartLineNumber(), description };
80
83
  }
81
84
 
82
- function stringLiteralOf(node: Node | undefined): Node | undefined {
83
- return node !== undefined && Node.isStringLiteral(node) ? node : undefined;
84
- }
85
-
86
- interface TextEdit {
87
- readonly start: number;
88
- readonly end: number;
89
- readonly text: string;
90
- }
91
-
92
- function applyEdits(source: string, edits: readonly TextEdit[]): string {
93
- let output = source;
94
- for (const edit of [...edits].sort((x, y) => y.start - x.start)) {
95
- output = output.slice(0, edit.start) + edit.text + output.slice(edit.end);
96
- }
97
- return output;
98
- }
99
-
100
- function moduleSpecifierLiterals(sourceFile: SourceFile): Node[] {
101
- const literals: Node[] = [];
102
- for (const declaration of [
103
- ...sourceFile.getImportDeclarations(),
104
- ...sourceFile.getExportDeclarations(),
105
- ]) {
106
- const literal = declaration.getModuleSpecifier();
107
- if (literal !== undefined) literals.push(literal);
108
- }
109
- for (const importType of sourceFile.getDescendantsOfKind(SyntaxKind.ImportType)) {
110
- const argument = importType.getArgument();
111
- if (Node.isLiteralTypeNode(argument)) literals.push(argument.getLiteral());
112
- }
113
- for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
114
- const callee = call.getExpression();
115
- const isModuleLoad =
116
- callee.getKind() === SyntaxKind.ImportKeyword || callee.getText() === "mock.module";
117
- const literal = stringLiteralOf(call.getArguments()[0]);
118
- if (isModuleLoad && literal !== undefined) literals.push(literal);
119
- }
120
- return literals;
121
- }
122
-
123
85
  function rewriteLibSpecifiers(
124
86
  sourceFile: SourceFile,
125
87
  source: string,
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env bun
2
+ // testing-injection — kumiko-testing no longer imports bundled-features or dev-server. The test
3
+ // functions that need them are bound in `@cosmicdrift/kumiko-dev-server/testing`:
4
+ // @cosmicdrift/kumiko-testing { setupAppTestStack, SetupAppTestStackOptions,
5
+ // seedTenant, persistTenantRows }
6
+ // @cosmicdrift/kumiko-testing/e2e/seed-route { createE2eSeedRoutes }
7
+ // -> @cosmicdrift/kumiko-dev-server/testing
8
+ // Names that stay (types like SeededTenant, E2eExtraSeeder, isE2eSeedingEnabled) keep their import.
9
+ //
10
+ // Usage:
11
+ // bun scripts/codemod/testing-injection.ts <targetDir...> [--check | --dry-run | --write] [--no-format]
12
+ // Without a mode flag it writes (kumiko-upgrade --apply calls it that way). --check exits 1
13
+ // while any rewritable or manual site is left; --dry-run only reports.
14
+
15
+ import { type ImportDeclaration, Project, SyntaxKind } from "ts-morph";
16
+ import {
17
+ type CodemodResult,
18
+ ensureNamedImport,
19
+ runCodemodCli,
20
+ type SiteLists,
21
+ } from "./codemod-support.ts";
22
+
23
+ const TESTING_PACKAGE = "@cosmicdrift/kumiko-testing";
24
+ const TARGET_MODULE = "@cosmicdrift/kumiko-dev-server/testing";
25
+
26
+ const MOVED_NAMES_BY_MODULE: ReadonlyMap<string, ReadonlySet<string>> = new Map([
27
+ [
28
+ TESTING_PACKAGE,
29
+ new Set(["setupAppTestStack", "SetupAppTestStackOptions", "seedTenant", "persistTenantRows"]),
30
+ ],
31
+ [`${TESTING_PACKAGE}/e2e/seed-route`, new Set(["createE2eSeedRoutes"])],
32
+ ]);
33
+
34
+ const CHEAP_PREFILTER = /kumiko-testing/;
35
+ const SKIPPED_PATH_SUFFIXES: readonly string[] = [
36
+ "packages/framework/src/scripts/codemod/testing-injection.ts",
37
+ "packages/framework/src/__tests__/testing-injection-codemod.test.ts",
38
+ "packages/dev-server/src/testing.ts",
39
+ ];
40
+
41
+ function movedNamesOf(declaration: ImportDeclaration): ReadonlySet<string> | undefined {
42
+ return MOVED_NAMES_BY_MODULE.get(declaration.getModuleSpecifierValue());
43
+ }
44
+
45
+ function reportNamespaceUses(
46
+ declaration: ImportDeclaration,
47
+ moved: ReadonlySet<string>,
48
+ filePath: string,
49
+ result: SiteLists,
50
+ ): void {
51
+ const namespaceName = declaration.getNamespaceImport()?.getText();
52
+ const accesses =
53
+ namespaceName === undefined
54
+ ? []
55
+ : declaration.getSourceFile().getDescendantsOfKind(SyntaxKind.PropertyAccessExpression);
56
+ for (const access of accesses) {
57
+ if (access.getExpression().getText() === namespaceName && moved.has(access.getName())) {
58
+ result.manual.push({
59
+ file: filePath,
60
+ line: access.getStartLineNumber(),
61
+ description: `${namespaceName}.${access.getName()}: import it from ${TARGET_MODULE}`,
62
+ });
63
+ }
64
+ }
65
+ }
66
+
67
+ export function transformTestingInjectionSource(
68
+ source: string,
69
+ filePath = "source.ts",
70
+ ): CodemodResult {
71
+ const result: SiteLists = { rewrites: [], manual: [] };
72
+ if (!CHEAP_PREFILTER.test(source)) return { output: source, ...result };
73
+ const project = new Project({
74
+ useInMemoryFileSystem: true,
75
+ skipAddingFilesFromTsConfig: true,
76
+ skipFileDependencyResolution: true,
77
+ });
78
+ const sourceFile = project.createSourceFile(filePath, source);
79
+ for (const declaration of sourceFile.getImportDeclarations()) {
80
+ const moved = movedNamesOf(declaration);
81
+ if (moved === undefined) continue;
82
+ reportNamespaceUses(declaration, moved, filePath, result);
83
+ const movedImports = declaration
84
+ .getNamedImports()
85
+ .filter((named) => moved.has(named.getName()));
86
+ if (movedImports.length === 0) continue;
87
+ const line = declaration.getStartLineNumber();
88
+ const typeOnlyDeclaration = declaration.isTypeOnly();
89
+ const names = movedImports.map((named) => named.getName());
90
+ // Collected first: removing a specifier invalidates the nodes of its siblings.
91
+ const snapshots = movedImports.map((named) => ({
92
+ name: named.getName(),
93
+ alias: named.getAliasNode()?.getText(),
94
+ typeOnly: named.isTypeOnly(),
95
+ }));
96
+ for (const named of movedImports) named.remove();
97
+ if (
98
+ declaration.getNamedImports().length === 0 &&
99
+ declaration.getNamespaceImport() === undefined &&
100
+ declaration.getDefaultImport() === undefined
101
+ ) {
102
+ declaration.remove();
103
+ }
104
+ for (const snapshot of snapshots) {
105
+ const typeOnly = typeOnlyDeclaration || snapshot.typeOnly;
106
+ if (snapshot.alias === undefined) {
107
+ ensureNamedImport(sourceFile, TARGET_MODULE, snapshot.name, typeOnly);
108
+ } else {
109
+ sourceFile.addImportDeclaration({
110
+ moduleSpecifier: TARGET_MODULE,
111
+ namedImports: [{ name: snapshot.name, alias: snapshot.alias }],
112
+ isTypeOnly: typeOnly,
113
+ });
114
+ }
115
+ }
116
+ result.rewrites.push({
117
+ file: filePath,
118
+ line,
119
+ description: `${names.join(", ")} -> ${TARGET_MODULE}`,
120
+ });
121
+ }
122
+ const output = result.rewrites.length > 0 ? sourceFile.getFullText() : source;
123
+ return { output, ...result };
124
+ }
125
+
126
+ function isSkippedPath(file: string): boolean {
127
+ return SKIPPED_PATH_SUFFIXES.some((suffix) => file.endsWith(suffix));
128
+ }
129
+
130
+ if (import.meta.main) {
131
+ await runCodemodCli(
132
+ { name: "testing-injection", transform: transformTestingInjectionSource, isSkippedPath },
133
+ process.argv.slice(2),
134
+ );
135
+ }