modality-ts 0.0.16 → 0.0.18

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 (173) hide show
  1. package/README.md +18 -14
  2. package/dist/check/check-model.d.ts +4 -0
  3. package/dist/check/check-model.d.ts.map +1 -0
  4. package/dist/check/check-model.js +159 -0
  5. package/dist/check/check-model.js.map +1 -0
  6. package/dist/check/index.d.ts +2 -2
  7. package/dist/check/index.d.ts.map +1 -1
  8. package/dist/check/index.js +2 -2
  9. package/dist/check/index.js.map +1 -1
  10. package/dist/check/model-api.d.ts.map +1 -0
  11. package/dist/check/model-api.js +8 -0
  12. package/dist/check/model-api.js.map +1 -0
  13. package/dist/check/native.d.ts +16 -0
  14. package/dist/check/native.d.ts.map +1 -0
  15. package/dist/check/native.js +143 -0
  16. package/dist/check/native.js.map +1 -0
  17. package/dist/check/serialize-properties.d.ts +4 -0
  18. package/dist/check/serialize-properties.d.ts.map +1 -0
  19. package/dist/check/serialize-properties.js +4 -0
  20. package/dist/check/serialize-properties.js.map +1 -0
  21. package/dist/cli/features/check/command.d.ts.map +1 -1
  22. package/dist/cli/features/check/command.js +25 -6
  23. package/dist/cli/features/check/command.js.map +1 -1
  24. package/dist/cli/features/extract/command.d.ts.map +1 -1
  25. package/dist/cli/features/extract/command.js +180 -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/runtime/index.d.ts.map +1 -1
  32. package/dist/cli/runtime/index.js +2 -1
  33. package/dist/cli/runtime/index.js.map +1 -1
  34. package/dist/core/artifacts/index.d.ts +3 -0
  35. package/dist/core/artifacts/index.d.ts.map +1 -1
  36. package/dist/core/artifacts/index.js +238 -0
  37. package/dist/core/artifacts/index.js.map +1 -1
  38. package/dist/core/index.d.ts +1 -0
  39. package/dist/core/index.d.ts.map +1 -1
  40. package/dist/core/index.js +1 -0
  41. package/dist/core/index.js.map +1 -1
  42. package/dist/core/ir/eval.d.ts +6 -0
  43. package/dist/core/ir/eval.d.ts.map +1 -0
  44. package/dist/core/ir/eval.js +104 -0
  45. package/dist/core/ir/eval.js.map +1 -0
  46. package/dist/core/ir/types.d.ts +83 -0
  47. package/dist/core/ir/types.d.ts.map +1 -1
  48. package/dist/core/props/index.d.ts +23 -60
  49. package/dist/core/props/index.d.ts.map +1 -1
  50. package/dist/core/props/index.js +177 -116
  51. package/dist/core/props/index.js.map +1 -1
  52. package/dist/core/report/types.d.ts +7 -0
  53. package/dist/core/report/types.d.ts.map +1 -1
  54. package/dist/extract/engine/pipeline/index.d.ts +4 -0
  55. package/dist/extract/engine/pipeline/index.d.ts.map +1 -1
  56. package/dist/extract/engine/pipeline/index.js +39 -15
  57. package/dist/extract/engine/pipeline/index.js.map +1 -1
  58. package/dist/extract/engine/spi/index.d.ts +32 -0
  59. package/dist/extract/engine/spi/index.d.ts.map +1 -1
  60. package/dist/extract/engine/ts/react-source-transitions.d.ts +3 -0
  61. package/dist/extract/engine/ts/react-source-transitions.d.ts.map +1 -1
  62. package/dist/extract/engine/ts/react-source-transitions.js +18 -0
  63. package/dist/extract/engine/ts/react-source-transitions.js.map +1 -1
  64. package/dist/extract/sources/router/index.d.ts.map +1 -1
  65. package/dist/extract/sources/router/index.js +5 -0
  66. package/dist/extract/sources/router/index.js.map +1 -1
  67. package/dist/extract/sources/router/module-roles.d.ts +7 -0
  68. package/dist/extract/sources/router/module-roles.d.ts.map +1 -0
  69. package/dist/extract/sources/router/module-roles.js +153 -0
  70. package/dist/extract/sources/router/module-roles.js.map +1 -0
  71. package/dist/modality-ts-native.zip +0 -0
  72. package/native/modality-checker.darwin-arm64.node +0 -0
  73. package/native/modality-checker.darwin-x64.node +0 -0
  74. package/native/modality-checker.linux-arm64-gnu.node +0 -0
  75. package/native/modality-checker.linux-x64-gnu.node +0 -0
  76. package/native/modality-checker.win32-x64-msvc.node +0 -0
  77. package/package.json +14 -4
  78. package/dist/check/diagnostics/bounds.d.ts +0 -5
  79. package/dist/check/diagnostics/bounds.d.ts.map +0 -1
  80. package/dist/check/diagnostics/bounds.js +0 -25
  81. package/dist/check/diagnostics/bounds.js.map +0 -1
  82. package/dist/check/diagnostics/vacuity.d.ts +0 -3
  83. package/dist/check/diagnostics/vacuity.d.ts.map +0 -1
  84. package/dist/check/diagnostics/vacuity.js +0 -22
  85. package/dist/check/diagnostics/vacuity.js.map +0 -1
  86. package/dist/check/engine/check-model.d.ts +0 -7
  87. package/dist/check/engine/check-model.d.ts.map +0 -1
  88. package/dist/check/engine/check-model.js +0 -527
  89. package/dist/check/engine/check-model.js.map +0 -1
  90. package/dist/check/engine/initial-states.d.ts +0 -3
  91. package/dist/check/engine/initial-states.d.ts.map +0 -1
  92. package/dist/check/engine/initial-states.js +0 -11
  93. package/dist/check/engine/initial-states.js.map +0 -1
  94. package/dist/check/engine/model-api.d.ts.map +0 -1
  95. package/dist/check/engine/model-api.js +0 -17
  96. package/dist/check/engine/model-api.js.map +0 -1
  97. package/dist/check/engine/mounts.d.ts +0 -3
  98. package/dist/check/engine/mounts.d.ts.map +0 -1
  99. package/dist/check/engine/mounts.js +0 -13
  100. package/dist/check/engine/mounts.js.map +0 -1
  101. package/dist/check/engine/stabilize.d.ts +0 -4
  102. package/dist/check/engine/stabilize.d.ts.map +0 -1
  103. package/dist/check/engine/stabilize.js +0 -104
  104. package/dist/check/engine/stabilize.js.map +0 -1
  105. package/dist/check/engine/state-utils.d.ts +0 -13
  106. package/dist/check/engine/state-utils.d.ts.map +0 -1
  107. package/dist/check/engine/state-utils.js +0 -43
  108. package/dist/check/engine/state-utils.js.map +0 -1
  109. package/dist/check/engine/transitions.d.ts +0 -12
  110. package/dist/check/engine/transitions.d.ts.map +0 -1
  111. package/dist/check/engine/transitions.js +0 -42
  112. package/dist/check/engine/transitions.js.map +0 -1
  113. package/dist/check/properties/checked-state.d.ts +0 -3
  114. package/dist/check/properties/checked-state.d.ts.map +0 -1
  115. package/dist/check/properties/checked-state.js +0 -21
  116. package/dist/check/properties/checked-state.js.map +0 -1
  117. package/dist/check/properties/finalize.d.ts +0 -5
  118. package/dist/check/properties/finalize.d.ts.map +0 -1
  119. package/dist/check/properties/finalize.js +0 -107
  120. package/dist/check/properties/finalize.js.map +0 -1
  121. package/dist/check/properties/leads-to.d.ts +0 -8
  122. package/dist/check/properties/leads-to.d.ts.map +0 -1
  123. package/dist/check/properties/leads-to.js +0 -70
  124. package/dist/check/properties/leads-to.js.map +0 -1
  125. package/dist/check/properties/observe.d.ts +0 -6
  126. package/dist/check/properties/observe.d.ts.map +0 -1
  127. package/dist/check/properties/observe.js +0 -111
  128. package/dist/check/properties/observe.js.map +0 -1
  129. package/dist/check/properties/reachable-from.d.ts +0 -10
  130. package/dist/check/properties/reachable-from.d.ts.map +0 -1
  131. package/dist/check/properties/reachable-from.js +0 -19
  132. package/dist/check/properties/reachable-from.js.map +0 -1
  133. package/dist/check/runtime/domains.d.ts +0 -5
  134. package/dist/check/runtime/domains.d.ts.map +0 -1
  135. package/dist/check/runtime/domains.js +0 -53
  136. package/dist/check/runtime/domains.js.map +0 -1
  137. package/dist/check/runtime/effects.d.ts +0 -9
  138. package/dist/check/runtime/effects.d.ts.map +0 -1
  139. package/dist/check/runtime/effects.js +0 -86
  140. package/dist/check/runtime/effects.js.map +0 -1
  141. package/dist/check/runtime/expr.d.ts +0 -7
  142. package/dist/check/runtime/expr.d.ts.map +0 -1
  143. package/dist/check/runtime/expr.js +0 -49
  144. package/dist/check/runtime/expr.js.map +0 -1
  145. package/dist/check/runtime/navigation.d.ts +0 -7
  146. package/dist/check/runtime/navigation.d.ts.map +0 -1
  147. package/dist/check/runtime/navigation.js +0 -60
  148. package/dist/check/runtime/navigation.js.map +0 -1
  149. package/dist/check/runtime/opaque.d.ts +0 -3
  150. package/dist/check/runtime/opaque.d.ts.map +0 -1
  151. package/dist/check/runtime/opaque.js +0 -72
  152. package/dist/check/runtime/opaque.js.map +0 -1
  153. package/dist/check/runtime/paths.d.ts +0 -4
  154. package/dist/check/runtime/paths.d.ts.map +0 -1
  155. package/dist/check/runtime/paths.js +0 -28
  156. package/dist/check/runtime/paths.js.map +0 -1
  157. package/dist/check/runtime/pending.d.ts +0 -9
  158. package/dist/check/runtime/pending.d.ts.map +0 -1
  159. package/dist/check/runtime/pending.js +0 -5
  160. package/dist/check/runtime/pending.js.map +0 -1
  161. package/dist/check/runtime/tokens.d.ts +0 -7
  162. package/dist/check/runtime/tokens.d.ts.map +0 -1
  163. package/dist/check/runtime/tokens.js +0 -36
  164. package/dist/check/runtime/tokens.js.map +0 -1
  165. package/dist/check/traces/step-facts.d.ts +0 -3
  166. package/dist/check/traces/step-facts.d.ts.map +0 -1
  167. package/dist/check/traces/step-facts.js +0 -35
  168. package/dist/check/traces/step-facts.js.map +0 -1
  169. package/dist/check/traces/trace.d.ts +0 -13
  170. package/dist/check/traces/trace.d.ts.map +0 -1
  171. package/dist/check/traces/trace.js +0 -47
  172. package/dist/check/traces/trace.js.map +0 -1
  173. /package/dist/check/{engine/model-api.d.ts → model-api.d.ts} +0 -0
