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.
- package/dist/check/check-model.d.ts +4 -0
- package/dist/check/check-model.d.ts.map +1 -0
- package/dist/check/check-model.js +159 -0
- package/dist/check/check-model.js.map +1 -0
- package/dist/check/index.d.ts +2 -2
- package/dist/check/index.d.ts.map +1 -1
- package/dist/check/index.js +2 -2
- package/dist/check/index.js.map +1 -1
- package/dist/check/model-api.d.ts.map +1 -0
- package/dist/check/model-api.js +8 -0
- package/dist/check/model-api.js.map +1 -0
- package/dist/check/native.d.ts +6 -0
- package/dist/check/native.d.ts.map +1 -0
- package/dist/check/native.js +69 -0
- package/dist/check/native.js.map +1 -0
- package/dist/check/serialize-properties.d.ts +4 -0
- package/dist/check/serialize-properties.d.ts.map +1 -0
- package/dist/check/serialize-properties.js +4 -0
- package/dist/check/serialize-properties.js.map +1 -0
- package/dist/cli/features/check/command.d.ts.map +1 -1
- package/dist/cli/features/check/command.js +25 -6
- package/dist/cli/features/check/command.js.map +1 -1
- package/dist/cli/features/extract/command.d.ts +4 -1
- package/dist/cli/features/extract/command.d.ts.map +1 -1
- package/dist/cli/features/extract/command.js +262 -193
- package/dist/cli/features/extract/command.js.map +1 -1
- package/dist/cli/features/extract/project.d.ts +36 -0
- package/dist/cli/features/extract/project.d.ts.map +1 -0
- package/dist/cli/features/extract/project.js +783 -0
- package/dist/cli/features/extract/project.js.map +1 -0
- package/dist/cli/features/init/command.d.ts.map +1 -1
- package/dist/cli/features/init/command.js +0 -1
- package/dist/cli/features/init/command.js.map +1 -1
- package/dist/cli/runtime/index.d.ts.map +1 -1
- package/dist/cli/runtime/index.js +2 -1
- package/dist/cli/runtime/index.js.map +1 -1
- package/dist/core/artifacts/index.d.ts +3 -0
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +238 -0
- package/dist/core/artifacts/index.js.map +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/ir/eval.d.ts +6 -0
- package/dist/core/ir/eval.d.ts.map +1 -0
- package/dist/core/ir/eval.js +104 -0
- package/dist/core/ir/eval.js.map +1 -0
- package/dist/core/ir/types.d.ts +83 -0
- package/dist/core/ir/types.d.ts.map +1 -1
- package/dist/core/props/index.d.ts +23 -54
- package/dist/core/props/index.d.ts.map +1 -1
- package/dist/core/props/index.js +182 -116
- package/dist/core/props/index.js.map +1 -1
- package/dist/core/report/types.d.ts +7 -0
- package/dist/core/report/types.d.ts.map +1 -1
- package/dist/extract/engine/pipeline/index.d.ts +4 -0
- package/dist/extract/engine/pipeline/index.d.ts.map +1 -1
- package/dist/extract/engine/pipeline/index.js +39 -15
- package/dist/extract/engine/pipeline/index.js.map +1 -1
- package/dist/extract/engine/spi/index.d.ts +32 -0
- package/dist/extract/engine/spi/index.d.ts.map +1 -1
- package/dist/extract/engine/ts/react-source-transitions.d.ts +3 -0
- package/dist/extract/engine/ts/react-source-transitions.d.ts.map +1 -1
- package/dist/extract/engine/ts/react-source-transitions.js +18 -0
- package/dist/extract/engine/ts/react-source-transitions.js.map +1 -1
- package/dist/extract/engine/ts/transition/handlers.d.ts +2 -2
- package/dist/extract/engine/ts/transition/handlers.d.ts.map +1 -1
- package/dist/extract/engine/ts/transition/handlers.js +112 -10
- package/dist/extract/engine/ts/transition/handlers.js.map +1 -1
- package/dist/extract/sources/router/index.d.ts.map +1 -1
- package/dist/extract/sources/router/index.js +5 -0
- package/dist/extract/sources/router/index.js.map +1 -1
- package/dist/extract/sources/router/module-roles.d.ts +7 -0
- package/dist/extract/sources/router/module-roles.d.ts.map +1 -0
- package/dist/extract/sources/router/module-roles.js +153 -0
- package/dist/extract/sources/router/module-roles.js.map +1 -0
- package/native/index.d.ts +8 -0
- package/native/index.js +317 -0
- package/native/modality-checker.linux-x64-gnu.node +0 -0
- package/package.json +14 -4
- package/dist/check/diagnostics/bounds.d.ts +0 -5
- package/dist/check/diagnostics/bounds.d.ts.map +0 -1
- package/dist/check/diagnostics/bounds.js +0 -25
- package/dist/check/diagnostics/bounds.js.map +0 -1
- package/dist/check/diagnostics/vacuity.d.ts +0 -3
- package/dist/check/diagnostics/vacuity.d.ts.map +0 -1
- package/dist/check/diagnostics/vacuity.js +0 -22
- package/dist/check/diagnostics/vacuity.js.map +0 -1
- package/dist/check/engine/check-model.d.ts +0 -7
- package/dist/check/engine/check-model.d.ts.map +0 -1
- package/dist/check/engine/check-model.js +0 -527
- package/dist/check/engine/check-model.js.map +0 -1
- package/dist/check/engine/initial-states.d.ts +0 -3
- package/dist/check/engine/initial-states.d.ts.map +0 -1
- package/dist/check/engine/initial-states.js +0 -11
- package/dist/check/engine/initial-states.js.map +0 -1
- package/dist/check/engine/model-api.d.ts.map +0 -1
- package/dist/check/engine/model-api.js +0 -17
- package/dist/check/engine/model-api.js.map +0 -1
- package/dist/check/engine/mounts.d.ts +0 -3
- package/dist/check/engine/mounts.d.ts.map +0 -1
- package/dist/check/engine/mounts.js +0 -13
- package/dist/check/engine/mounts.js.map +0 -1
- package/dist/check/engine/stabilize.d.ts +0 -4
- package/dist/check/engine/stabilize.d.ts.map +0 -1
- package/dist/check/engine/stabilize.js +0 -104
- package/dist/check/engine/stabilize.js.map +0 -1
- package/dist/check/engine/state-utils.d.ts +0 -13
- package/dist/check/engine/state-utils.d.ts.map +0 -1
- package/dist/check/engine/state-utils.js +0 -43
- package/dist/check/engine/state-utils.js.map +0 -1
- package/dist/check/engine/transitions.d.ts +0 -12
- package/dist/check/engine/transitions.d.ts.map +0 -1
- package/dist/check/engine/transitions.js +0 -42
- package/dist/check/engine/transitions.js.map +0 -1
- package/dist/check/properties/checked-state.d.ts +0 -3
- package/dist/check/properties/checked-state.d.ts.map +0 -1
- package/dist/check/properties/checked-state.js +0 -21
- package/dist/check/properties/checked-state.js.map +0 -1
- package/dist/check/properties/finalize.d.ts +0 -5
- package/dist/check/properties/finalize.d.ts.map +0 -1
- package/dist/check/properties/finalize.js +0 -107
- package/dist/check/properties/finalize.js.map +0 -1
- package/dist/check/properties/leads-to.d.ts +0 -8
- package/dist/check/properties/leads-to.d.ts.map +0 -1
- package/dist/check/properties/leads-to.js +0 -70
- package/dist/check/properties/leads-to.js.map +0 -1
- package/dist/check/properties/observe.d.ts +0 -6
- package/dist/check/properties/observe.d.ts.map +0 -1
- package/dist/check/properties/observe.js +0 -56
- package/dist/check/properties/observe.js.map +0 -1
- package/dist/check/properties/reachable-from.d.ts +0 -10
- package/dist/check/properties/reachable-from.d.ts.map +0 -1
- package/dist/check/properties/reachable-from.js +0 -19
- package/dist/check/properties/reachable-from.js.map +0 -1
- package/dist/check/runtime/domains.d.ts +0 -5
- package/dist/check/runtime/domains.d.ts.map +0 -1
- package/dist/check/runtime/domains.js +0 -53
- package/dist/check/runtime/domains.js.map +0 -1
- package/dist/check/runtime/effects.d.ts +0 -9
- package/dist/check/runtime/effects.d.ts.map +0 -1
- package/dist/check/runtime/effects.js +0 -86
- package/dist/check/runtime/effects.js.map +0 -1
- package/dist/check/runtime/expr.d.ts +0 -7
- package/dist/check/runtime/expr.d.ts.map +0 -1
- package/dist/check/runtime/expr.js +0 -49
- package/dist/check/runtime/expr.js.map +0 -1
- package/dist/check/runtime/navigation.d.ts +0 -7
- package/dist/check/runtime/navigation.d.ts.map +0 -1
- package/dist/check/runtime/navigation.js +0 -60
- package/dist/check/runtime/navigation.js.map +0 -1
- package/dist/check/runtime/opaque.d.ts +0 -3
- package/dist/check/runtime/opaque.d.ts.map +0 -1
- package/dist/check/runtime/opaque.js +0 -72
- package/dist/check/runtime/opaque.js.map +0 -1
- package/dist/check/runtime/paths.d.ts +0 -4
- package/dist/check/runtime/paths.d.ts.map +0 -1
- package/dist/check/runtime/paths.js +0 -28
- package/dist/check/runtime/paths.js.map +0 -1
- package/dist/check/runtime/pending.d.ts +0 -9
- package/dist/check/runtime/pending.d.ts.map +0 -1
- package/dist/check/runtime/pending.js +0 -5
- package/dist/check/runtime/pending.js.map +0 -1
- package/dist/check/runtime/tokens.d.ts +0 -7
- package/dist/check/runtime/tokens.d.ts.map +0 -1
- package/dist/check/runtime/tokens.js +0 -36
- package/dist/check/runtime/tokens.js.map +0 -1
- package/dist/check/traces/step-facts.d.ts +0 -3
- package/dist/check/traces/step-facts.d.ts.map +0 -1
- package/dist/check/traces/step-facts.js +0 -35
- package/dist/check/traces/step-facts.js.map +0 -1
- package/dist/check/traces/trace.d.ts +0 -13
- package/dist/check/traces/trace.d.ts.map +0 -1
- package/dist/check/traces/trace.js +0 -47
- package/dist/check/traces/trace.js.map +0 -1
- /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
|
|
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 =
|
|
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
|
|
209
|
-
|
|
210
|
-
return {
|
|
211
|
+
const rawEntries = [{ path: resolved, text: source }];
|
|
212
|
+
return emptySurfaceProject({
|
|
211
213
|
entryFile: resolved,
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
238
|
-
|
|
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
|
|
369
|
+
const rawEntriesByPath = new Map();
|
|
248
370
|
for (const project of projects) {
|
|
249
|
-
for (const
|
|
250
|
-
|
|
371
|
+
for (const entry of project.rawEntries) {
|
|
372
|
+
rawEntriesByPath.set(resolve(entry.path), entry);
|
|
251
373
|
}
|
|
252
374
|
}
|
|
253
|
-
|
|
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
|
-
|
|
258
|
-
|
|
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.
|
|
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;
|