@cplieger/deadset-ts 5.1.0 → 5.1.1
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/README.md +1 -1
- package/dist/src/analysis.js +72 -33
- package/dist/src/configuration-files.js +1 -1
- package/dist/src/contextual-uses.js +21 -14
- package/dist/src/exempt.js +23 -8
- package/dist/src/file-facts.js +23 -20
- package/dist/src/graph.js +1 -1
- package/dist/src/inventory.js +13 -3
- package/dist/src/manifest.js +29 -4
- package/dist/src/outside-tests.js +139 -0
- package/dist/src/overrides.js +104 -0
- package/dist/src/paths.js +17 -0
- package/dist/src/references.js +30 -1
- package/dist/src/roots.js +120 -36
- package/dist/src/session.js +4 -3
- package/dist/src/template-field.js +60 -3
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +110 -44
- package/src/configuration-files.ts +1 -1
- package/src/contextual-uses.ts +20 -16
- package/src/exempt.ts +28 -8
- package/src/file-facts.ts +24 -20
- package/src/graph.ts +4 -4
- package/src/inventory.ts +15 -3
- package/src/manifest.ts +40 -4
- package/src/outside-tests.ts +191 -0
- package/src/overrides.ts +136 -0
- package/src/paths.ts +18 -0
- package/src/references.ts +46 -7
- package/src/roots.ts +141 -43
- package/src/session.ts +4 -2
- package/src/template-field.ts +65 -3
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The overrides of the target's class members. A call through the base class, `this`
|
|
3
|
+
* in the base's own body or a value typed as the base, names the base's member and
|
|
4
|
+
* runs the override of whichever subclass the value is, so the base member's
|
|
5
|
+
* declaration references every override as a use of it.
|
|
6
|
+
*/
|
|
7
|
+
import { isClassDeclaration, isClassExpression, isExpressionWithTypeArguments, isPropertyAccessExpression, isQualifiedName, SyntaxKind, } from "@typescript/native/unstable/ast";
|
|
8
|
+
import { memberComponent } from "./class-members.js";
|
|
9
|
+
import { nodeKey } from "./inventory.js";
|
|
10
|
+
import { UNANSWERED } from "./query.js";
|
|
11
|
+
/** The member kinds a subclass overrides. */
|
|
12
|
+
const OVERRIDABLE = new Set(["method", "class-member"]);
|
|
13
|
+
/** The node naming the class one `extends` entry names. */
|
|
14
|
+
function nameOf(entry) {
|
|
15
|
+
if (isExpressionWithTypeArguments(entry)) {
|
|
16
|
+
return isPropertyAccessExpression(entry.expression) ? entry.expression.name : entry.expression;
|
|
17
|
+
}
|
|
18
|
+
return isQualifiedName(entry.typeName) ? entry.typeName.right : entry.typeName;
|
|
19
|
+
}
|
|
20
|
+
/** Each class of the target whose `extends` clause names a class, with the name node. */
|
|
21
|
+
function extending(project, held) {
|
|
22
|
+
const found = [];
|
|
23
|
+
for (const file of project.ownSourceFiles()) {
|
|
24
|
+
const visit = (node) => {
|
|
25
|
+
if (isClassDeclaration(node) || isClassExpression(node)) {
|
|
26
|
+
const id = held.declarations.get(nodeKey(file, node));
|
|
27
|
+
const clause = node.heritageClauses?.find((one) => one.token === SyntaxKind.ExtendsKeyword);
|
|
28
|
+
const base = clause?.types[0];
|
|
29
|
+
if (id !== undefined && base !== undefined) {
|
|
30
|
+
found.push({ id, base: nameOf(base) });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
node.forEachChild(visit);
|
|
34
|
+
};
|
|
35
|
+
file.forEachChild(visit);
|
|
36
|
+
}
|
|
37
|
+
return found;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The references each base class member makes to its overrides, for the classes of one
|
|
41
|
+
* project that extend another class of the target. A member a nearer base already
|
|
42
|
+
* overrides is reached through that override. A private member is not overridden.
|
|
43
|
+
* Each reference is placed at the override and made by its file.
|
|
44
|
+
*/
|
|
45
|
+
export function overrideReferences(project, held, chains, testFiles) {
|
|
46
|
+
const classes = extending(project, held);
|
|
47
|
+
if (classes.length === 0) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
|
|
51
|
+
const members = new Map();
|
|
52
|
+
for (const symbol of held.symbols) {
|
|
53
|
+
const component = memberComponent(symbol, byId);
|
|
54
|
+
if (component === undefined ||
|
|
55
|
+
!OVERRIDABLE.has(symbol.kind) ||
|
|
56
|
+
symbol.visibility === "private" ||
|
|
57
|
+
symbol.visibility === "private-name") {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const own = members.get(symbol.parent) ?? new Map();
|
|
61
|
+
own.set(component, symbol);
|
|
62
|
+
members.set(symbol.parent, own);
|
|
63
|
+
}
|
|
64
|
+
const bases = new Map();
|
|
65
|
+
const symbols = project.symbolsAt(classes.map((one) => project.handle(one.base)));
|
|
66
|
+
classes.forEach(({ id }, index) => {
|
|
67
|
+
const symbol = symbols[index];
|
|
68
|
+
if (symbol !== undefined && symbol !== UNANSWERED) {
|
|
69
|
+
bases.set(id, chains
|
|
70
|
+
.chainOf(symbol)
|
|
71
|
+
.map((target) => target.id)
|
|
72
|
+
.filter((target) => byId.get(target)?.kind === "class" && target !== id));
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
/** The nearest member of one component the class or a class it extends declares. */
|
|
76
|
+
const declaredAbove = (classId, component, seen) => {
|
|
77
|
+
if (seen.has(classId)) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
seen.add(classId);
|
|
81
|
+
const own = members.get(classId)?.get(component);
|
|
82
|
+
if (own !== undefined) {
|
|
83
|
+
return [own.id];
|
|
84
|
+
}
|
|
85
|
+
return (bases.get(classId) ?? []).flatMap((base) => declaredAbove(base, component, seen));
|
|
86
|
+
};
|
|
87
|
+
const found = [];
|
|
88
|
+
for (const [classId, baseIds] of bases) {
|
|
89
|
+
for (const [component, member] of members.get(classId) ?? []) {
|
|
90
|
+
const seen = new Set([classId]);
|
|
91
|
+
for (const base of baseIds.flatMap((one) => declaredAbove(one, component, seen))) {
|
|
92
|
+
found.push({
|
|
93
|
+
from: base,
|
|
94
|
+
to: member.id,
|
|
95
|
+
position: member.position,
|
|
96
|
+
use: "read",
|
|
97
|
+
resolution: "override",
|
|
98
|
+
test: testFiles.has(member.position.path),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return found;
|
|
104
|
+
}
|
package/dist/src/paths.js
CHANGED
|
@@ -65,6 +65,23 @@ export function dirnamePath(path) {
|
|
|
65
65
|
export function resolvePath(against, path) {
|
|
66
66
|
return isAbsolutePath(path) ? normalizePath(path) : joinPath(against, path);
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* The relative path before the query of a relative module specifier such as
|
|
70
|
+
* `./worker.ts?worker`, the module a bundler loads; undefined for a specifier with no
|
|
71
|
+
* query, one that is not relative, and one whose query has a `raw` parameter, which
|
|
72
|
+
* reads the file as text and so evaluates nothing.
|
|
73
|
+
*/
|
|
74
|
+
export function queriedModulePath(specifier) {
|
|
75
|
+
const query = specifier.indexOf("?");
|
|
76
|
+
if (query < 0 || !(specifier.startsWith("./") || specifier.startsWith("../"))) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
const raw = specifier
|
|
80
|
+
.slice(query + 1)
|
|
81
|
+
.split("&")
|
|
82
|
+
.some((parameter) => parameter.split("=")[0] === "raw");
|
|
83
|
+
return raw ? undefined : specifier.slice(0, query);
|
|
84
|
+
}
|
|
68
85
|
/**
|
|
69
86
|
* One path as it reads below a root, or undefined when it is not below the root.
|
|
70
87
|
* Both are normalized first, so a root written with a trailing separator or a `.`
|
package/dist/src/references.js
CHANGED
|
@@ -4,7 +4,8 @@ import { contextualUses } from "./contextual-uses.js";
|
|
|
4
4
|
import { destructuring, propertyReadsOf } from "./destructuring.js";
|
|
5
5
|
import { globExpression } from "./glob.js";
|
|
6
6
|
import { declarationsByName, nodeKey } from "./inventory.js";
|
|
7
|
-
import {
|
|
7
|
+
import { overrideReferences } from "./overrides.js";
|
|
8
|
+
import { dirnamePath, queriedModulePath, relativePath, resolvePath } from "./paths.js";
|
|
8
9
|
import { byPosition, isComponentFile, renderPosition } from "./position.js";
|
|
9
10
|
import { DEFAULT_BATCH_CAP, propertyTables, UNANSWERED } from "./query.js";
|
|
10
11
|
import { typeQueryAliases } from "./type-query-alias.js";
|
|
@@ -200,6 +201,19 @@ function evaluatedBy(node) {
|
|
|
200
201
|
}
|
|
201
202
|
return undefined;
|
|
202
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* The own file a module specifier with a query evaluates ({@link queriedModulePath}): the
|
|
206
|
+
* checker resolves the whole specifier to whatever module declaration matches it.
|
|
207
|
+
*/
|
|
208
|
+
function queriedFile(file, specifier, fileIdByName) {
|
|
209
|
+
if (!isStringLiteral(specifier) && !isNoSubstitutionTemplateLiteral(specifier)) {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
const path = queriedModulePath(specifier.text);
|
|
213
|
+
return path === undefined
|
|
214
|
+
? undefined
|
|
215
|
+
: fileIdByName.get(resolvePath(dirnamePath(file.fileName), path));
|
|
216
|
+
}
|
|
203
217
|
/**
|
|
204
218
|
* The re-exports one node writes, if it is an export declaration. A specifier and a
|
|
205
219
|
* namespace export are declarations whose names are where they are written, so no use is
|
|
@@ -654,6 +668,7 @@ export function references(project, held, targetRoot, options) {
|
|
|
654
668
|
const fileId = declaredAt(file) ?? "";
|
|
655
669
|
return { file, fileId, ...sitesOf(file, declarations, fileId, unread) };
|
|
656
670
|
});
|
|
671
|
+
const fileIdByName = new Map(walked.filter((one) => fileIds.has(one.fileId)).map((one) => [one.file.fileName, one.fileId]));
|
|
657
672
|
const contextual = contextualUses(project, held, chains, walked.flatMap((one) => one.literals), walked.flatMap((one) => one.flows), cap, tables);
|
|
658
673
|
const writtenBy = new Map();
|
|
659
674
|
for (const write of contextual.writes) {
|
|
@@ -796,6 +811,17 @@ export function references(project, held, targetRoot, options) {
|
|
|
796
811
|
}
|
|
797
812
|
}
|
|
798
813
|
for (const evaluation of evaluations) {
|
|
814
|
+
const queried = queriedFile(file, evaluation.node, fileIdByName);
|
|
815
|
+
if (queried !== undefined) {
|
|
816
|
+
found.push({
|
|
817
|
+
from: evaluation.from,
|
|
818
|
+
to: queried,
|
|
819
|
+
position: renderPosition(file, root, evaluation.node.getStart()),
|
|
820
|
+
use: "evaluation",
|
|
821
|
+
resolution: "syntax",
|
|
822
|
+
test,
|
|
823
|
+
});
|
|
824
|
+
}
|
|
799
825
|
const module = answered.get(evaluation.node);
|
|
800
826
|
if (module === undefined) {
|
|
801
827
|
continue;
|
|
@@ -924,6 +950,9 @@ export function references(project, held, targetRoot, options) {
|
|
|
924
950
|
}
|
|
925
951
|
}
|
|
926
952
|
}
|
|
953
|
+
if (consumer === undefined) {
|
|
954
|
+
found.push(...overrideReferences(project, held, chains, new Set(tests)));
|
|
955
|
+
}
|
|
927
956
|
found.sort((a, b) => byPosition(a.position, b.position) ||
|
|
928
957
|
compare(a.to, b.to) ||
|
|
929
958
|
compare(a.from, b.from) ||
|
package/dist/src/roots.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* decides; the published API only supposes a consumer, so a published export is live
|
|
8
8
|
* under reachability and still a candidate under reference counting.
|
|
9
9
|
*/
|
|
10
|
+
import { isExpressionWithTypeArguments, isFunctionLikeDeclaration, isPropertyAccessExpression, isQualifiedName, isTypeQueryNode, isTypeReferenceNode, } from "@typescript/native/unstable/ast";
|
|
10
11
|
import { aliasChains } from "./alias-chain.js";
|
|
11
12
|
import { conventionEntries } from "./conventions.js";
|
|
12
13
|
import { entryPoints } from "./entry-points.js";
|
|
@@ -131,7 +132,7 @@ export function roots(project, held, targetRoot, options) {
|
|
|
131
132
|
publishes: entry.published &&
|
|
132
133
|
(options.manifest.declaresExports ? entry.member.startsWith("exports") : true),
|
|
133
134
|
})),
|
|
134
|
-
...(options.
|
|
135
|
+
...(options.otherPackages ?? []).map((entry) => ({ entry, publishes: entry.published })),
|
|
135
136
|
];
|
|
136
137
|
for (const { entry, publishes } of entries) {
|
|
137
138
|
if (options.publishedAPI && publishes) {
|
|
@@ -188,10 +189,11 @@ export function roots(project, held, targetRoot, options) {
|
|
|
188
189
|
}
|
|
189
190
|
}
|
|
190
191
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
192
|
+
const api = options.publishedAPI
|
|
193
|
+
? publishedAPI(project, published, exportsOf, held, files.byPath)
|
|
194
|
+
: { ids: [], guessed: [] };
|
|
195
|
+
for (const id of api.ids) {
|
|
196
|
+
add(id, "published-api", "");
|
|
195
197
|
}
|
|
196
198
|
const unmatched = configured(held, options.patterns, add);
|
|
197
199
|
const sites = new Map(held.symbols.map((symbol) => [symbol.id, symbol.position]));
|
|
@@ -205,7 +207,7 @@ export function roots(project, held, targetRoot, options) {
|
|
|
205
207
|
configFile: project.configFile,
|
|
206
208
|
liveUnderReachability: ordered,
|
|
207
209
|
unmatched,
|
|
208
|
-
unanswered: answered ?
|
|
210
|
+
unanswered: answered ? api.guessed : held.symbols.map((symbol) => symbol.id),
|
|
209
211
|
};
|
|
210
212
|
}
|
|
211
213
|
/**
|
|
@@ -255,52 +257,134 @@ function exportTables(project, held, wanted) {
|
|
|
255
257
|
/** The kinds of container whose export table, not its members' visibility, says what it publishes. */
|
|
256
258
|
const EXPORTING_CONTAINERS = new Set(["file", "namespace"]);
|
|
257
259
|
/**
|
|
258
|
-
* Every declaration a consumer of the library can name: what the published files
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
* name. Below a published module or namespace the walk follows what its export table
|
|
265
|
-
* names; below any other published declaration it follows every member a consumer
|
|
266
|
-
* can write: a public one, and a protected one, which a consumer's subclass names. A
|
|
267
|
-
* private member is not published, nor is anything below one, and a type parameter
|
|
268
|
-
* is not published either, because a consumer supplies a type argument by position.
|
|
269
|
-
*
|
|
270
|
-
* A published file is not itself published. A consumer imports a module and names
|
|
271
|
-
* what it exports; the file is a root because the manifest names it, which is a
|
|
272
|
-
* different reason.
|
|
260
|
+
* Every declaration a consumer of the library can name: what the published files export
|
|
261
|
+
* and, below each, the members a consumer writes: a namespace's exported ones, any other
|
|
262
|
+
* declaration's public and protected ones, never a private one or a type parameter. A
|
|
263
|
+
* declaration a published one's type names is published too, exported or not. A type name
|
|
264
|
+
* the checker leaves unanswered publishes every declaration spelled as it is, and those are
|
|
265
|
+
* `guessed`. A published file is not itself published.
|
|
273
266
|
*/
|
|
274
|
-
function publishedAPI(published, exportsOf, held) {
|
|
267
|
+
function publishedAPI(project, published, exportsOf, held, byPath) {
|
|
275
268
|
const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
|
|
276
269
|
const children = childrenOf(held);
|
|
270
|
+
const nodes = declarationNodes(held, byPath);
|
|
271
|
+
const chains = aliasChains(project, held);
|
|
277
272
|
const reached = new Set();
|
|
273
|
+
const guessed = new Set();
|
|
278
274
|
const pending = [];
|
|
275
|
+
const reach = (id) => {
|
|
276
|
+
if (!reached.has(id) && byId.get(id)?.kind !== "file") {
|
|
277
|
+
reached.add(id);
|
|
278
|
+
pending.push(id);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
279
281
|
for (const file of published) {
|
|
280
282
|
for (const id of exportsOf.get(file.fileName) ?? []) {
|
|
281
283
|
if (byId.get(id)?.kind === "file") {
|
|
282
284
|
pending.push(id);
|
|
283
285
|
}
|
|
284
|
-
else
|
|
285
|
-
|
|
286
|
-
pending.push(id);
|
|
286
|
+
else {
|
|
287
|
+
reach(id);
|
|
287
288
|
}
|
|
288
289
|
}
|
|
289
290
|
}
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
for (
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
291
|
+
while (pending.length > 0) {
|
|
292
|
+
const round = [];
|
|
293
|
+
for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
|
|
294
|
+
round.push(next);
|
|
295
|
+
const exporting = EXPORTING_CONTAINERS.has(byId.get(next)?.kind ?? "");
|
|
296
|
+
for (const member of children.get(next) ?? []) {
|
|
297
|
+
const reachable = exporting
|
|
298
|
+
? member.exported
|
|
299
|
+
: member.visibility !== "private" && member.visibility !== "private-name";
|
|
300
|
+
if (member.kind !== "type-parameter" && reachable) {
|
|
301
|
+
reach(member.id);
|
|
302
|
+
}
|
|
298
303
|
}
|
|
299
|
-
reached.add(member.id);
|
|
300
|
-
pending.push(member.id);
|
|
301
304
|
}
|
|
305
|
+
const named = round.flatMap((id) => {
|
|
306
|
+
const symbol = byId.get(id);
|
|
307
|
+
return symbol === undefined || symbol.kind === "file" ? [] : nodes.typeNamesOf(symbol);
|
|
308
|
+
});
|
|
309
|
+
project.symbolsAt(named.map((node) => project.handle(node))).forEach((symbol, index) => {
|
|
310
|
+
if (symbol === UNANSWERED) {
|
|
311
|
+
for (const id of held.byName.get(named[index]?.getText() ?? "") ?? []) {
|
|
312
|
+
guessed.add(id);
|
|
313
|
+
reach(id);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
else if (symbol !== undefined) {
|
|
317
|
+
for (const target of chains.chainOf(symbol)) {
|
|
318
|
+
reach(target.id);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
});
|
|
302
322
|
}
|
|
303
|
-
return [...reached];
|
|
323
|
+
return { ids: [...reached], guessed: [...guessed] };
|
|
324
|
+
}
|
|
325
|
+
/** The type names declarations of the inventory write, each file walked once, when first asked. */
|
|
326
|
+
function declarationNodes(held, byPath) {
|
|
327
|
+
const walked = new Map();
|
|
328
|
+
const nodesIn = (path) => {
|
|
329
|
+
const known = walked.get(path);
|
|
330
|
+
if (known !== undefined) {
|
|
331
|
+
return known;
|
|
332
|
+
}
|
|
333
|
+
const found = { byId: new Map(), nodes: new Set() };
|
|
334
|
+
const file = byPath.get(path);
|
|
335
|
+
if (file !== undefined) {
|
|
336
|
+
const visit = (node) => {
|
|
337
|
+
const id = held.declarations.get(nodeKey(file, node));
|
|
338
|
+
if (id !== undefined) {
|
|
339
|
+
found.nodes.add(node);
|
|
340
|
+
if (!found.byId.has(id)) {
|
|
341
|
+
found.byId.set(id, node);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
node.forEachChild(visit);
|
|
345
|
+
};
|
|
346
|
+
visit(file);
|
|
347
|
+
}
|
|
348
|
+
walked.set(path, found);
|
|
349
|
+
return found;
|
|
350
|
+
};
|
|
351
|
+
return {
|
|
352
|
+
typeNamesOf: (symbol) => {
|
|
353
|
+
const { byId, nodes } = nodesIn(symbol.position.path);
|
|
354
|
+
const declaration = byId.get(symbol.id);
|
|
355
|
+
return declaration === undefined ? [] : typeNamesOf(declaration, nodes);
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* The names of types one declaration's own syntax writes: in a type reference, a
|
|
361
|
+
* heritage clause or a type query. A function body is not read, and neither is a
|
|
362
|
+
* declaration nested in this one, which `nested` holds and which is read on its own.
|
|
363
|
+
*/
|
|
364
|
+
function typeNamesOf(declaration, nested) {
|
|
365
|
+
const found = [];
|
|
366
|
+
const visit = (node) => {
|
|
367
|
+
if (node !== declaration && nested.has(node)) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (isTypeReferenceNode(node)) {
|
|
371
|
+
found.push(isQualifiedName(node.typeName) ? node.typeName.right : node.typeName);
|
|
372
|
+
}
|
|
373
|
+
else if (isExpressionWithTypeArguments(node)) {
|
|
374
|
+
found.push(isPropertyAccessExpression(node.expression) ? node.expression.name : node.expression);
|
|
375
|
+
}
|
|
376
|
+
else if (isTypeQueryNode(node)) {
|
|
377
|
+
found.push(isQualifiedName(node.exprName) ? node.exprName.right : node.exprName);
|
|
378
|
+
}
|
|
379
|
+
const body = isFunctionLikeDeclaration(node) ? node.body : undefined;
|
|
380
|
+
node.forEachChild((child) => {
|
|
381
|
+
if (child !== body) {
|
|
382
|
+
visit(child);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
};
|
|
386
|
+
visit(declaration);
|
|
387
|
+
return found;
|
|
304
388
|
}
|
|
305
389
|
/**
|
|
306
390
|
* Keeps every declaration the configuration names and returns every string that
|
package/dist/src/session.js
CHANGED
|
@@ -225,10 +225,10 @@ function projectsOf(snapshot, built, written) {
|
|
|
225
225
|
* snapshot of one client, and answers in their order. A configuration `programs` describes is
|
|
226
226
|
* built as that program, or opened from the configuration it writes, instead of as a project.
|
|
227
227
|
* One the snapshot holds nothing for ends the run under `"refuse"` and is named in the result
|
|
228
|
-
* under `"record"`.
|
|
229
|
-
* after it, whether the visit completed or threw.
|
|
228
|
+
* under `"record"`. `last` runs after the last visit while the client still answers; the
|
|
229
|
+
* snapshot is disposed and the client released after it, whether the visit completed or threw.
|
|
230
230
|
*/
|
|
231
|
-
export function runSession(engine, configFiles, visit, unopened = "refuse", programs = new Map()) {
|
|
231
|
+
export function runSession(engine, configFiles, visit, unopened = "refuse", programs = new Map(), last = () => undefined) {
|
|
232
232
|
try {
|
|
233
233
|
const built = [];
|
|
234
234
|
const params = [];
|
|
@@ -262,6 +262,7 @@ export function runSession(engine, configFiles, visit, unopened = "refuse", prog
|
|
|
262
262
|
}
|
|
263
263
|
projects.push(visit(viewOf(project, configFile, engine, log)));
|
|
264
264
|
}
|
|
265
|
+
last();
|
|
265
266
|
return {
|
|
266
267
|
projects,
|
|
267
268
|
unopened: missing,
|
|
@@ -87,12 +87,69 @@ function stringEnd(text, from) {
|
|
|
87
87
|
}
|
|
88
88
|
return at + 1;
|
|
89
89
|
}
|
|
90
|
-
/** The
|
|
91
|
-
function
|
|
90
|
+
/** The end of the `${` substitution whose expression starts at `from`, past its closing brace. */
|
|
91
|
+
function substitutionEnd(text, from) {
|
|
92
|
+
let depth = 1;
|
|
93
|
+
let at = from;
|
|
94
|
+
while (at < text.length) {
|
|
95
|
+
const char = text[at];
|
|
96
|
+
if (char === '"' || char === "'") {
|
|
97
|
+
at = stringEnd(text, at);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (char === "`") {
|
|
101
|
+
at = templateEnd(text, at, () => undefined);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (char === "{") {
|
|
105
|
+
depth += 1;
|
|
106
|
+
}
|
|
107
|
+
else if (char === "}") {
|
|
108
|
+
depth -= 1;
|
|
109
|
+
if (depth === 0) {
|
|
110
|
+
return at + 1;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
at += 1;
|
|
114
|
+
}
|
|
115
|
+
return at;
|
|
116
|
+
}
|
|
117
|
+
/** The end of the template literal at `from`; each `${…}` expression goes to `substitution`. */
|
|
118
|
+
function templateEnd(text, from, substitution) {
|
|
119
|
+
let at = from + 1;
|
|
120
|
+
while (at < text.length && text[at] !== "`") {
|
|
121
|
+
if (text[at] === "\\") {
|
|
122
|
+
at += 2;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (text[at] === "$" && text[at + 1] === "{") {
|
|
126
|
+
const start = at + 2;
|
|
127
|
+
const end = substitutionEnd(text, start);
|
|
128
|
+
substitution(text.slice(start, end - 1), start);
|
|
129
|
+
at = end;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
at += 1;
|
|
133
|
+
}
|
|
134
|
+
return at + 1;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The identifiers one action's inner text writes outside a quoted string, with their offsets
|
|
138
|
+
* in it. In `script` text, a component's markup, a template literal's substitutions are code.
|
|
139
|
+
*/
|
|
140
|
+
function identifiersOf(inner, script = false) {
|
|
92
141
|
const found = [];
|
|
93
142
|
let at = 0;
|
|
94
143
|
while (at < inner.length) {
|
|
95
144
|
const char = inner[at] ?? "";
|
|
145
|
+
if (char === "`" && script) {
|
|
146
|
+
at = templateEnd(inner, at, (expression, offset) => {
|
|
147
|
+
for (const one of identifiersOf(expression, true)) {
|
|
148
|
+
found.push({ name: one.name, offset: offset + one.offset });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
96
153
|
if (char === '"' || char === "'" || char === "`") {
|
|
97
154
|
at = stringEnd(inner, at);
|
|
98
155
|
continue;
|
|
@@ -142,7 +199,7 @@ function namesOf(text, delimiters) {
|
|
|
142
199
|
}
|
|
143
200
|
/** Every identifier the markup of one component file writes in an action. */
|
|
144
201
|
function markupNamesOf(text) {
|
|
145
|
-
return readComponent(text).markup.flatMap((one) => identifiersOf(one.text).map(({ name, offset }) => ({
|
|
202
|
+
return readComponent(text).markup.flatMap((one) => identifiersOf(one.text, true).map(({ name, offset }) => ({
|
|
146
203
|
name,
|
|
147
204
|
offset: one.start + offset,
|
|
148
205
|
action: one.action,
|
package/jsr.json
CHANGED