modality-ts 0.0.15 → 0.0.17

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 (177) hide show
  1. package/dist/check/check-model.d.ts +4 -0
  2. package/dist/check/check-model.d.ts.map +1 -0
  3. package/dist/check/check-model.js +159 -0
  4. package/dist/check/check-model.js.map +1 -0
  5. package/dist/check/index.d.ts +2 -2
  6. package/dist/check/index.d.ts.map +1 -1
  7. package/dist/check/index.js +2 -2
  8. package/dist/check/index.js.map +1 -1
  9. package/dist/check/model-api.d.ts.map +1 -0
  10. package/dist/check/model-api.js +8 -0
  11. package/dist/check/model-api.js.map +1 -0
  12. package/dist/check/native.d.ts +6 -0
  13. package/dist/check/native.d.ts.map +1 -0
  14. package/dist/check/native.js +69 -0
  15. package/dist/check/native.js.map +1 -0
  16. package/dist/check/serialize-properties.d.ts +4 -0
  17. package/dist/check/serialize-properties.d.ts.map +1 -0
  18. package/dist/check/serialize-properties.js +4 -0
  19. package/dist/check/serialize-properties.js.map +1 -0
  20. package/dist/cli/features/check/command.d.ts.map +1 -1
  21. package/dist/cli/features/check/command.js +25 -6
  22. package/dist/cli/features/check/command.js.map +1 -1
  23. package/dist/cli/features/extract/command.d.ts +4 -1
  24. package/dist/cli/features/extract/command.d.ts.map +1 -1
  25. package/dist/cli/features/extract/command.js +262 -193
  26. package/dist/cli/features/extract/command.js.map +1 -1
  27. package/dist/cli/features/extract/project.d.ts +36 -0
  28. package/dist/cli/features/extract/project.d.ts.map +1 -0
  29. package/dist/cli/features/extract/project.js +783 -0
  30. package/dist/cli/features/extract/project.js.map +1 -0
  31. package/dist/cli/features/init/command.d.ts.map +1 -1
  32. package/dist/cli/features/init/command.js +0 -1
  33. package/dist/cli/features/init/command.js.map +1 -1
  34. package/dist/cli/runtime/index.d.ts.map +1 -1
  35. package/dist/cli/runtime/index.js +2 -1
  36. package/dist/cli/runtime/index.js.map +1 -1
  37. package/dist/core/artifacts/index.d.ts +3 -0
  38. package/dist/core/artifacts/index.d.ts.map +1 -1
  39. package/dist/core/artifacts/index.js +238 -0
  40. package/dist/core/artifacts/index.js.map +1 -1
  41. package/dist/core/index.d.ts +1 -0
  42. package/dist/core/index.d.ts.map +1 -1
  43. package/dist/core/index.js +1 -0
  44. package/dist/core/index.js.map +1 -1
  45. package/dist/core/ir/eval.d.ts +6 -0
  46. package/dist/core/ir/eval.d.ts.map +1 -0
  47. package/dist/core/ir/eval.js +104 -0
  48. package/dist/core/ir/eval.js.map +1 -0
  49. package/dist/core/ir/types.d.ts +83 -0
  50. package/dist/core/ir/types.d.ts.map +1 -1
  51. package/dist/core/props/index.d.ts +23 -54
  52. package/dist/core/props/index.d.ts.map +1 -1
  53. package/dist/core/props/index.js +182 -116
  54. package/dist/core/props/index.js.map +1 -1
  55. package/dist/core/report/types.d.ts +7 -0
  56. package/dist/core/report/types.d.ts.map +1 -1
  57. package/dist/extract/engine/pipeline/index.d.ts +4 -0
  58. package/dist/extract/engine/pipeline/index.d.ts.map +1 -1
  59. package/dist/extract/engine/pipeline/index.js +39 -15
  60. package/dist/extract/engine/pipeline/index.js.map +1 -1
  61. package/dist/extract/engine/spi/index.d.ts +32 -0
  62. package/dist/extract/engine/spi/index.d.ts.map +1 -1
  63. package/dist/extract/engine/ts/react-source-transitions.d.ts +3 -0
  64. package/dist/extract/engine/ts/react-source-transitions.d.ts.map +1 -1
  65. package/dist/extract/engine/ts/react-source-transitions.js +18 -0
  66. package/dist/extract/engine/ts/react-source-transitions.js.map +1 -1
  67. package/dist/extract/engine/ts/transition/handlers.d.ts +2 -2
  68. package/dist/extract/engine/ts/transition/handlers.d.ts.map +1 -1
  69. package/dist/extract/engine/ts/transition/handlers.js +112 -10
  70. package/dist/extract/engine/ts/transition/handlers.js.map +1 -1
  71. package/dist/extract/sources/router/index.d.ts.map +1 -1
  72. package/dist/extract/sources/router/index.js +5 -0
  73. package/dist/extract/sources/router/index.js.map +1 -1
  74. package/dist/extract/sources/router/module-roles.d.ts +7 -0
  75. package/dist/extract/sources/router/module-roles.d.ts.map +1 -0
  76. package/dist/extract/sources/router/module-roles.js +153 -0
  77. package/dist/extract/sources/router/module-roles.js.map +1 -0
  78. package/native/index.d.ts +8 -0
  79. package/native/index.js +317 -0
  80. package/native/modality-checker.linux-x64-gnu.node +0 -0
  81. package/package.json +14 -4
  82. package/dist/check/diagnostics/bounds.d.ts +0 -5
  83. package/dist/check/diagnostics/bounds.d.ts.map +0 -1
  84. package/dist/check/diagnostics/bounds.js +0 -25
  85. package/dist/check/diagnostics/bounds.js.map +0 -1
  86. package/dist/check/diagnostics/vacuity.d.ts +0 -3
  87. package/dist/check/diagnostics/vacuity.d.ts.map +0 -1
  88. package/dist/check/diagnostics/vacuity.js +0 -22
  89. package/dist/check/diagnostics/vacuity.js.map +0 -1
  90. package/dist/check/engine/check-model.d.ts +0 -7
  91. package/dist/check/engine/check-model.d.ts.map +0 -1
  92. package/dist/check/engine/check-model.js +0 -527
  93. package/dist/check/engine/check-model.js.map +0 -1
  94. package/dist/check/engine/initial-states.d.ts +0 -3
  95. package/dist/check/engine/initial-states.d.ts.map +0 -1
  96. package/dist/check/engine/initial-states.js +0 -11
  97. package/dist/check/engine/initial-states.js.map +0 -1
  98. package/dist/check/engine/model-api.d.ts.map +0 -1
  99. package/dist/check/engine/model-api.js +0 -17
  100. package/dist/check/engine/model-api.js.map +0 -1
  101. package/dist/check/engine/mounts.d.ts +0 -3
  102. package/dist/check/engine/mounts.d.ts.map +0 -1
  103. package/dist/check/engine/mounts.js +0 -13
  104. package/dist/check/engine/mounts.js.map +0 -1
  105. package/dist/check/engine/stabilize.d.ts +0 -4
  106. package/dist/check/engine/stabilize.d.ts.map +0 -1
  107. package/dist/check/engine/stabilize.js +0 -104
  108. package/dist/check/engine/stabilize.js.map +0 -1
  109. package/dist/check/engine/state-utils.d.ts +0 -13
  110. package/dist/check/engine/state-utils.d.ts.map +0 -1
  111. package/dist/check/engine/state-utils.js +0 -43
  112. package/dist/check/engine/state-utils.js.map +0 -1
  113. package/dist/check/engine/transitions.d.ts +0 -12
  114. package/dist/check/engine/transitions.d.ts.map +0 -1
  115. package/dist/check/engine/transitions.js +0 -42
  116. package/dist/check/engine/transitions.js.map +0 -1
  117. package/dist/check/properties/checked-state.d.ts +0 -3
  118. package/dist/check/properties/checked-state.d.ts.map +0 -1
  119. package/dist/check/properties/checked-state.js +0 -21
  120. package/dist/check/properties/checked-state.js.map +0 -1
  121. package/dist/check/properties/finalize.d.ts +0 -5
  122. package/dist/check/properties/finalize.d.ts.map +0 -1
  123. package/dist/check/properties/finalize.js +0 -107
  124. package/dist/check/properties/finalize.js.map +0 -1
  125. package/dist/check/properties/leads-to.d.ts +0 -8
  126. package/dist/check/properties/leads-to.d.ts.map +0 -1
  127. package/dist/check/properties/leads-to.js +0 -70
  128. package/dist/check/properties/leads-to.js.map +0 -1
  129. package/dist/check/properties/observe.d.ts +0 -6
  130. package/dist/check/properties/observe.d.ts.map +0 -1
  131. package/dist/check/properties/observe.js +0 -56
  132. package/dist/check/properties/observe.js.map +0 -1
  133. package/dist/check/properties/reachable-from.d.ts +0 -10
  134. package/dist/check/properties/reachable-from.d.ts.map +0 -1
  135. package/dist/check/properties/reachable-from.js +0 -19
  136. package/dist/check/properties/reachable-from.js.map +0 -1
  137. package/dist/check/runtime/domains.d.ts +0 -5
  138. package/dist/check/runtime/domains.d.ts.map +0 -1
  139. package/dist/check/runtime/domains.js +0 -53
  140. package/dist/check/runtime/domains.js.map +0 -1
  141. package/dist/check/runtime/effects.d.ts +0 -9
  142. package/dist/check/runtime/effects.d.ts.map +0 -1
  143. package/dist/check/runtime/effects.js +0 -86
  144. package/dist/check/runtime/effects.js.map +0 -1
  145. package/dist/check/runtime/expr.d.ts +0 -7
  146. package/dist/check/runtime/expr.d.ts.map +0 -1
  147. package/dist/check/runtime/expr.js +0 -49
  148. package/dist/check/runtime/expr.js.map +0 -1
  149. package/dist/check/runtime/navigation.d.ts +0 -7
  150. package/dist/check/runtime/navigation.d.ts.map +0 -1
  151. package/dist/check/runtime/navigation.js +0 -60
  152. package/dist/check/runtime/navigation.js.map +0 -1
  153. package/dist/check/runtime/opaque.d.ts +0 -3
  154. package/dist/check/runtime/opaque.d.ts.map +0 -1
  155. package/dist/check/runtime/opaque.js +0 -72
  156. package/dist/check/runtime/opaque.js.map +0 -1
  157. package/dist/check/runtime/paths.d.ts +0 -4
  158. package/dist/check/runtime/paths.d.ts.map +0 -1
  159. package/dist/check/runtime/paths.js +0 -28
  160. package/dist/check/runtime/paths.js.map +0 -1
  161. package/dist/check/runtime/pending.d.ts +0 -9
  162. package/dist/check/runtime/pending.d.ts.map +0 -1
  163. package/dist/check/runtime/pending.js +0 -5
  164. package/dist/check/runtime/pending.js.map +0 -1
  165. package/dist/check/runtime/tokens.d.ts +0 -7
  166. package/dist/check/runtime/tokens.d.ts.map +0 -1
  167. package/dist/check/runtime/tokens.js +0 -36
  168. package/dist/check/runtime/tokens.js.map +0 -1
  169. package/dist/check/traces/step-facts.d.ts +0 -3
  170. package/dist/check/traces/step-facts.d.ts.map +0 -1
  171. package/dist/check/traces/step-facts.js +0 -35
  172. package/dist/check/traces/step-facts.js.map +0 -1
  173. package/dist/check/traces/trace.d.ts +0 -13
  174. package/dist/check/traces/trace.d.ts.map +0 -1
  175. package/dist/check/traces/trace.js +0 -47
  176. package/dist/check/traces/trace.js.map +0 -1
  177. /package/dist/check/{engine/model-api.d.ts → model-api.d.ts} +0 -0
