modality-ts 0.0.16 → 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.map +1 -1
- package/dist/cli/features/extract/command.js +180 -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/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 -60
- package/dist/core/props/index.d.ts.map +1 -1
- package/dist/core/props/index.js +177 -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/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 -111
- 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
|
@@ -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
|
|
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 =
|
|
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
|
|
210
|
-
|
|
211
|
-
return {
|
|
211
|
+
const rawEntries = [{ path: resolved, text: source }];
|
|
212
|
+
return emptySurfaceProject({
|
|
212
213
|
entryFile: resolved,
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
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
|
|
369
|
+
const rawEntriesByPath = new Map();
|
|
249
370
|
for (const project of projects) {
|
|
250
|
-
for (const
|
|
251
|
-
|
|
371
|
+
for (const entry of project.rawEntries) {
|
|
372
|
+
rawEntriesByPath.set(resolve(entry.path), entry);
|
|
252
373
|
}
|
|
253
374
|
}
|
|
254
|
-
|
|
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
|
-
|
|
259
|
-
|
|
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.
|
|
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,
|
|
398
|
+
return { ...project, rawEntries: files, inventory };
|
|
286
399
|
}
|
|
287
400
|
function findManifestPath(project) {
|
|
288
|
-
return project.
|
|
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.
|
|
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;
|