@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
@@ -4,6 +4,7 @@ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { getFlag, getStringFlag, parseArgs } from "./arg-parser.js";
5
5
  import { compareVersions, filterEntriesAfter, parseFeatureChangelog, sortEntries, } from "./engine/index.js";
6
6
  import { ensureTemporalPolyfill } from "./time/index.js";
7
+ import { mergeUpgradeSection } from "./upgrade-changelog.js";
7
8
  const SEMVER_RE = /^\d+\.\d+\.\d+$/;
8
9
  const SEMVER_CORE_RE = /^(\d+\.\d+\.\d+)(?:-[0-9A-Za-z.-]+)?$/;
9
10
  const CODEMOD_SUBDIR = "scripts/codemod";
@@ -340,12 +341,24 @@ function carryOverManual(previous, newManual) {
340
341
  ...(previous.resolvedManual.length > 0 && { resolvedManual: previous.resolvedManual }),
341
342
  };
342
343
  }
344
+ // The marker records every codemod ever applied, not just the last bump's, so the
345
+ // CI guard and a resumed run see the whole history.
346
+ function accumulateCodemods(previous, ran) {
347
+ const seen = new Set();
348
+ return [...previous, ...ran].filter((entry) => {
349
+ const key = `${entry.version}\0${entry.codemod}`;
350
+ if (seen.has(key))
351
+ return false;
352
+ seen.add(key);
353
+ return true;
354
+ });
355
+ }
343
356
  function writeUpgradeMarker(targetDir, marker) {
344
357
  const dir = join(targetDir, ".kumiko");
345
358
  mkdirSync(dir, { recursive: true });
346
359
  writeFileSync(join(dir, "upgrade-state.json"), `${JSON.stringify(marker, null, 2)}\n`, "utf-8");
347
360
  }
348
- const NO_CARRY = { pendingManual: [], resolvedManual: [] };
361
+ const NO_CARRY = { codemods: [], pendingManual: [], resolvedManual: [] };
349
362
  function isRecord(value) {
350
363
  return typeof value === "object" && value !== null && !Array.isArray(value);
351
364
  }
