@eventra_dev/eventra-cli 2.3.0 → 2.3.2
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/analysis/extractor/extractor.js +2 -1
- package/dist/analysis/resolver/callResolver.js +64 -2
- package/dist/analysis/sdk/isEventraTrackCall.js +79 -3
- package/dist/analysis/shared/utils.js +10 -5
- package/dist/analysis/symbols/symbolUtils.js +39 -0
- package/dist/cli/sync.js +4 -1
- package/dist/cli/watch.js +2 -0
- package/dist/plugin/builtins/consoleDynamicEventReporter.js +2 -1
- package/dist/plugin/registry.js +5 -8
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@ const typescript_1 = __importDefault(require("typescript"));
|
|
|
8
8
|
const resolver_1 = require("../resolver/resolver");
|
|
9
9
|
const evaluationContext_1 = require("../shared/evaluationContext");
|
|
10
10
|
const propagationExtractor_1 = require("./propagationExtractor");
|
|
11
|
+
const symbolUtils_1 = require("../symbols/symbolUtils");
|
|
11
12
|
const eventValidation_1 = require("../shared/eventValidation");
|
|
12
13
|
const dynamicOccurrence_1 = require("../shared/dynamicOccurrence");
|
|
13
14
|
function extractEvents(index, checker, _config, evaluationCache, exportCache, resolvedCallCache, returnPropagationCache, wrapperRegistry) {
|
|
@@ -38,7 +39,7 @@ function extractEvents(index, checker, _config, evaluationCache, exportCache, re
|
|
|
38
39
|
visit(index.sourceFile);
|
|
39
40
|
return {
|
|
40
41
|
events,
|
|
41
|
-
detectedFunctionWrappers: new Set(index.wrappers.map(wrapper => wrapper.symbol
|
|
42
|
+
detectedFunctionWrappers: new Set(index.wrappers.map(wrapper => (0, symbolUtils_1.wrapperName)(wrapper.declaration, wrapper.symbol))),
|
|
42
43
|
dynamicOccurrences,
|
|
43
44
|
};
|
|
44
45
|
}
|
|
@@ -6,9 +6,60 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.resolveFunctionFromCall = resolveFunctionFromCall;
|
|
7
7
|
const typescript_1 = __importDefault(require("typescript"));
|
|
8
8
|
const exportResolver_1 = require("./exportResolver");
|
|
9
|
+
const utils_1 = require("../shared/utils");
|
|
9
10
|
function isFunctionLike(node) {
|
|
10
11
|
return (typescript_1.default.isFunctionDeclaration(node) || typescript_1.default.isMethodDeclaration(node) || typescript_1.default.isArrowFunction(node) || typescript_1.default.isFunctionExpression(node));
|
|
11
12
|
}
|
|
13
|
+
/** `return expr` statements of a function body, not looking into functions nested in it. */
|
|
14
|
+
function returnedExpressions(fn) {
|
|
15
|
+
if (!fn.body) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
if (!typescript_1.default.isBlock(fn.body)) {
|
|
19
|
+
return [fn.body];
|
|
20
|
+
}
|
|
21
|
+
const found = [];
|
|
22
|
+
const visit = (node) => {
|
|
23
|
+
if (typescript_1.default.isFunctionLike(node)) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (typescript_1.default.isReturnStatement(node) && node.expression) {
|
|
27
|
+
found.push(node.expression);
|
|
28
|
+
}
|
|
29
|
+
typescript_1.default.forEachChild(node, visit);
|
|
30
|
+
};
|
|
31
|
+
visit(fn.body);
|
|
32
|
+
return found;
|
|
33
|
+
}
|
|
34
|
+
const MAX_RETURN_HOPS = 4;
|
|
35
|
+
/**
|
|
36
|
+
* The function a call hands back: `useTracker()` for `function useTracker() { return (n) => t.track(n) }`,
|
|
37
|
+
* where the returned value is a function expression or a name bound to one.
|
|
38
|
+
*/
|
|
39
|
+
function resolveReturnedFunction(call, checker, callCache, exportCache, hops) {
|
|
40
|
+
if (hops > MAX_RETURN_HOPS) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const factory = resolveFunctionFromCall(call.expression, checker, callCache, exportCache, hops + 1);
|
|
44
|
+
if (!factory) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
for (const returned of returnedExpressions(factory)) {
|
|
48
|
+
const value = (0, utils_1.unwrapExpression)(returned);
|
|
49
|
+
if (isFunctionLike(value)) {
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
if (typescript_1.default.isIdentifier(value)) {
|
|
53
|
+
for (const next of checker.getSymbolAtLocation(value)?.getDeclarations() ?? []) {
|
|
54
|
+
const resolved = resolveDeclaration(next, checker);
|
|
55
|
+
if (resolved) {
|
|
56
|
+
return resolved;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
12
63
|
function resolveDeclaration(declaration, checker) {
|
|
13
64
|
// function foo() {}
|
|
14
65
|
if (isFunctionLike(declaration)) {
|
|
@@ -64,7 +115,15 @@ function getExpressionSymbol(expression, checker) {
|
|
|
64
115
|
// track()
|
|
65
116
|
return checker.getSymbolAtLocation(expression);
|
|
66
117
|
}
|
|
67
|
-
function resolveFunctionFromCall(expression, checker, callCache, exportCache) {
|
|
118
|
+
function resolveFunctionFromCall(expression, checker, callCache, exportCache, hops = 0) {
|
|
119
|
+
// useTracker()("x")
|
|
120
|
+
let callee = expression;
|
|
121
|
+
while (typescript_1.default.isParenthesizedExpression(callee)) {
|
|
122
|
+
callee = callee.expression;
|
|
123
|
+
}
|
|
124
|
+
if (typescript_1.default.isCallExpression(callee)) {
|
|
125
|
+
return resolveReturnedFunction(callee, checker, callCache, exportCache, hops);
|
|
126
|
+
}
|
|
68
127
|
let symbol = getExpressionSymbol(expression, checker);
|
|
69
128
|
if (!symbol) {
|
|
70
129
|
return null;
|
|
@@ -83,7 +142,10 @@ function resolveFunctionFromCall(expression, checker, callCache, exportCache) {
|
|
|
83
142
|
// declaration resolution
|
|
84
143
|
const declarations = symbol.getDeclarations() ?? [];
|
|
85
144
|
for (const declaration of declarations) {
|
|
86
|
-
const
|
|
145
|
+
// const track = useTracker(); track("x")
|
|
146
|
+
const resolved = resolveDeclaration(declaration, checker) ?? (typescript_1.default.isVariableDeclaration(declaration) && declaration.initializer && typescript_1.default.isCallExpression(declaration.initializer)
|
|
147
|
+
? resolveReturnedFunction(declaration.initializer, checker, callCache, exportCache, hops)
|
|
148
|
+
: null);
|
|
87
149
|
if (!resolved) {
|
|
88
150
|
continue;
|
|
89
151
|
}
|
|
@@ -91,7 +91,12 @@ function isEventraNewExpression(node, checker) {
|
|
|
91
91
|
return false;
|
|
92
92
|
}
|
|
93
93
|
const symbol = checker.getSymbolAtLocation(expr);
|
|
94
|
-
|
|
94
|
+
if (symbol && isEventraSymbol(symbol, checker)) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
// `const { Eventra } = await import("@eventra_dev/eventra-sdk")`: the binding is not an import, but
|
|
98
|
+
// the instance it builds is still typed as the SDK's class
|
|
99
|
+
return typeLooksLikeEventra(checker.getTypeAtLocation(node), checker);
|
|
95
100
|
}
|
|
96
101
|
function typeLooksLikeEventra(type, checker, depth = 0) {
|
|
97
102
|
// cycle guard - TypeScript flattens union/intersection types (a literal
|
|
@@ -115,7 +120,63 @@ function typeLooksLikeEventra(type, checker, depth = 0) {
|
|
|
115
120
|
}
|
|
116
121
|
return false;
|
|
117
122
|
}
|
|
118
|
-
|
|
123
|
+
const MAX_RECEIVER_HOPS = 6;
|
|
124
|
+
/** `return expr` statements of a function body, not looking into functions nested in it. */
|
|
125
|
+
function returnedExpressions(fn) {
|
|
126
|
+
if (!fn.body) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
if (!typescript_1.default.isBlock(fn.body)) {
|
|
130
|
+
return [fn.body];
|
|
131
|
+
}
|
|
132
|
+
const found = [];
|
|
133
|
+
const visit = (node) => {
|
|
134
|
+
if (typescript_1.default.isFunctionLike(node)) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (typescript_1.default.isReturnStatement(node) && node.expression) {
|
|
138
|
+
found.push(node.expression);
|
|
139
|
+
}
|
|
140
|
+
typescript_1.default.forEachChild(node, visit);
|
|
141
|
+
};
|
|
142
|
+
visit(fn.body);
|
|
143
|
+
return found;
|
|
144
|
+
}
|
|
145
|
+
function functionOf(decl) {
|
|
146
|
+
if (typescript_1.default.isFunctionDeclaration(decl) || typescript_1.default.isMethodDeclaration(decl)) {
|
|
147
|
+
return decl;
|
|
148
|
+
}
|
|
149
|
+
if (typescript_1.default.isVariableDeclaration(decl) && decl.initializer && (typescript_1.default.isArrowFunction(decl.initializer) || typescript_1.default.isFunctionExpression(decl.initializer))) {
|
|
150
|
+
return decl.initializer;
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
/** `t = new Eventra(...)` anywhere in the file of a variable's declaration: a lazily filled `let t: any`. */
|
|
155
|
+
function assignedValues(symbol, checker) {
|
|
156
|
+
const values = [];
|
|
157
|
+
const files = new Set((symbol.getDeclarations() ?? []).map((decl) => decl.getSourceFile()));
|
|
158
|
+
for (const file of files) {
|
|
159
|
+
const visit = (node) => {
|
|
160
|
+
if (typescript_1.default.isBinaryExpression(node) &&
|
|
161
|
+
node.operatorToken.kind === typescript_1.default.SyntaxKind.EqualsToken &&
|
|
162
|
+
typescript_1.default.isIdentifier(node.left) &&
|
|
163
|
+
checker.getSymbolAtLocation(node.left) === symbol) {
|
|
164
|
+
values.push(node.right);
|
|
165
|
+
}
|
|
166
|
+
typescript_1.default.forEachChild(node, visit);
|
|
167
|
+
};
|
|
168
|
+
visit(file);
|
|
169
|
+
}
|
|
170
|
+
return values;
|
|
171
|
+
}
|
|
172
|
+
function isEventraReceiverExpression(receiver, checker, hops = 0) {
|
|
173
|
+
if (hops > MAX_RECEIVER_HOPS) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
// `(await getTracker()).track(...)`, `(tracker as Eventra).track(...)`, `tracker!.track(...)`
|
|
177
|
+
if (typescript_1.default.isParenthesizedExpression(receiver) || typescript_1.default.isAwaitExpression(receiver) || typescript_1.default.isNonNullExpression(receiver) || typescript_1.default.isAsExpression(receiver)) {
|
|
178
|
+
return isEventraReceiverExpression(receiver.expression, checker, hops + 1);
|
|
179
|
+
}
|
|
119
180
|
if (typescript_1.default.isNewExpression(receiver)) {
|
|
120
181
|
return isEventraNewExpression(receiver, checker);
|
|
121
182
|
}
|
|
@@ -126,6 +187,18 @@ function isEventraReceiverExpression(receiver, checker) {
|
|
|
126
187
|
if (typeLooksLikeEventra(type, checker)) {
|
|
127
188
|
return true;
|
|
128
189
|
}
|
|
190
|
+
// `getTracker().track(...)` where the getter's type is lost (`any`) but what it returns is a tracker
|
|
191
|
+
if (typescript_1.default.isCallExpression(receiver)) {
|
|
192
|
+
const callee = typescript_1.default.isIdentifier(receiver.expression) || typescript_1.default.isPropertyAccessExpression(receiver.expression)
|
|
193
|
+
? checker.getSymbolAtLocation(typescript_1.default.isIdentifier(receiver.expression) ? receiver.expression : receiver.expression.name)
|
|
194
|
+
: undefined;
|
|
195
|
+
for (const decl of callee?.getDeclarations() ?? []) {
|
|
196
|
+
const fn = functionOf(decl);
|
|
197
|
+
if (fn && returnedExpressions(fn).some((returned) => isEventraReceiverExpression(returned, checker, hops + 1))) {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
129
202
|
if (typescript_1.default.isIdentifier(receiver)) {
|
|
130
203
|
const symbol = checker.getSymbolAtLocation(receiver);
|
|
131
204
|
if (!symbol) {
|
|
@@ -134,6 +207,9 @@ function isEventraReceiverExpression(receiver, checker) {
|
|
|
134
207
|
// See the identical `?? []` above isEventraSymbol's declaration loop: a
|
|
135
208
|
// real ts.Symbol here always has a declarations array.
|
|
136
209
|
/* v8 ignore next */
|
|
210
|
+
if (assignedValues(symbol, checker).some((value) => isEventraReceiverExpression(value, checker, hops + 1))) {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
137
213
|
for (const decl of symbol.getDeclarations() ?? []) {
|
|
138
214
|
if (typescript_1.default.isVariableDeclaration(decl) && decl.initializer) {
|
|
139
215
|
if (typescript_1.default.isNewExpression(decl.initializer) && isEventraNewExpression(decl.initializer, checker)) {
|
|
@@ -167,7 +243,7 @@ function isEventraReceiverExpression(receiver, checker) {
|
|
|
167
243
|
}
|
|
168
244
|
}
|
|
169
245
|
if (typescript_1.default.isPropertyAccessExpression(receiver) || typescript_1.default.isPropertyAccessChain(receiver)) {
|
|
170
|
-
return isEventraReceiverExpression(receiver.expression, checker);
|
|
246
|
+
return isEventraReceiverExpression(receiver.expression, checker, hops + 1);
|
|
171
247
|
}
|
|
172
248
|
return false;
|
|
173
249
|
}
|
|
@@ -4,21 +4,26 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.isExpressionWrapper = isExpressionWrapper;
|
|
7
8
|
exports.unwrapExpression = unwrapExpression;
|
|
8
9
|
exports.getPropertyName = getPropertyName;
|
|
9
10
|
exports.getCallName = getCallName;
|
|
10
11
|
exports.getFunctionName = getFunctionName;
|
|
11
12
|
const typescript_1 = __importDefault(require("typescript"));
|
|
13
|
+
/** A node that only wraps an expression without changing what it is: `(x)`, `x as T`, `<T>x`, `x!`, `x satisfies T`. */
|
|
14
|
+
function isExpressionWrapper(node) {
|
|
15
|
+
return (typescript_1.default.isAsExpression(node) ||
|
|
16
|
+
typescript_1.default.isTypeAssertionExpression(node) ||
|
|
17
|
+
typescript_1.default.isNonNullExpression(node) ||
|
|
18
|
+
typescript_1.default.isParenthesizedExpression(node) ||
|
|
19
|
+
typescript_1.default.isSatisfiesExpression(node));
|
|
20
|
+
}
|
|
12
21
|
// track(payload?.event as string)
|
|
13
22
|
// track((payload.event))
|
|
14
23
|
// track(payload!.event)
|
|
15
24
|
function unwrapExpression(expression) {
|
|
16
25
|
let current = expression;
|
|
17
|
-
while (
|
|
18
|
-
typescript_1.default.isTypeAssertionExpression(current) ||
|
|
19
|
-
typescript_1.default.isNonNullExpression(current) ||
|
|
20
|
-
typescript_1.default.isParenthesizedExpression(current) ||
|
|
21
|
-
typescript_1.default.isSatisfiesExpression(current)) {
|
|
26
|
+
while (isExpressionWrapper(current)) {
|
|
22
27
|
current = current.expression;
|
|
23
28
|
}
|
|
24
29
|
return current;
|
|
@@ -5,7 +5,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.getValueSymbol = getValueSymbol;
|
|
7
7
|
exports.getFunctionSymbol = getFunctionSymbol;
|
|
8
|
+
exports.isReturnedFunction = isReturnedFunction;
|
|
9
|
+
exports.wrapperName = wrapperName;
|
|
8
10
|
const typescript_1 = __importDefault(require("typescript"));
|
|
11
|
+
const utils_1 = require("../shared/utils");
|
|
9
12
|
/**
|
|
10
13
|
* Symbol an identifier *refers to*. For the name in a shorthand property (`{ event }`) a plain
|
|
11
14
|
* `getSymbolAtLocation` yields the property's own symbol, not the variable/parameter it reads.
|
|
@@ -53,5 +56,41 @@ function getFunctionSymbol(node, checker) {
|
|
|
53
56
|
if (typescript_1.default.isExportAssignment(parent)) {
|
|
54
57
|
return checker.getSymbolAtLocation(node);
|
|
55
58
|
}
|
|
59
|
+
// `return (name) => ...` / `() => (name) => ...`: a function a hook or factory hands back has no
|
|
60
|
+
// name of its own, but the binder still gives it a symbol, and that is what a call of the result
|
|
61
|
+
// (`const track = useTracker(); track("x")`) is matched against.
|
|
62
|
+
if (isReturnedFunction(node)) {
|
|
63
|
+
return node.symbol;
|
|
64
|
+
}
|
|
56
65
|
return undefined;
|
|
57
66
|
}
|
|
67
|
+
/** An arrow/function expression that is the value of a `return`, or the concise body of an arrow, possibly in parentheses, `as` or `!`. */
|
|
68
|
+
function isReturnedFunction(node) {
|
|
69
|
+
if (!typescript_1.default.isArrowFunction(node) && !typescript_1.default.isFunctionExpression(node)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
// `inner` is the outermost wrapper around the function, or the function itself
|
|
73
|
+
let inner = node;
|
|
74
|
+
let outer = node.parent;
|
|
75
|
+
while ((0, utils_1.isExpressionWrapper)(outer)) {
|
|
76
|
+
inner = outer;
|
|
77
|
+
outer = outer.parent;
|
|
78
|
+
}
|
|
79
|
+
return typescript_1.default.isReturnStatement(outer) || (typescript_1.default.isArrowFunction(outer) && outer.body === inner);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The name a wrapper is reported under: its own, or for an anonymous returned function the hook that
|
|
83
|
+
* returns it (`useTracker` for `function useTracker() { return (n) => t.track(n); }`).
|
|
84
|
+
*/
|
|
85
|
+
function wrapperName(declaration, symbol) {
|
|
86
|
+
if (!isReturnedFunction(declaration)) {
|
|
87
|
+
return symbol.getName();
|
|
88
|
+
}
|
|
89
|
+
const owner = typescript_1.default.findAncestor(declaration.parent, (n) => typescript_1.default.isFunctionLike(n));
|
|
90
|
+
const ownerName = owner && owner.name;
|
|
91
|
+
if (ownerName && typescript_1.default.isIdentifier(ownerName)) {
|
|
92
|
+
return ownerName.text;
|
|
93
|
+
}
|
|
94
|
+
const holder = owner?.parent;
|
|
95
|
+
return holder && typescript_1.default.isVariableDeclaration(holder) && typescript_1.default.isIdentifier(holder.name) ? holder.name.text : symbol.getName();
|
|
96
|
+
}
|
package/dist/cli/sync.js
CHANGED
|
@@ -18,7 +18,10 @@ async function sync({ strict = false } = {}) {
|
|
|
18
18
|
// report what was saved: with skipped files that is the union of the known and
|
|
19
19
|
// the found events, not just what this scan found
|
|
20
20
|
const saved = await (0, scanResults_1.persistScanResults)(config, engine, { preserveKnown: skipped.length > 0 });
|
|
21
|
-
|
|
21
|
+
// normalizeConfig always fills functionWrappers; the type only marks it optional
|
|
22
|
+
/* v8 ignore next */
|
|
23
|
+
const wrapperCount = saved.functionWrappers?.length ?? 0;
|
|
24
|
+
console.log(chalk_1.default.green(`Found ${(0, skipped_1.plural)(saved.events.length, "event")}, ${(0, skipped_1.plural)(wrapperCount, "function wrapper")}`));
|
|
22
25
|
await plugins.runDynamicEventReporters(engine.getAllDynamicOccurrences());
|
|
23
26
|
(0, skipped_1.reportSkipped)(skipped);
|
|
24
27
|
(0, skipped_1.reportIgnored)(ignored);
|
package/dist/cli/watch.js
CHANGED
|
@@ -221,6 +221,8 @@ async function watch() {
|
|
|
221
221
|
.on("add", schedule)
|
|
222
222
|
.on("unlink", (file) => {
|
|
223
223
|
const abs = normalize(file);
|
|
224
|
+
// chokidar's `ignored` already drops excluded paths, so this only guards against a stray event
|
|
225
|
+
/* v8 ignore next */
|
|
224
226
|
if (filter.isExcluded(abs))
|
|
225
227
|
return;
|
|
226
228
|
// a file that failed to analyse is in no set but is still remembered as skipped
|
|
@@ -9,7 +9,8 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
// paths are printed relative to where the command runs, with forward slashes
|
|
10
10
|
function displayPath(fileName) {
|
|
11
11
|
const relative = path_1.default.relative(process.cwd(), fileName);
|
|
12
|
-
|
|
12
|
+
// path.relative always joins with path.sep, so that is the only separator to look for
|
|
13
|
+
const outside = relative === ".." || relative.startsWith(`..${path_1.default.sep}`);
|
|
13
14
|
const shown = relative && !outside && !path_1.default.isAbsolute(relative) ? relative : fileName;
|
|
14
15
|
return shown.replace(/\\/g, "/");
|
|
15
16
|
}
|
package/dist/plugin/registry.js
CHANGED
|
@@ -23,9 +23,9 @@ function templateRegionOf(content) {
|
|
|
23
23
|
}
|
|
24
24
|
// `__eventra_x_template_event__(expr)` (callText may be cut with an ellipsis)
|
|
25
25
|
function templateExpression(occurrence) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
expression =
|
|
26
|
+
// no "(" gives indexOf -1, so slice(0) keeps the whole text
|
|
27
|
+
const afterOpen = occurrence.callText.slice(occurrence.callText.indexOf("(") + 1);
|
|
28
|
+
const expression = afterOpen.endsWith(")") ? afterOpen.slice(0, -1) : afterOpen;
|
|
29
29
|
// Angular qualifies a bare name with `this.`; the template never writes it
|
|
30
30
|
return occurrence.calleeText.includes("_angular_") ? expression.replace(/^this\./, "") : expression;
|
|
31
31
|
}
|
|
@@ -86,11 +86,8 @@ class PluginRegistry {
|
|
|
86
86
|
}
|
|
87
87
|
/** Disk paths produced by the last preprocess run for a source file (for watch unlink). */
|
|
88
88
|
getVirtualPathsForSource(fileName) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return mapped;
|
|
92
|
-
}
|
|
93
|
-
return [this.normalizePath(fileName)];
|
|
89
|
+
// a source is only recorded with at least one virtual file, so a hit is never empty
|
|
90
|
+
return this.sourceToVirtuals.get(this.normalizePath(fileName)) ?? [this.normalizePath(fileName)];
|
|
94
91
|
}
|
|
95
92
|
async preprocessFile(input) {
|
|
96
93
|
const sourceKey = this.normalizePath(input.fileName);
|