@@ -3,12 +3,13 @@ import { createHash } from "node:crypto";
3
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);
@@ -31,7 +32,8 @@ export async function runExtractCommand(options) {
31
32
  routerPlugin: options.routerPlugin ?? config.routerPlugin,
32
33
  });
33
34
  const routerAdapter = registry.routerPlugin ?? routerSource();
34
- const project = await attachRouteInventory(projectBase, routerAdapter);
35
+ const projectWithInventory = await attachRouteInventory(projectBase, routerAdapter);
36
+ const project = await buildClientProjectSurface(projectWithInventory, routerAdapter);
35
37
  const route = resolveExtractionRoute(project, config, options, sourcePaths);
36
38
  const routePatterns = project.inventory.routes.map((node) => node.pattern);
37
39
  const effectApis = uniqueStrings([
@@ -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 });
@@ -204,25 +205,22 @@ async function loadExtractionProject(sourcePaths) {
204
205
  if (!resolved)
205
206
  throw new Error("extract requires at least one source path");
206
207
  const info = await stat(resolved);
208
+ const tsconfig = await readTsConfigResolution(info.isDirectory() ? resolved : dirname(resolved));
207
209
  if (!info.isDirectory()) {
208
210
  const source = await readFile(resolved, "utf8");
209
- const tsconfig = await readTsConfigResolution(dirname(resolved));
210
- const imported = await sourceWithLocalImports([{ path: resolved, text: source }], tsconfig);
211
- return {
211
+ const rawEntries = [{ path: resolved, text: source }];
212
+ return emptySurfaceProject({
212
213
  entryFile: resolved,
213
- sourceText: imported.sources.map((entry) => entry.text).join("\n"),
214
- sourceFiles: imported.sources.map((entry) => entry.path),
215
- sources: imported.sources,
216
- inventory: { routes: [] },
217
- effectApis: fetchEffectApis(imported.sources.map((entry) => entry.text).join("\n")),
214
+ rawEntries,
215
+ tsconfig,
218
216
  configStartDir: dirname(resolved),
219
- };
217
+ });
220
218
  }
221
219
  const routesPath = join(resolved, "app", "routes.ts");
222
220
  const routeEntries = parseReactRouterRoutes(await readFile(routesPath, "utf8"));
223
221
  const rootPath = join(resolved, "app", "root.tsx");
224
222
  const roots = await existingFiles([rootPath]);
225
- const entries = [
223
+ const rawEntries = [
226
224
  { path: routesPath, text: await readFile(routesPath, "utf8") },
227
225
  ...(await Promise.all(roots.map(async (path) => ({ path, text: await readFile(path, "utf8") })))),
228
226
  ...(await Promise.all(routeEntries.map(async (entry) => ({
@@ -230,44 +228,159 @@ async function loadExtractionProject(sourcePaths) {
230
228
  text: await readFile(resolve(dirname(routesPath), entry.file), "utf8"),
231
229
  })))),
232
230
  ];
233
- const tsconfig = await readTsConfigResolution(resolved);
234
- const imported = await sourceWithLocalImports(entries, tsconfig);
235
- const sourceText = imported.sources.map((entry) => entry.text).join("\n");
236
- return {
231
+ return emptySurfaceProject({
237
232
  entryFile: routesPath,
238
- sourceText,
239
- sourceFiles: imported.sources.map((entry) => entry.path),
240
- sources: imported.sources,
241
- inventory: { routes: [] },
242
- effectApis: fetchEffectApis(sourceText),
233
+ rawEntries,
234
+ tsconfig,
243
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,
244
365
  };
245
366
  }
246
367
  async function loadMultiFileExtractionProject(sourcePaths) {
247
368
  const projects = await Promise.all(sourcePaths.map((sourcePath) => loadExtractionProject([sourcePath])));
248
- const sourcesByPath = new Map();
369
+ const rawEntriesByPath = new Map();
249
370
  for (const project of projects) {
250
- for (const source of project.sources) {
251
- sourcesByPath.set(source.path, source);
371
+ for (const entry of project.rawEntries) {
372
+ rawEntriesByPath.set(resolve(entry.path), entry);
252
373
  }
253
374
  }
254
- const sources = [...sourcesByPath.values()].sort((left, right) => left.path.localeCompare(right.path));
255
- const sourceText = sources.map((entry) => entry.text).join("\n");
256
- return {
375
+ return emptySurfaceProject({
257
376
  entryFile: projects.map((project) => project.entryFile).join(","),
258
- sourceText,
259
- sourceFiles: sources.map((entry) => entry.path),
260
- sources,
261
- inventory: { routes: [] },
262
- effectApis: uniqueStrings([
263
- ...projects.flatMap((project) => project.effectApis),
264
- ...fetchEffectApis(sourceText),
265
- ]),
377
+ rawEntries: [...rawEntriesByPath.values()].sort((left, right) => left.path.localeCompare(right.path)),
378
+ tsconfig: projects[0]?.tsconfig ?? { paths: [] },
266
379
  configStartDir: commonAncestor(projects.map((project) => project.configStartDir)),
267
- };
380
+ });
268
381
  }
269
382
  async function attachRouteInventory(project, adapter) {
270
- const files = [...project.sources];
383
+ const files = [...project.rawEntries];
271
384
  const manifestPath = files.find((file) => file.path.endsWith("routes.ts"))?.path ??
272
385
  (await findNearestRoutesManifest(project.configStartDir));
273
386
  if (manifestPath &&
@@ -282,10 +395,11 @@ async function attachRouteInventory(project, adapter) {
282
395
  files,
283
396
  readFile: (path) => readFile(path, "utf8"),
284
397
  });
285
- return { ...project, sources: files, inventory };
398
+ return { ...project, rawEntries: files, inventory };
286
399
  }
287
400
  function findManifestPath(project) {
288
- return project.sources.find((file) => file.path.endsWith("routes.ts"))?.path;
401
+ return project.rawEntries.find((file) => file.path.endsWith("routes.ts"))
402
+ ?.path;
289
403
  }
290
404
  function resolveProjectRoot(manifestPath, fallback) {
291
405
  if (!manifestPath)
@@ -317,7 +431,7 @@ function manifestRouteFiles(project) {
317
431
  const manifestPath = findManifestPath(project);
318
432
  if (!manifestPath)
319
433
  return [];
320
- const manifest = project.sources.find((file) => resolve(file.path) === resolve(manifestPath));
434
+ const manifest = project.rawEntries.find((file) => resolve(file.path) === resolve(manifestPath));
321
435
  if (!manifest)
322
436
  return [];
323
437
  const manifestDir = dirname(manifestPath);
@@ -384,105 +498,6 @@ async function findNearestRoutesManifest(startDir) {
384
498
  }
385
499
  return undefined;
386
500
  }
387
- async function sourceWithLocalImports(entries, tsconfig) {
388
- const seen = new Set();
389
- const sources = [];
390
- const queue = [...entries];
391
- while (queue.length > 0) {
392
- const next = queue.shift();
393
- if (!next)
394
- break;
395
- const canonical = resolve(next.path);
396
- if (seen.has(canonical))
397
- continue;
398
- seen.add(canonical);
399
- sources.push({ path: canonical, text: next.text });
400
- for (const specifier of localImportSpecifiers(next.text)) {
401
- const imported = await resolveImportPath(dirname(canonical), specifier, tsconfig);
402
- if (imported)
403
- queue.push({ path: imported, text: await readFile(imported, "utf8") });
404
- }
405
- }
406
- return { sources };
407
- }
408
- function localImportSpecifiers(source) {
409
- const specs = [];
410
- const parsed = tsCreateSourceFile(source);
411
- const visit = (node) => {
412
- if (tsIsImportDeclaration(node) &&
413
- tsIsStringLiteral(node.moduleSpecifier) &&
414
- isLocalImportSpecifier(node.moduleSpecifier.text)) {
415
- specs.push(node.moduleSpecifier.text);
416
- }
417
- tsForEachChild(node, visit);
418
- };
419
- visit(parsed);
420
- return specs;
421
- }
422
- function tsCreateSourceFile(source) {
423
- return ts.createSourceFile("imports.tsx", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
424
- }
425
- function tsIsImportDeclaration(node) {
426
- return ts.isImportDeclaration(node);
427
- }
428
- function tsIsStringLiteral(node) {
429
- return ts.isStringLiteral(node);
430
- }
431
- function tsForEachChild(node, cb) {
432
- ts.forEachChild(node, cb);
433
- }
434
- function isLocalImportSpecifier(specifier) {
435
- return specifier.startsWith(".") || specifier.startsWith("~/");
436
- }
437
- async function resolveImportPath(baseDir, specifier, tsconfig) {
438
- if (specifier.startsWith("./+types/") || specifier.startsWith("../+types/"))
439
- return undefined;
440
- const bases = importBases(baseDir, specifier, tsconfig);
441
- for (const base of bases) {
442
- const resolved = await firstExistingModulePath(base);
443
- if (resolved)
444
- return resolved;
445
- }
446
- return undefined;
447
- }
448
- function importBases(baseDir, specifier, tsconfig) {
449
- if (specifier.startsWith("."))
450
- return [resolve(baseDir, specifier)];
451
- const matches = tsconfig.paths.flatMap((entry) => {
452
- if (!specifier.startsWith(entry.prefix) ||
453
- !specifier.endsWith(entry.suffix))
454
- return [];
455
- const star = specifier.slice(entry.prefix.length, specifier.length - entry.suffix.length);
456
- return entry.targets.map((target) => resolve(target.replace("*", star)));
457
- });
458
- if (matches.length > 0)
459
- return matches;
460
- return tsconfig.baseUrl ? [resolve(tsconfig.baseUrl, specifier)] : [];
461
- }
462
- async function firstExistingModulePath(base) {
463
- const candidates = /\.[cm]?[jt]sx?$/.test(base)
464
- ? [base]
465
- : [
466
- `${base}.ts`,
467
- `${base}.tsx`,
468
- `${base}.mts`,
469
- `${base}.cts`,
470
- join(base, "index.ts"),
471
- join(base, "index.tsx"),
472
- ];
473
- for (const candidate of candidates) {
474
- try {
475
- const candidateStat = await stat(candidate);
476
- if (candidateStat.isFile())
477
- return candidate;
478
- }
479
- catch (error) {
480
- if (error.code !== "ENOENT")
481
- throw error;
482
- }
483
- }
484
- return undefined;
485
- }
486
501
  async function readTsConfigResolution(startDir) {
487
502
  const tsconfigPath = await findNearestTsConfig(startDir);
488
503
  if (!tsconfigPath)
@@ -540,55 +555,6 @@ async function existingFiles(paths) {
540
555
  function sourceHashes(sources) {
541
556
  return Object.fromEntries(sources.map((source) => [source.path, sha256(source.text)]));
542
557
  }
543
- function fetchEffectApis(sourceText) {
544
- const source = tsCreateSourceFile(sourceText);
545
- const ops = new Set();
546
- const visit = (node) => {
547
- if (ts.isCallExpression(node) &&
548
- ts.isIdentifier(node.expression) &&
549
- node.expression.text === "fetch") {
550
- const op = fetchOpId(node);
551
- if (op)
552
- ops.add(op);
553
- }
554
- tsForEachChild(node, visit);
555
- };
556
- visit(source);
557
- return [...ops].sort();
558
- }
559
- function fetchOpId(call) {
560
- const first = call.arguments[0];
561
- if (!first)
562
- return undefined;
563
- const path = fetchPathValue(first);
564
- if (!path)
565
- return undefined;
566
- const method = fetchMethodValue(call.arguments[1]) ?? "GET";
567
- return `${method} ${path}`;
568
- }
569
- function fetchPathValue(expression) {
570
- if (ts.isStringLiteral(expression) ||
571
- ts.isNoSubstitutionTemplateLiteral(expression))
572
- return normalizeFetchPath(expression.text);
573
- if (ts.isTemplateExpression(expression)) {
574
- let value = expression.head.text;
575
- for (const span of expression.templateSpans)
576
- value += `:id${span.literal.text}`;
577
- return normalizeFetchPath(value);
578
- }
579
- return undefined;
580
- }
581
- function normalizeFetchPath(path) {
582
- return (path.startsWith("/") ? path : `/${path}`).replace(/\/:param(?=\/|$)/g, "/:id");
583
- }
584
- function fetchMethodValue(expression) {
585
- if (!expression || !ts.isObjectLiteralExpression(expression))
586
- return undefined;
587
- const method = expression.properties.find((property) => ts.isPropertyAssignment(property) &&
588
- propertyName(property.name) === "method");
589
- const value = method ? literalString(method.initializer) : undefined;
590
- return value?.toUpperCase();
591
- }
592
558
  function literalString(expression) {
593
559
  return ts.isStringLiteral(expression) ||
594
560
  ts.isNoSubstitutionTemplateLiteral(expression)
@@ -749,7 +715,7 @@ function buildStateContributors(model, limit = 20) {
749
715
  .sort((a, b) => b.bits - a.bits || a.source.localeCompare(b.source));
750
716
  return { totalBits, topVars, bySource };
751
717
  }
752
- function createExtractionReport(sourceFiles, model, warnings, ignoredVars, now, inventory) {
718
+ function createExtractionReport(sourceFiles, model, warnings, ignoredVars, now, inventory, effectOperations) {
753
719
  const caveats = model.metadata?.extractionCaveats ?? emptyExtractionCaveats();
754
720
  const varDomains = new Map(model.vars.map((decl) => [decl.id, decl.domain]));
755
721
  const transitionHandlers = model.transitions.map((transition) => ({
@@ -814,8 +780,29 @@ function createExtractionReport(sourceFiles, model, warnings, ignoredVars, now,
814
780
  percentExactOrOverlay: handlers.length === 0 ? 1 : exactOrOverlay / handlers.length,
815
781
  },
816
782
  warnings,
783
+ ...(effectOperations && effectOperations.length > 0
784
+ ? { effectOperations }
785
+ : {}),
817
786
  };
818
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
+ }
819
806
  function buildRouteCoverage(inventory, model) {
820
807
  if (!inventory || inventory.routes.length === 0)
821
808
  return undefined;