@@ -427,16 +440,52 @@ function carryOf(read) {
427
440
  function printable(text) {
428
441
  return text.replace(/\p{Cc}/gu, "");
429
442
  }
430
- function logOpenManual(out, open) {
443
+ function manualDetailsOf(entries) {
444
+ return new Map(entries.map((entry) => [
445
+ manualEntryId(entry.version, entry.title),
446
+ { detail: entry.detail, migration: entry.migration },
447
+ ]));
448
+ }
449
+ function logOpenManual(out, open, details) {
431
450
  // skip: no open manual migrations, nothing to report
432
451
  if (open.length === 0)
433
452
  return;
434
453
  out.log(` ⚠ ${open.length} manual migration(s) still open:`);
435
454
  for (const entry of open) {
436
455
  out.log(` ${printable(entry.id)} · ${printable(entry.version)} · ${printable(entry.title)}`);
456
+ const entryDetails = details?.get(entry.id);
457
+ if (entryDetails?.detail)
458
+ out.log(` ${printable(entryDetails.detail.replace(/\s*\n\s*/g, " "))}`);
459
+ if (entryDetails?.migration)
460
+ out.log(` → Migration: ${printable(entryDetails.migration)}`);
437
461
  }
438
462
  out.log(' Migrate them by hand, then mark them done: kumiko-upgrade --resolve <id|version> --reason "<what you did>" [--not-applicable]');
439
463
  }
464
+ function manualWithStatus(manualEntries, previous) {
465
+ return manualEntries.map((entry) => {
466
+ const id = manualEntryId(entry.version, entry.title);
467
+ const resolved = previous.resolvedManual.find((candidate) => candidate.id === id);
468
+ return {
469
+ id,
470
+ version: entry.version,
471
+ title: entry.title,
472
+ status: resolved === undefined ? "open" : resolved.resolution,
473
+ };
474
+ });
475
+ }
476
+ // Runs only for a real bump: an app that is already current keeps its CHANGELOG.md untouched.
477
+ function writeUpgradeChangelog(targetDir, bump) {
478
+ const path = join(targetDir, "CHANGELOG.md");
479
+ const existing = existsSync(path) ? readFileSync(path, "utf-8") : undefined;
480
+ writeFileSync(path, mergeUpgradeSection(existing, {
481
+ from: bump.from,
482
+ to: bump.to,
483
+ date: Temporal.Now.instant().toString().slice(0, 10),
484
+ breaking: bump.pending.filter((entry) => entry.type === "breaking"),
485
+ manual: manualWithStatus(bump.manualEntries, bump.previous),
486
+ codemods: bump.ran,
487
+ }), "utf-8");
488
+ }
440
489
  // Records the codemods that already wrote files before a later one failed, so
441
490
  // a re-run resumes after them instead of replaying non-idempotent codemods.
442
491
  function writePartialMarker(out, targetDir, pending, manualEntries, failedVersion, ran, previous) {
@@ -453,7 +502,7 @@ function writePartialMarker(out, targetDir, pending, manualEntries, failedVersio
453
502
  writeUpgradeMarker(targetDir, {
454
503
  version,
455
504
  appliedAt: Temporal.Now.instant().toString(),
456
- codemods: ran,
505
+ codemods: accumulateCodemods(previous.codemods, ran),
457
506
  ...carryOverManual(previous, manualEntries.map(toMarkerManual)),
458
507
  });
459
508
  out.err(` ⚠ ${ran.length} codemod(s) already applied; wrote marker at ${version} so a re-run resumes at the failed one.`);
@@ -469,7 +518,7 @@ async function applyCodemods(out, pending, repoRoot, targetDir, dryRun,
469
518
  // Installed version for the zero-pending bootstrap marker — never a
470
519
  // `--from` filter override (that would permanently skip real pending
471
520
  // codemods once the CI guard compares against the fake marker).
472
- markerVersion, previous) {
521
+ markerVersion, previous, fromVersion, manualDetails) {
473
522
  if (pending.length === 0) {
474
523
  out.log(" ✓ Nothing new since your version.");
475
524
  if (!dryRun) {
@@ -477,11 +526,11 @@ markerVersion, previous) {
477
526
  writeUpgradeMarker(targetDir, {
478
527
  version: markerVersion,
479
528
  appliedAt: Temporal.Now.instant().toString(),
480
- codemods: [],
529
+ codemods: previous.codemods,
481
530
  ...carried,
482
531
  });
483
532
  out.log(` ✓ Applied 0 codemod(s). Wrote ${join(targetDir, ".kumiko/upgrade-state.json")}`);
484
- logOpenManual(out, carried.pendingManual ?? []);
533
+ logOpenManual(out, carried.pendingManual ?? [], manualDetails);
485
534
  }
486
535
  return 0;
487
536
  }
@@ -504,11 +553,19 @@ markerVersion, previous) {
504
553
  writeUpgradeMarker(targetDir, {
505
554
  version: markerVersion,
506
555
  appliedAt: Temporal.Now.instant().toString(),
507
- codemods: [],
556
+ codemods: previous.codemods,
508
557
  ...carried,
509
558
  });
559
+ writeUpgradeChangelog(targetDir, {
560
+ from: fromVersion,
561
+ to: markerVersion,
562
+ pending,
563
+ manualEntries,
564
+ ran: [],
565
+ previous,
566
+ });
510
567
  out.log(` ✓ Applied 0 codemod(s). Wrote ${join(targetDir, ".kumiko/upgrade-state.json")}`);
511
- logOpenManual(out, carried.pendingManual ?? []);
568
+ logOpenManual(out, carried.pendingManual ?? [], manualDetails);
512
569
  }
513
570
  return 0;
514
571
  }
@@ -546,11 +603,19 @@ markerVersion, previous) {
546
603
  writeUpgradeMarker(targetDir, {
547
604
  version: markerVersion,
548
605
  appliedAt: Temporal.Now.instant().toString(),
549
- codemods: ran,
606
+ codemods: accumulateCodemods(previous.codemods, ran),
550
607
  ...carried,
551
608
  });
609
+ writeUpgradeChangelog(targetDir, {
610
+ from: fromVersion,
611
+ to: markerVersion,
612
+ pending,
613
+ manualEntries,
614
+ ran,
615
+ previous,
616
+ });
552
617
  out.log(` ✓ Applied ${ran.length} codemod(s). Wrote ${join(targetDir, ".kumiko/upgrade-state.json")}`);
553
- logOpenManual(out, carried.pendingManual ?? []);
618
+ logOpenManual(out, carried.pendingManual ?? [], manualDetails);
554
619
  return 0;
555
620
  }
556
621
  function parseResolveInput(args) {
@@ -751,7 +816,7 @@ export async function runUpgradeCli(argv, cwd, out, options) {
751
816
  return 1;
752
817
  }
753
818
  out.log("");
754
- const code = await applyCodemods(out, pending, repoRoot, targetDir, dryRun, markerVersion ?? currentVersion, carryOf(markerRead));
819
+ const code = await applyCodemods(out, pending, repoRoot, targetDir, dryRun, markerVersion ?? currentVersion, carryOf(markerRead), currentVersion, verbose ? manualDetailsOf(allEntries) : undefined);
755
820
  out.log("");
756
821
  return code;
757
822
  }
@@ -774,7 +839,7 @@ export async function runUpgradeCli(argv, cwd, out, options) {
774
839
  if (pending.length === 0) {
775
840
  out.log(" ✓ Nothing new since your version.");
776
841
  out.log("");
777
- logOpenManual(out, openManual);
842
+ logOpenManual(out, openManual, verbose ? manualDetailsOf(allEntries) : undefined);
778
843
  return 0;
779
844
  }
780
845
  if (breaking.length > 0) {
@@ -819,6 +884,6 @@ export async function runUpgradeCli(argv, cwd, out, options) {
819
884
  out.log(" Run with --verbose for full migration details.");
820
885
  out.log("");
821
886
  }
822
- logOpenManual(out, openManual);
887
+ logOpenManual(out, openManual, verbose ? manualDetailsOf(allEntries) : undefined);
823
888
  return 0;
824
889
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.374.0",
3
+ "version": "0.377.0",
4
4
  "description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -199,8 +199,7 @@
199
199
  "./package.json": "./package.json"
200
200
  },
201
201
  "dependencies": {
202
- "@cosmicdrift/kumiko-http": "0.374.0",
203
- "@cosmicdrift/kumiko-types": "0.374.0",
202
+ "@cosmicdrift/kumiko-types": "0.377.0",
204
203
  "bullmq": "^5.76.7",
205
204
  "bun-types": "^1.3.13",
206
205
  "hono": "^4.13.1",
@@ -216,8 +215,7 @@
216
215
  "zod": "^4.4.3"
217
216
  },
218
217
  "devDependencies": {
219
- "@cosmicdrift/kumiko-dispatcher-live": "0.374.0",
220
- "@cosmicdrift/kumiko-testing": "0.374.0",
218
+ "@cosmicdrift/kumiko-testing": "0.377.0",
221
219
  "bun-types": "^1.3.13",
222
220
  "pino-pretty": "^13.1.3"
223
221
  },
@@ -62,6 +62,14 @@
62
62
  }
63
63
  },
64
64
  "overrides": [
65
+ {
66
+ "includes": ["bin/**", "scripts/**", "**/bin/**", "**/scripts/**"],
67
+ "linter": {
68
+ "rules": {
69
+ "suspicious": { "noConsole": "off" }
70
+ }
71
+ }
72
+ },
65
73
  {
66
74
  "includes": [
67
75
  "**/*.test.ts",
package/src/changes.json CHANGED
@@ -1,4 +1,75 @@
1
1
  [
2
+ {
3
+ "version": "0.377.0",
4
+ "type": "improvement",
5
+ "title": "design decisions log",
6
+ "detail": "docs/reference/decisions.md records lasting design decisions with their reasons. It is documentation only and changes no behavior."
7
+ },
8
+ {
9
+ "version": "0.377.0",
10
+ "type": "improvement",
11
+ "title": "Env hint links to the infra deploy runbook",
12
+ "detail": "The boot env report ends with a link to docs/runbooks/standard-deploy-app.md in the infra repo instead of a path in kumiko-platform that no longer exists."
13
+ },
14
+ {
15
+ "version": "0.377.0",
16
+ "type": "improvement",
17
+ "title": "Replay parser and migration splitter cover dollar quotes, RENAME, CREATE TABLE AS and DO",
18
+ "detail": "splitSqlStatements keeps dollar-quoted bodies whole and throws on an unterminated one. The replay parser follows RENAME TO, RENAME COLUMN, CREATE TABLE AS and DO blocks; tables with columns it cannot derive are skipped in the schema diff. The Bun.SQL wording in db/query.ts errors and logger names now says postgres-js."
19
+ },
20
+ {
21
+ "version": "0.377.0",
22
+ "type": "improvement",
23
+ "title": "providerPluginContextFromHandler and providerPluginContextFromJob",
24
+ "detail": "Build a ProviderPluginContext from a HandlerContext or JobContext (jobs use the system user and have no dbOutsideTransaction)."
25
+ },
26
+ {
27
+ "version": "0.377.0",
28
+ "type": "improvement",
29
+ "title": "hasOutsideTransactionDb and requireOutsideTransactionDb",
30
+ "detail": "Public helpers in the db barrel. hasOutsideTransactionDb(ctx) narrows ctx.dbOutsideTransaction to TenantDb; requireOutsideTransactionDb(ctx) returns it or throws an InternalError."
31
+ },
32
+ {
33
+ "version": "0.377.0",
34
+ "type": "breaking",
35
+ "title": "Provider contexts expose auditUserId instead of _userId",
36
+ "detail": "FileProviderContext, MailTransportContext, InboundMailContext and the context read by requireSecretsContext name the audit identity auditUserId. The old underscore field is gone.",
37
+ "migration": "Rename _userId to auditUserId wherever a provider context is built by hand (tests, custom resolvers, worker wiring). requireSecretsContext still falls back to ctx.user.id when it is unset."
38
+ },
39
+ {
40
+ "version": "0.377.0",
41
+ "type": "improvement",
42
+ "title": "Boot always sets configAccessorFactory and overrides an app value",
43
+ "detail": "addConfigAccessorFactory writes configAccessorFactory and the _configAccessorFactory alias from the effective configResolver, replacing any app-supplied factory. The replay parser also rejects DO blocks without a dollar-quoted body and accepts tags like $do1$."
44
+ },
45
+ {
46
+ "version": "0.377.0",
47
+ "type": "improvement",
48
+ "title": "http moved into framework",
49
+ "detail": "egress(), the host policy helpers and their errors are implemented in @cosmicdrift/kumiko-framework/http. @cosmicdrift/kumiko-http now only re-exports that subpath and is removed after the consumer pass of wave 4.",
50
+ "migration": "Run bun kumiko-upgrade --apply, or bun kumiko codemod package-merge . --write. It rewrites imports of @cosmicdrift/kumiko-http to @cosmicdrift/kumiko-framework/http and replaces the kumiko-http dependency in package.json with kumiko-framework.",
51
+ "codemod": "scripts/codemod/package-merge.ts"
52
+ },
53
+ {
54
+ "version": "0.377.0",
55
+ "type": "improvement",
56
+ "title": "package-merge codemod covers headless and dispatcher-live",
57
+ "detail": "The mapping table of the package-merge codemod gained the headless and dispatcher-live moves into kumiko-renderer.",
58
+ "codemod": "scripts/codemod/package-merge.ts"
59
+ },
60
+ {
61
+ "version": "0.377.0",
62
+ "type": "improvement",
63
+ "title": "package-merge codemod covers renderer-web and CSS files",
64
+ "detail": "The mapping table of the package-merge codemod gained the renderer-web move into kumiko-renderer/web, and the codemod now also rewrites .css files (@import, @plugin, @config). A @source path into the old package is reported for manual follow-up.",
65
+ "codemod": "scripts/codemod/package-merge.ts"
66
+ },
67
+ {
68
+ "version": "0.376.0",
69
+ "type": "improvement",
70
+ "title": "kumiko-upgrade writes CHANGELOG.md and shows manual entries with --verbose",
71
+ "detail": "--apply adds one section per bump to CHANGELOG.md in the app (changed packages, breaking titles, manual migrations with status, codemods applied); a re-run of the same bump replaces its section. --verbose prints detail and migration of the open manual entries. A second --apply keeps the codemods already recorded in the marker. The biome-app preset allows console in bin and scripts. AppConfig takes an optional includeBundled flag that the managed kumiko/schema.ts uses to compose the bundled auth features."
72
+ },
2
73
  {
3
74
  "version": "0.374.0",
4
75
  "type": "improvement",
@@ -140,3 +140,28 @@ bun node_modules/@cosmicdrift/kumiko-framework/src/scripts/codemod/remove-engine
140
140
 
141
141
  Runs via `bun kumiko-upgrade --apply`. Without a mode flag it writes, then formats the changed files
142
142
  with Biome. `--check` exits 1 while any rewritable or manual site is left.
143
+
144
+ ## package-merge.ts
145
+
146
+ Packages folded into another package stay as re-export shims for one wave. The codemod rewrites
147
+ imports, re-exports, `import("…")` and `mock.module` specifiers (including subpaths) of the old
148
+ package to the new one, and in `package.json` replaces the old dependency with the target package
149
+ (same version range, same block, skipped when the block already lists it). The moves are the rows
150
+ of `PACKAGE_MERGE_MAPPINGS`: `@cosmicdrift/kumiko-http` to `@cosmicdrift/kumiko-framework/http`,
151
+ `@cosmicdrift/kumiko-repo-manifest` to `@cosmicdrift/kumiko-guards/repo-manifest`,
152
+ `@cosmicdrift/kumiko-headless` to `@cosmicdrift/kumiko-renderer/headless` (subpaths keep their name),
153
+ `@cosmicdrift/kumiko-dispatcher-live` to `@cosmicdrift/kumiko-renderer/live`,
154
+ `@cosmicdrift/kumiko-renderer-web` to `@cosmicdrift/kumiko-renderer/web` (so
155
+ `@import "@cosmicdrift/kumiko-renderer-web/styles.css"` becomes
156
+ `@import "@cosmicdrift/kumiko-renderer/web/styles.css"`).
157
+
158
+ `.css` files are covered too: the specifier of `@import`, `@plugin` and `@config` is rewritten like
159
+ an import. A `@source` path into `node_modules/<old package>/…` has no mechanical target and is
160
+ reported as manual.
161
+
162
+ ```bash
163
+ bun node_modules/@cosmicdrift/kumiko-framework/src/scripts/codemod/package-merge.ts <dir> [--check | --dry-run | --write] [--no-format]
164
+ ```
165
+
166
+ Runs via `bun kumiko-upgrade --apply`. Without a mode flag it writes. `--check` exits 1 while any
167
+ site is left. Run your package manager's install afterwards to refresh the lockfile.
@@ -1,6 +1,12 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join, relative, resolve } from "node:path";
3
- import { type ImportDeclaration, type ImportSpecifier, Node, type SourceFile } from "ts-morph";
3
+ import {
4
+ type ImportDeclaration,
5
+ type ImportSpecifier,
6
+ Node,
7
+ type SourceFile,
8
+ SyntaxKind,
9
+ } from "ts-morph";
4
10
  import { collectSourceFiles } from "./collect-source-files.ts";
5
11
 
6
12
  export interface CodemodSite {
@@ -48,6 +54,47 @@ export function relativeSpecifier(
48
54
  return `${path.startsWith(".") ? path : `./${path}`}${extension}`;
49
55
  }
50
56
 
57
+ function stringLiteralOf(node: Node | undefined): Node | undefined {
58
+ return node !== undefined && Node.isStringLiteral(node) ? node : undefined;
59
+ }
60
+
61
+ export interface TextEdit {
62
+ readonly start: number;
63
+ readonly end: number;
64
+ readonly text: string;
65
+ }
66
+
67
+ export function applyEdits(source: string, edits: readonly TextEdit[]): string {
68
+ let output = source;
69
+ for (const edit of [...edits].sort((x, y) => y.start - x.start)) {
70
+ output = output.slice(0, edit.start) + edit.text + output.slice(edit.end);
71
+ }
72
+ return output;
73
+ }
74
+
75
+ export function moduleSpecifierLiterals(sourceFile: SourceFile): Node[] {
76
+ const literals: Node[] = [];
77
+ for (const declaration of [
78
+ ...sourceFile.getImportDeclarations(),
79
+ ...sourceFile.getExportDeclarations(),
80
+ ]) {
81
+ const literal = declaration.getModuleSpecifier();
82
+ if (literal !== undefined) literals.push(literal);
83
+ }
84
+ for (const importType of sourceFile.getDescendantsOfKind(SyntaxKind.ImportType)) {
85
+ const argument = importType.getArgument();
86
+ if (Node.isLiteralTypeNode(argument)) literals.push(argument.getLiteral());
87
+ }
88
+ for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
89
+ const callee = call.getExpression();
90
+ const isModuleLoad =
91
+ callee.getKind() === SyntaxKind.ImportKeyword || callee.getText() === "mock.module";
92
+ const literal = stringLiteralOf(call.getArguments()[0]);
93
+ if (isModuleLoad && literal !== undefined) literals.push(literal);
94
+ }
95
+ return literals;
96
+ }
97
+
51
98
  export function isPlainNamedImport(declaration: Node): boolean {
52
99
  return (
53
100
  Node.isImportDeclaration(declaration) &&
@@ -202,7 +249,7 @@ function parseArgs(argv: readonly string[]): CliOptions {
202
249
  return { mode: mode ?? "write", format, paths };
203
250
  }
204
251
 
205
- function isMigratableFile(name: string): boolean {
252
+ function isSourceFileName(name: string): boolean {
206
253
  return /\.(ts|tsx)$/.test(name) && !name.endsWith(".d.ts");
207
254
  }
208
255
 
@@ -215,6 +262,10 @@ export interface CodemodCliConfig {
215
262
  readonly name: string;
216
263
  readonly transform: (source: string, filePath: string) => CodemodResult;
217
264
  readonly isSkippedPath: (file: string) => boolean;
265
+ // Exact file names (e.g. package.json) handed to transform next to the .ts/.tsx sources.
266
+ readonly extraFileNames?: readonly string[];
267
+ // File extensions (e.g. ".css") handed to transform next to the .ts/.tsx sources.
268
+ readonly extraExtensions?: readonly string[];
218
269
  }
219
270
 
220
271
  function parseArgsOrExit(codemodName: string, argv: readonly string[]): CliOptions {
@@ -262,15 +313,22 @@ export async function runCodemodCli(
262
313
  argv: readonly string[],
263
314
  ): Promise<void> {
264
315
  const options = parseArgsOrExit(config.name, argv);
265
- const files = collectSourceFiles(options.paths, { isMigratableFile }).filter(
266
- (file) => !(file.includes("/__tests__/fixtures/") || config.isSkippedPath(file)),
267
- );
316
+ const files = collectSourceFiles(options.paths, {
317
+ isMigratableFile: (name) =>
318
+ isSourceFileName(name) ||
319
+ (config.extraFileNames?.includes(name) ?? false) ||
320
+ (config.extraExtensions?.some((extension) => name.endsWith(extension)) ?? false),
321
+ }).filter((file) => !(file.includes("/__tests__/fixtures/") || config.isSkippedPath(file)));
268
322
  const { rewrites, manual, changedFiles } = transformFiles(config, files, options.mode);
269
323
  console.log(
270
324
  `\n[${config.name}] ${options.mode === "write" ? "rewritten" : "rewritable"}: ${rewrites.length} site(s) in ${new Set(rewrites.map((site) => site.file)).size} file(s); manual: ${manual.length}`,
271
325
  );
272
326
  if (options.mode === "write" && options.format && changedFiles.length > 0) {
273
- await formatWithBiome(config.name, changedFiles, resolve(options.paths[0] ?? "."));
327
+ await formatWithBiome(
328
+ config.name,
329
+ changedFiles.filter((file) => isSourceFileName(file)),
330
+ resolve(options.paths[0] ?? "."),
331
+ );
274
332
  }
275
333
  process.exit(options.mode === "check" && rewrites.length + manual.length > 0 ? 1 : 0);
276
334
  }