@@ -1,19 +1,19 @@
1
1
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
2
  import { createHash } from "node:crypto";
3
- import { dirname, extname, join, parse, resolve } from "node:path";
3
+ import { basename, dirname, extname, join, parse, relative, resolve, } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import * as ts from "typescript";
6
- import { runExtractionPipeline } from "modality-ts/extract";
6
+ import { runExtractionPipeline, } from "modality-ts/extract";
7
7
  import { canonicalJson, collectTokenDomainPaths, domainCardinality, parseModelArtifact, } from "modality-ts/core";
8
8
  import { parseReactRouterRoutes, routerSource, } from "modality-ts/extract/sources/router";
9
9
  import { emitAppModel } from "../../codegen/model.js";
10
10
  import { loadAndApplyOverlay, loadOverlaySpec } from "../../overlay.js";
11
11
  import { createBuiltinModalityRegistry } from "../../registry/index.js";
12
+ import { sourceWithReachableImports, } from "./project.js";
12
13
  export async function runExtractCommand(options) {
13
14
  const sourcePaths = normalizedSourcePaths(options);
14
15
  const projectBase = await loadExtractionProject(sourcePaths);
15
16
  const config = await loadModalityConfig(options.configPath ?? (await findNearestConfig(projectBase.configStartDir)));
16
- const route = options.route ?? config.route ?? "/";
17
17
  const appModelPath = options.appModelPath ?? `${dirname(options.modelPath)}/app.model.ts`;
18
18
  const packageJsonPath = options.packageJsonPath ??
19
19
  config.packageJsonPath ??
@@ -32,7 +32,9 @@ export async function runExtractCommand(options) {
32
32
  routerPlugin: options.routerPlugin ?? config.routerPlugin,
33
33
  });
34
34
  const routerAdapter = registry.routerPlugin ?? routerSource();
35
- const project = await attachRouteInventory(projectBase, routerAdapter);
35
+ const projectWithInventory = await attachRouteInventory(projectBase, routerAdapter);
36
+ const project = await buildClientProjectSurface(projectWithInventory, routerAdapter);
37
+ const route = resolveExtractionRoute(project, config, options, sourcePaths);
36
38
  const routePatterns = project.inventory.routes.map((node) => node.pattern);
37
39
  const effectApis = uniqueStrings([
38
40
  ...(config.effectApis ?? []),
@@ -46,9 +48,7 @@ export async function runExtractCommand(options) {
46
48
  ...(config.bounds ?? {}),
47
49
  ...(options.bounds ?? {}),
48
50
  };
49
- const pipeline = runExtractionPipeline({
50
- sourceText: project.sourceText,
51
- fileName: project.entryFile,
51
+ const pipeline = runProjectExtractionPipeline(project, {
52
52
  route,
53
53
  routePatterns,
54
54
  effectApis,
@@ -90,6 +90,7 @@ export async function runExtractCommand(options) {
90
90
  ].join("\n"));
91
91
  }
92
92
  const warnings = [
93
+ ...project.surfaceWarnings,
93
94
  ...pipeline.warnings,
94
95
  ...overlay.warnings,
95
96
  ...pluginConformanceWarnings(registry.sourcePlugins, dependencies),
@@ -102,7 +103,7 @@ export async function runExtractCommand(options) {
102
103
  extractionCaveats,
103
104
  },
104
105
  };
105
- const report = createExtractionReport(project.sourceFiles, model, warnings, overlay.ignoredVars, options.now ?? new Date(), project.inventory);
106
+ const report = createExtractionReport(project.sourceFiles, model, warnings, overlay.ignoredVars, options.now ?? new Date(), project.inventory, buildEffectOperations(project.effectApiProvenance, config.effectApis, options.effectApis));
106
107
  await mkdir(dirname(options.modelPath), { recursive: true });
107
108
  await writeFile(options.modelPath, `${canonicalJson(model)}\n`, "utf8");
108
109
  await mkdir(dirname(appModelPath), { recursive: true });
@@ -166,6 +167,7 @@ export async function runExtractCommand(options) {
166
167
  artifacts,
167
168
  lines: [
168
169
  `extracted vars=${varCount} transitions=${transitions.length}`,
170
+ `route=${route}`,
169
171
  ...(stateSpaceLine ? [stateSpaceLine] : []),
170
172
  ...(routeCoverageLine ? [routeCoverageLine] : []),
171
173
  ...(coarseDomainsLine ? [coarseDomainsLine] : []),
@@ -203,25 +205,22 @@ async function loadExtractionProject(sourcePaths) {
203
205
  if (!resolved)
204
206
  throw new Error("extract requires at least one source path");
205
207
  const info = await stat(resolved);
208
+ const tsconfig = await readTsConfigResolution(info.isDirectory() ? resolved : dirname(resolved));
206
209
  if (!info.isDirectory()) {
207
210
  const source = await readFile(resolved, "utf8");
208
- const tsconfig = await readTsConfigResolution(dirname(resolved));
209
- const imported = await sourceWithLocalImports([{ path: resolved, text: source }], tsconfig);
210
- return {
211
+ const rawEntries = [{ path: resolved, text: source }];
212
+ return emptySurfaceProject({
211
213
  entryFile: resolved,
212
- sourceText: imported.sources.map((entry) => entry.text).join("\n"),
213
- sourceFiles: imported.sources.map((entry) => entry.path),
214
- sources: imported.sources,
215
- inventory: { routes: [] },
216
- effectApis: fetchEffectApis(imported.sources.map((entry) => entry.text).join("\n")),
214
+ rawEntries,
215
+ tsconfig,
217
216
  configStartDir: dirname(resolved),
218
- };
217
+ });
219
218
  }
220
219
  const routesPath = join(resolved, "app", "routes.ts");
221
220
  const routeEntries = parseReactRouterRoutes(await readFile(routesPath, "utf8"));
222
221
  const rootPath = join(resolved, "app", "root.tsx");
223
222
  const roots = await existingFiles([rootPath]);
224
- const entries = [
223
+ const rawEntries = [
225
224
  { path: routesPath, text: await readFile(routesPath, "utf8") },
226
225
  ...(await Promise.all(roots.map(async (path) => ({ path, text: await readFile(path, "utf8") })))),
227
226
  ...(await Promise.all(routeEntries.map(async (entry) => ({
@@ -229,44 +228,159 @@ async function loadExtractionProject(sourcePaths) {
229
228
  text: await readFile(resolve(dirname(routesPath), entry.file), "utf8"),
230
229
  })))),
231
230
  ];
232
- const tsconfig = await readTsConfigResolution(resolved);
233
- const imported = await sourceWithLocalImports(entries, tsconfig);
234
- const sourceText = imported.sources.map((entry) => entry.text).join("\n");
235
- return {
231
+ return emptySurfaceProject({
236
232
  entryFile: routesPath,
237
- sourceText,
238
- sourceFiles: imported.sources.map((entry) => entry.path),
239
- sources: imported.sources,
240
- inventory: { routes: [] },
241
- effectApis: fetchEffectApis(sourceText),
233
+ rawEntries,
234
+ tsconfig,
242
235
  configStartDir: resolved,
236
+ });
237
+ }
238
+ function emptySurfaceProject(input) {
239
+ return {
240
+ entryFile: input.entryFile,
241
+ sourceText: "",
242
+ interactionSources: [],
243
+ sourceFiles: [],
244
+ sources: [],
245
+ inventory: { routes: [] },
246
+ effectApis: [],
247
+ effectApiProvenance: [],
248
+ surfaceWarnings: [],
249
+ configStartDir: input.configStartDir,
250
+ rawEntries: input.rawEntries,
251
+ tsconfig: input.tsconfig,
252
+ };
253
+ }
254
+ async function buildClientProjectSurface(project, adapter) {
255
+ const reachable = await sourceWithReachableImports(project.rawEntries, project.tsconfig, adapter, project.inventory);
256
+ const includedSources = reachable.sources.filter((entry) => entry.included);
257
+ const interactionSources = includedSources
258
+ .filter((entry) => entry.interactionText.trim().length > 0)
259
+ .map((entry) => ({ path: entry.path, text: entry.interactionText }))
260
+ .sort((left, right) => left.path.localeCompare(right.path));
261
+ const interactionSourcePaths = new Set(interactionSources.map((entry) => entry.path));
262
+ const reportSources = includedSources.filter((entry) => interactionSourcePaths.has(entry.path) ||
263
+ entry.path.endsWith("routes.ts"));
264
+ return {
265
+ ...project,
266
+ sourceText: interactionSources.map((entry) => entry.text).join("\n"),
267
+ interactionSources,
268
+ sourceFiles: reportSources
269
+ .map((entry) => entry.path)
270
+ .sort((left, right) => left.localeCompare(right)),
271
+ sources: reportSources.map((entry) => ({
272
+ path: entry.path,
273
+ text: entry.text,
274
+ })),
275
+ effectApis: reachable.effectApis,
276
+ effectApiProvenance: reachable.effectApiProvenance,
277
+ surfaceWarnings: reachable.warnings,
278
+ };
279
+ }
280
+ function runProjectExtractionPipeline(project, options) {
281
+ const fragments = project.interactionSources.length > 0
282
+ ? project.interactionSources
283
+ : project.sourceText.trim().length > 0
284
+ ? [{ path: project.entryFile, text: project.sourceText }]
285
+ : [];
286
+ if (fragments.length === 0) {
287
+ return runExtractionPipeline({
288
+ sourceText: "",
289
+ fileName: project.entryFile,
290
+ ...options,
291
+ });
292
+ }
293
+ const discoverFragments = fragments.map((entry) => ({
294
+ sourceText: entry.text,
295
+ fileName: entry.path,
296
+ }));
297
+ if (fragments.length === 1) {
298
+ const fragment = fragments[0];
299
+ return runExtractionPipeline({
300
+ sourceText: fragment.text,
301
+ fileName: fragment.path,
302
+ discoverFragments,
303
+ ...options,
304
+ });
305
+ }
306
+ return mergeExtractionPipelineResults(fragments.map((fragment) => runExtractionPipeline({
307
+ sourceText: fragment.text,
308
+ fileName: fragment.path,
309
+ discoverFragments,
310
+ ...options,
311
+ inventory: { routes: [] },
312
+ })), runExtractionPipeline({
313
+ sourceText: "",
314
+ fileName: project.entryFile,
315
+ ...options,
316
+ }));
317
+ }
318
+ function mergeExtractionPipelineResults(fragmentResults, inventoryResult) {
319
+ const transitionIds = new Set();
320
+ const transitions = [];
321
+ for (const result of fragmentResults) {
322
+ for (const transition of result.transitions) {
323
+ if (transitionIds.has(transition.id))
324
+ continue;
325
+ transitionIds.add(transition.id);
326
+ transitions.push(transition);
327
+ }
328
+ }
329
+ for (const transition of inventoryResult.transitions) {
330
+ if (transitionIds.has(transition.id))
331
+ continue;
332
+ transitionIds.add(transition.id);
333
+ transitions.push(transition);
334
+ }
335
+ const varIds = new Set();
336
+ const stateVars = [];
337
+ for (const result of fragmentResults) {
338
+ for (const decl of result.stateVars) {
339
+ if (varIds.has(decl.id))
340
+ continue;
341
+ varIds.add(decl.id);
342
+ stateVars.push(decl);
343
+ }
344
+ }
345
+ const writeChannelIds = new Set();
346
+ const writeChannels = [];
347
+ for (const result of fragmentResults) {
348
+ for (const channel of result.writeChannels) {
349
+ if (writeChannelIds.has(channel.id))
350
+ continue;
351
+ writeChannelIds.add(channel.id);
352
+ writeChannels.push(channel);
353
+ }
354
+ }
355
+ const templateFragments = fragmentResults.flatMap((result) => result.templateFragments);
356
+ const warnings = uniqueStrings(fragmentResults.flatMap((result) => [...result.warnings]));
357
+ return {
358
+ transitions,
359
+ warnings,
360
+ stateVars,
361
+ templateFragments,
362
+ routeVars: inventoryResult.routeVars,
363
+ writeChannels,
364
+ plugins: inventoryResult.plugins,
243
365
  };
244
366
  }
245
367
  async function loadMultiFileExtractionProject(sourcePaths) {
246
368
  const projects = await Promise.all(sourcePaths.map((sourcePath) => loadExtractionProject([sourcePath])));
247
- const sourcesByPath = new Map();
369
+ const rawEntriesByPath = new Map();
248
370
  for (const project of projects) {
249
- for (const source of project.sources) {
250
- sourcesByPath.set(source.path, source);
371
+ for (const entry of project.rawEntries) {
372
+ rawEntriesByPath.set(resolve(entry.path), entry);
251
373
  }
252
374
  }
253
- const sources = [...sourcesByPath.values()].sort((left, right) => left.path.localeCompare(right.path));
254
- const sourceText = sources.map((entry) => entry.text).join("\n");
255
- return {
375
+ return emptySurfaceProject({
256
376
  entryFile: projects.map((project) => project.entryFile).join(","),
257
- sourceText,
258
- sourceFiles: sources.map((entry) => entry.path),
259
- sources,
260
- inventory: { routes: [] },
261
- effectApis: uniqueStrings([
262
- ...projects.flatMap((project) => project.effectApis),
263
- ...fetchEffectApis(sourceText),
264
- ]),
377
+ rawEntries: [...rawEntriesByPath.values()].sort((left, right) => left.path.localeCompare(right.path)),
378
+ tsconfig: projects[0]?.tsconfig ?? { paths: [] },
265
379
  configStartDir: commonAncestor(projects.map((project) => project.configStartDir)),
266
- };
380
+ });
267
381
  }
268
382
  async function attachRouteInventory(project, adapter) {
269
- const files = [...project.sources];
383
+ const files = [...project.rawEntries];
270
384
  const manifestPath = files.find((file) => file.path.endsWith("routes.ts"))?.path ??
271
385
  (await findNearestRoutesManifest(project.configStartDir));
272
386
  if (manifestPath &&
@@ -281,7 +395,89 @@ async function attachRouteInventory(project, adapter) {
281
395
  files,
282
396
  readFile: (path) => readFile(path, "utf8"),
283
397
  });
284
- return { ...project, inventory };
398
+ return { ...project, rawEntries: files, inventory };
399
+ }
400
+ function findManifestPath(project) {
401
+ return project.rawEntries.find((file) => file.path.endsWith("routes.ts"))
402
+ ?.path;
403
+ }
404
+ function resolveProjectRoot(manifestPath, fallback) {
405
+ if (!manifestPath)
406
+ return fallback;
407
+ const appDir = dirname(manifestPath);
408
+ if (basename(appDir) === "app")
409
+ return dirname(appDir);
410
+ return fallback;
411
+ }
412
+ function projectRelativeSourcePath(sourcePath, project) {
413
+ const manifestPath = findManifestPath(project);
414
+ const projectRoot = resolveProjectRoot(manifestPath, project.configStartDir);
415
+ return relative(projectRoot, resolve(sourcePath)).split("\\").join("/");
416
+ }
417
+ function routesForSourceFile(sourcePath, project) {
418
+ const manifestPath = findManifestPath(project);
419
+ if (!manifestPath)
420
+ return [];
421
+ const manifestDir = dirname(manifestPath);
422
+ const resolvedSource = resolve(sourcePath);
423
+ return project.inventory.routes
424
+ .filter((node) => (node.kind === "page" || node.kind === "index") &&
425
+ node.file !== undefined &&
426
+ resolve(manifestDir, node.file) === resolvedSource)
427
+ .map((node) => node.pattern)
428
+ .sort((left, right) => left.localeCompare(right));
429
+ }
430
+ function manifestRouteFiles(project) {
431
+ const manifestPath = findManifestPath(project);
432
+ if (!manifestPath)
433
+ return [];
434
+ const manifest = project.rawEntries.find((file) => resolve(file.path) === resolve(manifestPath));
435
+ if (!manifest)
436
+ return [];
437
+ const manifestDir = dirname(manifestPath);
438
+ return parseReactRouterRoutes(manifest.text)
439
+ .map((entry) => resolve(manifestDir, entry.file))
440
+ .sort((left, right) => left.localeCompare(right));
441
+ }
442
+ function isManifestRouteSource(sourcePath, project) {
443
+ const resolvedSource = resolve(sourcePath);
444
+ return manifestRouteFiles(project).some((filePath) => resolve(filePath) === resolvedSource);
445
+ }
446
+ function resolveExtractionRoute(project, config, options, sourcePaths) {
447
+ if (options.route)
448
+ return options.route;
449
+ const manifestPath = findManifestPath(project);
450
+ const hasRouteInventory = project.inventory.routes.length > 0;
451
+ if (sourcePaths.length === 1) {
452
+ const sourcePath = resolve(sourcePaths[0] ?? "");
453
+ const relativeSource = projectRelativeSourcePath(sourcePath, project);
454
+ const configuredRoute = config.navigation?.routeBySource?.[relativeSource];
455
+ if (configuredRoute)
456
+ return configuredRoute;
457
+ const matchedRoutes = routesForSourceFile(sourcePath, project);
458
+ if (matchedRoutes.length === 1)
459
+ return matchedRoutes[0] ?? "/";
460
+ if (matchedRoutes.length > 1) {
461
+ throw new Error(`Source ${relativeSource} maps to multiple routes (${matchedRoutes.join(", ")}). Configure navigation.routeBySource to disambiguate.`);
462
+ }
463
+ if (isManifestRouteSource(sourcePath, project)) {
464
+ if (!manifestPath || !hasRouteInventory) {
465
+ throw new Error(`Cannot resolve route for ${relativeSource}: route inventory is unavailable.`);
466
+ }
467
+ throw new Error(`Cannot resolve route for ${relativeSource}: source is not mapped in the route inventory.`);
468
+ }
469
+ return config.navigation?.initialRoute ?? "/";
470
+ }
471
+ const matchedRoutes = new Set();
472
+ for (const sourcePath of sourcePaths) {
473
+ for (const pattern of routesForSourceFile(resolve(sourcePath), project)) {
474
+ matchedRoutes.add(pattern);
475
+ }
476
+ }
477
+ if (matchedRoutes.size > 1 && !config.navigation?.initialRoute) {
478
+ throw new Error(`Multi-source extraction spans routes ${[...matchedRoutes].sort().join(", ")}. Configure navigation.initialRoute.`);
479
+ }
480
+ return config.navigation?.initialRoute ?? "/";
285
481
  }
286
482
  async function findNearestRoutesManifest(startDir) {
287
483
  let current = resolve(startDir);
@@ -302,105 +498,6 @@ async function findNearestRoutesManifest(startDir) {
302
498
  }
303
499
  return undefined;
304
500
  }
305
- async function sourceWithLocalImports(entries, tsconfig) {
306
- const seen = new Set();
307
- const sources = [];
308
- const queue = [...entries];
309
- while (queue.length > 0) {
310
- const next = queue.shift();
311
- if (!next)
312
- break;
313
- const canonical = resolve(next.path);
314
- if (seen.has(canonical))
315
- continue;
316
- seen.add(canonical);
317
- sources.push({ path: canonical, text: next.text });
318
- for (const specifier of localImportSpecifiers(next.text)) {
319
- const imported = await resolveImportPath(dirname(canonical), specifier, tsconfig);
320
- if (imported)
321
- queue.push({ path: imported, text: await readFile(imported, "utf8") });
322
- }
323
- }
324
- return { sources };
325
- }
326
- function localImportSpecifiers(source) {
327
- const specs = [];
328
- const parsed = tsCreateSourceFile(source);
329
- const visit = (node) => {
330
- if (tsIsImportDeclaration(node) &&
331
- tsIsStringLiteral(node.moduleSpecifier) &&
332
- isLocalImportSpecifier(node.moduleSpecifier.text)) {
333
- specs.push(node.moduleSpecifier.text);
334
- }
335
- tsForEachChild(node, visit);
336
- };
337
- visit(parsed);
338
- return specs;
339
- }
340
- function tsCreateSourceFile(source) {
341
- return ts.createSourceFile("imports.tsx", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
342
- }
343
- function tsIsImportDeclaration(node) {
344
- return ts.isImportDeclaration(node);
345
- }
346
- function tsIsStringLiteral(node) {
347
- return ts.isStringLiteral(node);
348
- }
349
- function tsForEachChild(node, cb) {
350
- ts.forEachChild(node, cb);
351
- }
352
- function isLocalImportSpecifier(specifier) {
353
- return specifier.startsWith(".") || specifier.startsWith("~/");
354
- }
355
- async function resolveImportPath(baseDir, specifier, tsconfig) {
356
- if (specifier.startsWith("./+types/") || specifier.startsWith("../+types/"))
357
- return undefined;
358
- const bases = importBases(baseDir, specifier, tsconfig);
359
- for (const base of bases) {
360
- const resolved = await firstExistingModulePath(base);
361
- if (resolved)
362
- return resolved;
363
- }
364
- return undefined;
365
- }
366
- function importBases(baseDir, specifier, tsconfig) {
367
- if (specifier.startsWith("."))
368
- return [resolve(baseDir, specifier)];
369
- const matches = tsconfig.paths.flatMap((entry) => {
370
- if (!specifier.startsWith(entry.prefix) ||
371
- !specifier.endsWith(entry.suffix))
372
- return [];
373
- const star = specifier.slice(entry.prefix.length, specifier.length - entry.suffix.length);
374
- return entry.targets.map((target) => resolve(target.replace("*", star)));
375
- });
376
- if (matches.length > 0)
377
- return matches;
378
- return tsconfig.baseUrl ? [resolve(tsconfig.baseUrl, specifier)] : [];
379
- }
380
- async function firstExistingModulePath(base) {
381
- const candidates = /\.[cm]?[jt]sx?$/.test(base)
382
- ? [base]
383
- : [
384
- `${base}.ts`,
385
- `${base}.tsx`,
386
- `${base}.mts`,
387
- `${base}.cts`,
388
- join(base, "index.ts"),
389
- join(base, "index.tsx"),
390
- ];
391
- for (const candidate of candidates) {
392
- try {
393
- const candidateStat = await stat(candidate);
394
- if (candidateStat.isFile())
395
- return candidate;
396
- }
397
- catch (error) {
398
- if (error.code !== "ENOENT")
399
- throw error;
400
- }
401
- }
402
- return undefined;
403
- }
404
501
  async function readTsConfigResolution(startDir) {
405
502
  const tsconfigPath = await findNearestTsConfig(startDir);
406
503
  if (!tsconfigPath)
@@ -458,55 +555,6 @@ async function existingFiles(paths) {
458
555
  function sourceHashes(sources) {
459
556
  return Object.fromEntries(sources.map((source) => [source.path, sha256(source.text)]));
460
557
  }
461
- function fetchEffectApis(sourceText) {
462
- const source = tsCreateSourceFile(sourceText);
463
- const ops = new Set();
464
- const visit = (node) => {
465
- if (ts.isCallExpression(node) &&
466
- ts.isIdentifier(node.expression) &&
467
- node.expression.text === "fetch") {
468
- const op = fetchOpId(node);
469
- if (op)
470
- ops.add(op);
471
- }
472
- tsForEachChild(node, visit);
473
- };
474
- visit(source);
475
- return [...ops].sort();
476
- }
477
- function fetchOpId(call) {
478
- const first = call.arguments[0];
479
- if (!first)
480
- return undefined;
481
- const path = fetchPathValue(first);
482
- if (!path)
483
- return undefined;
484
- const method = fetchMethodValue(call.arguments[1]) ?? "GET";
485
- return `${method} ${path}`;
486
- }
487
- function fetchPathValue(expression) {
488
- if (ts.isStringLiteral(expression) ||
489
- ts.isNoSubstitutionTemplateLiteral(expression))
490
- return normalizeFetchPath(expression.text);
491
- if (ts.isTemplateExpression(expression)) {
492
- let value = expression.head.text;
493
- for (const span of expression.templateSpans)
494
- value += `:id${span.literal.text}`;
495
- return normalizeFetchPath(value);
496
- }
497
- return undefined;
498
- }
499
- function normalizeFetchPath(path) {
500
- return (path.startsWith("/") ? path : `/${path}`).replace(/\/:param(?=\/|$)/g, "/:id");
501
- }
502
- function fetchMethodValue(expression) {
503
- if (!expression || !ts.isObjectLiteralExpression(expression))
504
- return undefined;
505
- const method = expression.properties.find((property) => ts.isPropertyAssignment(property) &&
506
- propertyName(property.name) === "method");
507
- const value = method ? literalString(method.initializer) : undefined;
508
- return value?.toUpperCase();
509
- }
510
558
  function literalString(expression) {
511
559
  return ts.isStringLiteral(expression) ||
512
560
  ts.isNoSubstitutionTemplateLiteral(expression)
@@ -667,7 +715,7 @@ function buildStateContributors(model, limit = 20) {
667
715
  .sort((a, b) => b.bits - a.bits || a.source.localeCompare(b.source));
668
716
  return { totalBits, topVars, bySource };
669
717
  }
670
- function createExtractionReport(sourceFiles, model, warnings, ignoredVars, now, inventory) {
718
+ function createExtractionReport(sourceFiles, model, warnings, ignoredVars, now, inventory, effectOperations) {
671
719
  const caveats = model.metadata?.extractionCaveats ?? emptyExtractionCaveats();
672
720
  const varDomains = new Map(model.vars.map((decl) => [decl.id, decl.domain]));
673
721
  const transitionHandlers = model.transitions.map((transition) => ({
@@ -732,8 +780,29 @@ function createExtractionReport(sourceFiles, model, warnings, ignoredVars, now,
732
780
  percentExactOrOverlay: handlers.length === 0 ? 1 : exactOrOverlay / handlers.length,
733
781
  },
734
782
  warnings,
783
+ ...(effectOperations && effectOperations.length > 0
784
+ ? { effectOperations }
785
+ : {}),
735
786
  };
736
787
  }
788
+ function buildEffectOperations(provenance, configApis, optionApis) {
789
+ const entries = provenance.map((entry) => ({
790
+ opId: entry.opId,
791
+ source: entry.source.file,
792
+ line: entry.source.line,
793
+ column: entry.source.column,
794
+ origin: "source",
795
+ }));
796
+ for (const opId of configApis ?? []) {
797
+ entries.push({ opId, origin: "config" });
798
+ }
799
+ for (const opId of optionApis ?? []) {
800
+ entries.push({ opId, origin: "option" });
801
+ }
802
+ return entries.sort((left, right) => left.opId.localeCompare(right.opId) ||
803
+ (left.origin ?? "").localeCompare(right.origin ?? "") ||
804
+ (left.source ?? "").localeCompare(right.source ?? ""));
805
+ }
737
806
  function buildRouteCoverage(inventory, model) {
738
807
  if (!inventory || inventory.routes.length === 0)
739
808
  return undefined;