@tinkerlandai/indiana-iam 0.3.0 → 0.4.0
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 +56 -3
- package/assets/document-controller.js +18 -1
- package/assets/document-runtime.js +189 -1
- package/assets/viewer.js +19 -5
- package/package.json +5 -13
- package/src/analyze.mjs +799 -109
- package/src/build-options.d.ts +1 -1
- package/src/build-runtime.mjs +4 -1
- package/src/build-styles.mjs +49 -6
- package/src/build.mjs +3 -3
- package/src/component-registry.mjs +229 -0
- package/src/dependencies.mjs +2 -2
- package/src/framework-routes.mjs +3 -1
- package/src/generate.mjs +3 -1
- package/src/graph-sources.mjs +54 -0
- package/src/index.d.ts +2 -0
- package/src/library-entry.mjs +4 -0
- package/src/lucide-source.mjs +18 -0
- package/src/next-loader.cjs +7 -1
- package/src/next.cjs +5 -0
- package/src/node-hashes.mjs +9 -8
- package/src/route-lazy.mjs +80 -0
- package/src/route-tree.mjs +15 -3
- package/src/source-attributes.mjs +24 -0
- package/src/source-charts.mjs +38 -0
- package/src/source-collections.mjs +241 -0
- package/src/source-environment.mjs +26 -0
- package/src/source-media.mjs +21 -0
- package/src/source-query-evidence.mjs +64 -0
- package/src/source-redirects.mjs +388 -0
- package/src/static-primitives.mjs +48 -0
- package/src/static-query.mjs +39 -11
- package/src/tab-scenes.mjs +214 -0
- package/src/ui-preview.mjs +225 -0
- package/src/ui-state-machine.mjs +76 -0
- package/src/ui-structure.mjs +645 -0
- package/src/validate.mjs +5 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { uiRelation } from "./ui-structure.mjs";
|
|
2
|
+
|
|
3
|
+
/** Bounded source dataflow: current location -> query parameter -> Navigate query consumer.
|
|
4
|
+
* This records conditional UI relations only. It never runs guards, helpers or authentication. */
|
|
5
|
+
export function sourceRedirects({
|
|
6
|
+
ts,
|
|
7
|
+
modules,
|
|
8
|
+
binding,
|
|
9
|
+
functionFrom,
|
|
10
|
+
externalImport,
|
|
11
|
+
sourceLocation,
|
|
12
|
+
constant,
|
|
13
|
+
pages,
|
|
14
|
+
components,
|
|
15
|
+
documents,
|
|
16
|
+
}) {
|
|
17
|
+
const router = (ref) =>
|
|
18
|
+
["react-router", "react-router-dom"].includes(ref?.specifier);
|
|
19
|
+
const unknown = Symbol("unresolved redirect");
|
|
20
|
+
const unwrap = (n) => {
|
|
21
|
+
while (
|
|
22
|
+
n &&
|
|
23
|
+
(ts.isParenthesizedExpression(n) ||
|
|
24
|
+
ts.isAsExpression(n) ||
|
|
25
|
+
ts.isNonNullExpression(n) ||
|
|
26
|
+
ts.isTypeAssertionExpression(n))
|
|
27
|
+
)
|
|
28
|
+
n = n.expression;
|
|
29
|
+
return n;
|
|
30
|
+
};
|
|
31
|
+
const score = (v) =>
|
|
32
|
+
v?.kind === "url"
|
|
33
|
+
? 5
|
|
34
|
+
: v?.kind === "query"
|
|
35
|
+
? 4
|
|
36
|
+
: v?.kind === "origin"
|
|
37
|
+
? 3
|
|
38
|
+
: typeof v === "string"
|
|
39
|
+
? 1
|
|
40
|
+
: 0;
|
|
41
|
+
const choose = (values) =>
|
|
42
|
+
values
|
|
43
|
+
.filter((v) => v !== unknown && v !== undefined && v !== null)
|
|
44
|
+
.sort((a, b) => score(b) - score(a))[0] ?? unknown;
|
|
45
|
+
const definitions = new Map();
|
|
46
|
+
let budget = 0;
|
|
47
|
+
function value(raw, module, env, refs, depth = 0) {
|
|
48
|
+
const node = unwrap(raw);
|
|
49
|
+
if (!node || depth > 16 || ++budget > 20000) return unknown;
|
|
50
|
+
const next = (n) => value(n, module, env, refs, depth + 1);
|
|
51
|
+
if (ts.isIdentifier(node)) {
|
|
52
|
+
if (env.has(node.text)) return env.get(node.text);
|
|
53
|
+
const c = constant(node, module);
|
|
54
|
+
return c === undefined ? unknown : c;
|
|
55
|
+
}
|
|
56
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
|
|
57
|
+
return node.text;
|
|
58
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
59
|
+
const parent = next(node.expression),
|
|
60
|
+
key = node.name.text;
|
|
61
|
+
if (
|
|
62
|
+
parent?.kind === "location" &&
|
|
63
|
+
["pathname", "search", "hash"].includes(key)
|
|
64
|
+
)
|
|
65
|
+
return { kind: "origin", pathname: key === "pathname" };
|
|
66
|
+
if (parent && typeof parent === "object" && Object.hasOwn(parent, key))
|
|
67
|
+
return parent[key];
|
|
68
|
+
const c = constant(node, module);
|
|
69
|
+
return c === undefined ? unknown : c;
|
|
70
|
+
}
|
|
71
|
+
if (ts.isObjectLiteralExpression(node))
|
|
72
|
+
return Object.fromEntries(
|
|
73
|
+
node.properties.flatMap((p) =>
|
|
74
|
+
ts.isShorthandPropertyAssignment(p)
|
|
75
|
+
? [[p.name.text, next(p.name)]]
|
|
76
|
+
: ts.isPropertyAssignment(p)
|
|
77
|
+
? [[p.name.text, next(p.initializer)]]
|
|
78
|
+
: [],
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
if (ts.isConditionalExpression(node))
|
|
82
|
+
return choose([next(node.whenTrue), next(node.whenFalse)]);
|
|
83
|
+
if (
|
|
84
|
+
ts.isBinaryExpression(node) &&
|
|
85
|
+
[ts.SyntaxKind.BarBarToken, ts.SyntaxKind.QuestionQuestionToken].includes(
|
|
86
|
+
node.operatorToken.kind,
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
return choose([next(node.left), next(node.right)]);
|
|
90
|
+
if (
|
|
91
|
+
ts.isNewExpression(node) &&
|
|
92
|
+
ts.isIdentifier(node.expression) &&
|
|
93
|
+
node.expression.text === "URLSearchParams" &&
|
|
94
|
+
!node.arguments?.length
|
|
95
|
+
)
|
|
96
|
+
return { kind: "params", entries: new Map() };
|
|
97
|
+
if (ts.isTemplateExpression(node)) {
|
|
98
|
+
const parts = [node.head.text];
|
|
99
|
+
for (const span of node.templateSpans)
|
|
100
|
+
parts.push(next(span.expression), span.literal.text);
|
|
101
|
+
const dynamic = parts.filter((p) => typeof p !== "string");
|
|
102
|
+
if (
|
|
103
|
+
dynamic.length &&
|
|
104
|
+
dynamic.every((p) => p?.kind === "origin") &&
|
|
105
|
+
dynamic.some((p) => p.pathname) &&
|
|
106
|
+
parts.filter((p) => typeof p === "string").every((p) => p === "")
|
|
107
|
+
)
|
|
108
|
+
return { kind: "origin", pathname: true };
|
|
109
|
+
if (dynamic.length === 1 && dynamic[0]?.kind === "querystring") {
|
|
110
|
+
const index = parts.indexOf(dynamic[0]),
|
|
111
|
+
prefix = parts.slice(0, index).join(""),
|
|
112
|
+
suffix = parts.slice(index + 1).join("");
|
|
113
|
+
if (prefix.endsWith("?") && suffix === "")
|
|
114
|
+
return {
|
|
115
|
+
kind: "url",
|
|
116
|
+
path: prefix.slice(0, -1),
|
|
117
|
+
entries: dynamic[0].entries,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return dynamic.length ? unknown : parts.join("");
|
|
121
|
+
}
|
|
122
|
+
if (ts.isCallExpression(node)) {
|
|
123
|
+
if (ts.isIdentifier(node.expression)) {
|
|
124
|
+
const primitive = externalImport(module, node.expression.text);
|
|
125
|
+
if (router(primitive) && primitive.name === "useLocation")
|
|
126
|
+
return { kind: "location" };
|
|
127
|
+
if (router(primitive) && primitive.name === "useSearchParams")
|
|
128
|
+
return { kind: "search-hook" };
|
|
129
|
+
const ref = binding(module, node.expression.text),
|
|
130
|
+
fn = ref && functionFrom(ref.node);
|
|
131
|
+
if (
|
|
132
|
+
fn &&
|
|
133
|
+
!fn.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) &&
|
|
134
|
+
fn.parameters.every((p) => ts.isIdentifier(p.name))
|
|
135
|
+
) {
|
|
136
|
+
refs.set(
|
|
137
|
+
ref.module.file + "#" + ref.name,
|
|
138
|
+
sourceLocation(ref.module, fn, ref.name),
|
|
139
|
+
);
|
|
140
|
+
const local = new Map(
|
|
141
|
+
fn.parameters.map((p, i) => [p.name.text, next(node.arguments[i])]),
|
|
142
|
+
);
|
|
143
|
+
const returns = [];
|
|
144
|
+
scanBody(fn.body, ref.module, local, refs, returns, null, depth + 1);
|
|
145
|
+
return choose(returns);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (ts.isPropertyAccessExpression(node.expression)) {
|
|
149
|
+
const parent = next(node.expression.expression),
|
|
150
|
+
method = node.expression.name.text;
|
|
151
|
+
if (
|
|
152
|
+
parent?.kind === "search" &&
|
|
153
|
+
method === "get" &&
|
|
154
|
+
node.arguments.length === 1
|
|
155
|
+
) {
|
|
156
|
+
const key = next(node.arguments[0]);
|
|
157
|
+
return typeof key === "string"
|
|
158
|
+
? { kind: "query", parameter: key }
|
|
159
|
+
: unknown;
|
|
160
|
+
}
|
|
161
|
+
if (
|
|
162
|
+
parent?.kind === "params" &&
|
|
163
|
+
method === "set" &&
|
|
164
|
+
node.arguments.length === 2
|
|
165
|
+
) {
|
|
166
|
+
const key = next(node.arguments[0]),
|
|
167
|
+
v = next(node.arguments[1]);
|
|
168
|
+
if (typeof key === "string" && v !== unknown)
|
|
169
|
+
parent.entries.set(key, v);
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
if (parent?.kind === "params" && method === "toString")
|
|
173
|
+
return { kind: "querystring", entries: parent.entries };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const c = constant(node, module);
|
|
177
|
+
return c === undefined ? unknown : c;
|
|
178
|
+
}
|
|
179
|
+
function scanBody(
|
|
180
|
+
node,
|
|
181
|
+
module,
|
|
182
|
+
env,
|
|
183
|
+
refs,
|
|
184
|
+
returns,
|
|
185
|
+
onNavigate,
|
|
186
|
+
depth = 0,
|
|
187
|
+
condition = null,
|
|
188
|
+
) {
|
|
189
|
+
if (!node || depth > 24) return;
|
|
190
|
+
if (ts.isBlock(node) || ts.isSourceFile(node))
|
|
191
|
+
for (const statement of node.statements)
|
|
192
|
+
scanBody(
|
|
193
|
+
statement,
|
|
194
|
+
module,
|
|
195
|
+
env,
|
|
196
|
+
refs,
|
|
197
|
+
returns,
|
|
198
|
+
onNavigate,
|
|
199
|
+
depth + 1,
|
|
200
|
+
condition,
|
|
201
|
+
);
|
|
202
|
+
else if (ts.isVariableStatement(node))
|
|
203
|
+
for (const decl of node.declarationList.declarations) {
|
|
204
|
+
const v = value(decl.initializer, module, env, refs, depth);
|
|
205
|
+
if (ts.isIdentifier(decl.name)) env.set(decl.name.text, v);
|
|
206
|
+
else if (ts.isObjectBindingPattern(decl.name)) {
|
|
207
|
+
for (const field of decl.name.elements)
|
|
208
|
+
if (ts.isIdentifier(field.name)) {
|
|
209
|
+
const key = field.propertyName?.text || field.name.text;
|
|
210
|
+
env.set(
|
|
211
|
+
field.name.text,
|
|
212
|
+
v?.kind === "location" &&
|
|
213
|
+
["pathname", "search", "hash"].includes(key)
|
|
214
|
+
? { kind: "origin", pathname: key === "pathname" }
|
|
215
|
+
: v && typeof v === "object"
|
|
216
|
+
? (v[key] ?? unknown)
|
|
217
|
+
: unknown,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
} else if (
|
|
221
|
+
ts.isArrayBindingPattern(decl.name) &&
|
|
222
|
+
v?.kind === "search-hook"
|
|
223
|
+
) {
|
|
224
|
+
const name = decl.name.elements[0]?.name;
|
|
225
|
+
if (name && ts.isIdentifier(name))
|
|
226
|
+
env.set(name.text, { kind: "search" });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else if (ts.isIfStatement(node)) {
|
|
230
|
+
const label = node.expression.getText(module.source).slice(0, 160),
|
|
231
|
+
combine = (text) =>
|
|
232
|
+
condition ? condition + " && (" + text + ")" : text;
|
|
233
|
+
scanBody(
|
|
234
|
+
node.thenStatement,
|
|
235
|
+
module,
|
|
236
|
+
new Map(env),
|
|
237
|
+
refs,
|
|
238
|
+
returns,
|
|
239
|
+
onNavigate,
|
|
240
|
+
depth + 1,
|
|
241
|
+
combine(label),
|
|
242
|
+
);
|
|
243
|
+
if (node.elseStatement)
|
|
244
|
+
scanBody(
|
|
245
|
+
node.elseStatement,
|
|
246
|
+
module,
|
|
247
|
+
new Map(env),
|
|
248
|
+
refs,
|
|
249
|
+
returns,
|
|
250
|
+
onNavigate,
|
|
251
|
+
depth + 1,
|
|
252
|
+
combine("!(" + label + ")"),
|
|
253
|
+
);
|
|
254
|
+
} else if (ts.isExpressionStatement(node))
|
|
255
|
+
value(node.expression, module, env, refs, depth);
|
|
256
|
+
else if (ts.isReturnStatement(node)) {
|
|
257
|
+
if (onNavigate)
|
|
258
|
+
scanJsx(
|
|
259
|
+
node.expression,
|
|
260
|
+
module,
|
|
261
|
+
env,
|
|
262
|
+
refs,
|
|
263
|
+
onNavigate,
|
|
264
|
+
depth,
|
|
265
|
+
condition,
|
|
266
|
+
);
|
|
267
|
+
else returns.push(value(node.expression, module, env, refs, depth));
|
|
268
|
+
} else if (!ts.isFunctionLike(node) && onNavigate)
|
|
269
|
+
scanJsx(node, module, env, refs, onNavigate, depth, condition);
|
|
270
|
+
}
|
|
271
|
+
function scanJsx(node, module, env, refs, emit, depth, condition) {
|
|
272
|
+
if (!node || depth > 32) return;
|
|
273
|
+
if (ts.isConditionalExpression(node)) {
|
|
274
|
+
const label = node.condition.getText(module.source).slice(0, 160);
|
|
275
|
+
scanJsx(node.whenTrue, module, env, refs, emit, depth + 1, label);
|
|
276
|
+
scanJsx(
|
|
277
|
+
node.whenFalse,
|
|
278
|
+
module,
|
|
279
|
+
env,
|
|
280
|
+
refs,
|
|
281
|
+
emit,
|
|
282
|
+
depth + 1,
|
|
283
|
+
"!(" + label + ")",
|
|
284
|
+
);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
288
|
+
const opening = ts.isJsxElement(node) ? node.openingElement : node,
|
|
289
|
+
primitive = externalImport(
|
|
290
|
+
module,
|
|
291
|
+
opening.tagName.getText(module.source),
|
|
292
|
+
);
|
|
293
|
+
if (router(primitive) && primitive.name === "Navigate") {
|
|
294
|
+
const to = opening.attributes.properties.find(
|
|
295
|
+
(p) => ts.isJsxAttribute(p) && p.name.text === "to",
|
|
296
|
+
);
|
|
297
|
+
const proof = new Map(refs),
|
|
298
|
+
target = value(
|
|
299
|
+
ts.isJsxExpression(to?.initializer)
|
|
300
|
+
? to.initializer.expression
|
|
301
|
+
: to?.initializer,
|
|
302
|
+
module,
|
|
303
|
+
env,
|
|
304
|
+
proof,
|
|
305
|
+
);
|
|
306
|
+
emit({
|
|
307
|
+
target,
|
|
308
|
+
condition,
|
|
309
|
+
source: sourceLocation(module, node, "$source-navigation"),
|
|
310
|
+
refs: [...proof.values()],
|
|
311
|
+
});
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (ts.isFunctionLike(node)) return;
|
|
316
|
+
ts.forEachChild(node, (child) =>
|
|
317
|
+
scanJsx(child, module, env, refs, emit, depth + 1, condition),
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
for (const component of components) {
|
|
321
|
+
const module = modules.get(component.file),
|
|
322
|
+
ref = module && binding(module, component.name),
|
|
323
|
+
fn = ref && functionFrom(ref.node);
|
|
324
|
+
if (!fn?.body) continue;
|
|
325
|
+
budget = 0;
|
|
326
|
+
const rows = [];
|
|
327
|
+
scanBody(fn.body, module, new Map(), new Map(), [], (row) =>
|
|
328
|
+
rows.push(row),
|
|
329
|
+
);
|
|
330
|
+
definitions.set(component.componentKey, rows);
|
|
331
|
+
}
|
|
332
|
+
const byRoute = new Map(
|
|
333
|
+
pages.map((p) => [
|
|
334
|
+
p.pageKey,
|
|
335
|
+
p.componentKeys.flatMap((key) => definitions.get(key) ?? []),
|
|
336
|
+
]),
|
|
337
|
+
);
|
|
338
|
+
const result = [];
|
|
339
|
+
for (const page of pages)
|
|
340
|
+
for (const row of byRoute.get(page.pageKey)) {
|
|
341
|
+
const target = row.target;
|
|
342
|
+
if (target?.kind !== "url" || !target.path.startsWith("/")) continue;
|
|
343
|
+
const destination = pages.find((p) => p.route === target.path);
|
|
344
|
+
if (!destination) continue;
|
|
345
|
+
for (const [parameter, origin] of target.entries) {
|
|
346
|
+
if (origin?.kind !== "origin" || !origin.pathname) continue;
|
|
347
|
+
const consumers = byRoute
|
|
348
|
+
.get(destination.pageKey)
|
|
349
|
+
.filter(
|
|
350
|
+
(r) =>
|
|
351
|
+
r.target?.kind === "query" && r.target.parameter === parameter,
|
|
352
|
+
);
|
|
353
|
+
if (!consumers.length) continue;
|
|
354
|
+
const hasPassword = documents
|
|
355
|
+
.filter((d) => d.pageKey === destination.pageKey)
|
|
356
|
+
.some((d) =>
|
|
357
|
+
d.nodes.some(
|
|
358
|
+
(n) => n.tag === "input" && n.attributes.type === "password",
|
|
359
|
+
),
|
|
360
|
+
);
|
|
361
|
+
const sourceRefs = [row.source, ...row.refs].slice(0, 16);
|
|
362
|
+
result.push(
|
|
363
|
+
uiRelation({
|
|
364
|
+
routeKey: page.pageKey,
|
|
365
|
+
targetRouteKey: destination.pageKey,
|
|
366
|
+
kind: hasPassword ? "login" : "redirect",
|
|
367
|
+
returnParameter: parameter,
|
|
368
|
+
condition: row.condition,
|
|
369
|
+
activation: "route-condition",
|
|
370
|
+
sourceRefs,
|
|
371
|
+
}),
|
|
372
|
+
);
|
|
373
|
+
for (const consumer of consumers)
|
|
374
|
+
result.push(
|
|
375
|
+
uiRelation({
|
|
376
|
+
routeKey: destination.pageKey,
|
|
377
|
+
targetRouteKey: page.pageKey,
|
|
378
|
+
kind: "return",
|
|
379
|
+
returnParameter: parameter,
|
|
380
|
+
condition: consumer.condition,
|
|
381
|
+
activation: "route-condition",
|
|
382
|
+
sourceRefs: [consumer.source, ...consumer.refs].slice(0, 16),
|
|
383
|
+
}),
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return [...new Map(result.map((r) => [r.relationKey, r])).values()];
|
|
388
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** A finite source-only React Router matcher. No merchant modules or regular expressions run. */
|
|
2
|
+
export function sourceMatchPath(input, pathname) {
|
|
3
|
+
const options = typeof input === "string" ? { path: input } : input;
|
|
4
|
+
if (
|
|
5
|
+
!options ||
|
|
6
|
+
typeof options.path !== "string" ||
|
|
7
|
+
typeof pathname !== "string" ||
|
|
8
|
+
options.path.length > 1024 ||
|
|
9
|
+
pathname.length > 4000
|
|
10
|
+
)
|
|
11
|
+
throw Error("Unsupported source path");
|
|
12
|
+
const parts = options.path.split("/").filter(Boolean),
|
|
13
|
+
names = [];
|
|
14
|
+
let pattern = "^";
|
|
15
|
+
for (const [index, part] of parts.entries()) {
|
|
16
|
+
if (part === "*" && index === parts.length - 1) {
|
|
17
|
+
names.push("*");
|
|
18
|
+
pattern += "(?:/(.*))?";
|
|
19
|
+
} else if (/^:[A-Za-z_][\w-]*\??$/.test(part)) {
|
|
20
|
+
names.push(part.slice(1).replace(/\?$/, ""));
|
|
21
|
+
pattern += part.endsWith("?") ? "(?:/([^/]+))?" : "/([^/]+)";
|
|
22
|
+
} else if (/^[\w.~-]+$/.test(part))
|
|
23
|
+
pattern += "/" + part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
24
|
+
else throw Error("Unsupported source pattern");
|
|
25
|
+
}
|
|
26
|
+
pattern += options.end === false ? "(?=/|$)" : "/*$";
|
|
27
|
+
const match = new RegExp(pattern, options.caseSensitive ? "" : "i").exec(
|
|
28
|
+
pathname,
|
|
29
|
+
);
|
|
30
|
+
if (!match) return null;
|
|
31
|
+
return {
|
|
32
|
+
params: Object.fromEntries(
|
|
33
|
+
names.map((name, index) => [
|
|
34
|
+
name,
|
|
35
|
+
match[index + 1] === undefined
|
|
36
|
+
? undefined
|
|
37
|
+
: decodeURIComponent(match[index + 1]),
|
|
38
|
+
]),
|
|
39
|
+
),
|
|
40
|
+
pathname: match[0] || "/",
|
|
41
|
+
pathnameBase: (match[0] || "/").replace(/\/+$/, "") || "/",
|
|
42
|
+
pattern: {
|
|
43
|
+
path: options.path,
|
|
44
|
+
caseSensitive: options.caseSensitive ?? false,
|
|
45
|
+
end: options.end ?? true,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
package/src/static-query.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import {sourceMatchPath} from './static-primitives.mjs';
|
|
1
2
|
// Abstract interpretation of bounded, synchronous, read-only source helpers.
|
|
2
3
|
// No import(), eval, native callback invocation, await or object mutation.
|
|
3
|
-
export function staticQuery({ ts, node, module, context, binding, unknown, redact, sensitive }) {
|
|
4
|
+
export function staticQuery({ ts, node, module, context, binding, unknown, redact, sensitive, reference, isReference, imported, project }) {
|
|
4
5
|
let steps = 0;
|
|
5
6
|
const closure = Symbol("source-function"), writable = Symbol("function-locals"), fail = () => { throw unknown; };
|
|
6
7
|
const tick = () => { if (++steps > 6000) fail(); };
|
|
@@ -9,7 +10,9 @@ export function staticQuery({ ts, node, module, context, binding, unknown, redac
|
|
|
9
10
|
const scalar = value => value === null || ["string", "boolean", "number", "undefined"].includes(typeof value);
|
|
10
11
|
function clean(value, depth = 0) {
|
|
11
12
|
tick();
|
|
12
|
-
if (depth > 20
|
|
13
|
+
if (depth > 20) fail();
|
|
14
|
+
if(isReference?.(value))return value;
|
|
15
|
+
if(value===unknown||value?.[closure]){if(depth>0)return unknown;fail();}
|
|
13
16
|
if (typeof value === "number" && !Number.isFinite(value)) fail();
|
|
14
17
|
if (typeof value === "string") return redact(value);
|
|
15
18
|
if (Array.isArray(value)) { if (value.length > 256) fail(); return value.map(v => clean(v, depth + 1)); }
|
|
@@ -20,6 +23,7 @@ export function staticQuery({ ts, node, module, context, binding, unknown, redac
|
|
|
20
23
|
function read(n, m, env, seen = new Set(), depth = 0) {
|
|
21
24
|
tick(); n = unwrap(n); if (!n || depth > 24) fail();
|
|
22
25
|
const ev = child => read(child, m, env, seen, depth + 1);
|
|
26
|
+
const opaque=!(ts.isIdentifier(n)&&Object.hasOwn(env,n.text))&&reference?.(n,m);if(opaque)return opaque;
|
|
23
27
|
if (ts.isStringLiteralLike(n)) return redact(n.text);
|
|
24
28
|
if (ts.isNumericLiteral(n)) return Number(n.text);
|
|
25
29
|
if (n.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
@@ -38,11 +42,11 @@ export function staticQuery({ ts, node, module, context, binding, unknown, redac
|
|
|
38
42
|
if (ts.isObjectLiteralExpression(n)) {
|
|
39
43
|
const value = Object.create(null);
|
|
40
44
|
for (const p of n.properties) {
|
|
41
|
-
if (ts.isShorthandPropertyAssignment(p)) { value[p.name.text] = sensitive(p.name.text) ? unknown : ev(p.name);
|
|
45
|
+
if (ts.isShorthandPropertyAssignment(p)) { try{value[p.name.text] = sensitive(p.name.text) ? unknown : ev(p.name);}catch{value[p.name.text]=unknown;}continue; }
|
|
42
46
|
if (!ts.isPropertyAssignment(p) || !p.name || ts.isComputedPropertyName(p.name)) fail();
|
|
43
47
|
const key = p.name.text; if (!key || ["__proto__", "constructor", "prototype"].includes(key)) fail();
|
|
44
48
|
// Keep unrelated async/mutating helpers opaque, never evaluate their bodies.
|
|
45
|
-
value[key] = sensitive(key) ? unknown : ev(p.initializer);
|
|
49
|
+
try{value[key] = sensitive(key) ? unknown : ev(p.initializer);}catch{value[key]=unknown;}
|
|
46
50
|
}
|
|
47
51
|
return value;
|
|
48
52
|
}
|
|
@@ -50,8 +54,11 @@ export function staticQuery({ ts, node, module, context, binding, unknown, redac
|
|
|
50
54
|
const object = ev(n.expression), key = ts.isPropertyAccessExpression(n) ? n.name.text : ev(n.argumentExpression);
|
|
51
55
|
if (object == null && n.questionDotToken) return undefined;
|
|
52
56
|
if (object == null || ["__proto__", "constructor", "prototype"].includes(key) || sensitive(String(key))) fail();
|
|
53
|
-
|
|
57
|
+
const result=Object.hasOwn(Object(object), key) || key === "length" ? object[key] : undefined;
|
|
58
|
+
if(result===unknown)fail();return result;
|
|
54
59
|
}
|
|
60
|
+
if(ts.isTemplateExpression(n)){let text=n.head.text;for(const span of n.templateSpans){const v=ev(span.expression);if(!scalar(v)||v===unknown)fail();text+=String(v)+span.literal.text;if(text.length>4000)fail();}return redact(text);}
|
|
61
|
+
if(ts.isRegularExpressionLiteral(n)){const raw=n.getText(m.source);if(raw==='/[?#]/')return {sourceRegex:'query-separator'};if(raw==='/\\/+$/')return {sourceRegex:'trailing-slashes'};if(raw==='/^./')return {sourceRegex:'first-character'};fail();}
|
|
55
62
|
if (ts.isTypeOfExpression(n)) return typeof ev(n.expression);
|
|
56
63
|
if (ts.isPrefixUnaryExpression(n)) { const value = ev(n.operand); if (n.operator === ts.SyntaxKind.ExclamationToken) return !value; if (n.operator === ts.SyntaxKind.MinusToken && typeof value === "number") return -value; fail(); }
|
|
57
64
|
if (ts.isBinaryExpression(n)) {
|
|
@@ -67,15 +74,34 @@ export function staticQuery({ ts, node, module, context, binding, unknown, redac
|
|
|
67
74
|
}
|
|
68
75
|
if (ts.isConditionalExpression(n)) return ev(ev(n.condition) ? n.whenTrue : n.whenFalse);
|
|
69
76
|
if (ts.isCallExpression(n)) {
|
|
77
|
+
const projected=project?.(n,m,env);if(Array.isArray(projected))return projected[0];
|
|
70
78
|
if (n.arguments.length > 8) fail();
|
|
79
|
+
const primitive=imported?.(m,n.expression.getText(m.source));
|
|
80
|
+
if(primitive?.specifier==='react'&&primitive.name==='useMemo'&&n.arguments.length===2){const fn=ev(n.arguments[0]);return call(fn,[],depth+1);}
|
|
71
81
|
const args = n.arguments.map(ev);
|
|
82
|
+
if(ts.isPropertyAccessExpression(n.expression)&&ts.isIdentifier(n.expression.expression)&&n.expression.expression.text==='Object'&&!Object.hasOwn(env,'Object')&&!binding(m,'Object')){
|
|
83
|
+
const method=n.expression.name.text,target=args[0];
|
|
84
|
+
if(!plain(target)||isReference?.(target)||Object.keys(target).length>256)fail();
|
|
85
|
+
if(method==='keys'&&args.length===1)return Object.keys(target);
|
|
86
|
+
if(method==='hasOwn'&&args.length===2&&['string','number'].includes(typeof args[1]))return Object.hasOwn(target,args[1]);
|
|
87
|
+
fail();
|
|
88
|
+
}
|
|
89
|
+
if(['react-router','react-router-dom'].includes(primitive?.specifier)&&primitive.name==='matchPath'&&args.length===2)return sourceMatchPath(args[0],args[1]);
|
|
90
|
+
if(ts.isIdentifier(n.expression)&&n.expression.text==='String'&&args.length===1&&scalar(args[0]))return String(args[0]);
|
|
72
91
|
if (ts.isPropertyAccessExpression(n.expression)) {
|
|
73
92
|
const target = ev(n.expression.expression), method = n.expression.name.text;
|
|
74
|
-
if (Array.isArray(target) && ["find", "filter", "map"].includes(method) && args.length === 1 && args[0]?.[closure]) {
|
|
93
|
+
if (Array.isArray(target) && ["find", "filter", "map", "some", "every", "flatMap"].includes(method) && args.length === 1 && args[0]?.[closure]) {
|
|
75
94
|
if (target.length > 256) fail();
|
|
76
95
|
const result = [];
|
|
77
|
-
for (let i = 0; i < target.length; i++) { const value = call(args[0], [target[i], i], depth + 1); if (method === "find" && value) return target[i]; if (method === "filter" && value) result.push(target[i]); if (method === "map") result.push(value); }
|
|
78
|
-
return method === "find" ? undefined : result;
|
|
96
|
+
for (let i = 0; i < target.length; i++) { const value = call(args[0], [target[i], i], depth + 1); if (method === "find" && value) return target[i]; if (method === "filter" && value) result.push(target[i]); if (method === "map") result.push(value); if(method==="flatMap"){if(!Array.isArray(value))fail();result.push(...value);} if(method==="some"&&value)return true;if(method==="every"&&!value)return false; }
|
|
97
|
+
return method === "find" ? undefined : method==="some"?false:method==="every"?true:result;
|
|
98
|
+
}
|
|
99
|
+
if(typeof target==='string'&&target.length<=4000){
|
|
100
|
+
if(['startsWith','endsWith','includes','toLowerCase','toUpperCase','trim','slice'].includes(method)&&args.every(v=>['string','number'].includes(typeof v)))return String.prototype[method].apply(target,args);
|
|
101
|
+
if(method==='split'&&args[0]?.sourceRegex==='query-separator'&&args[1]===1)return target.split(/[?#]/,1);
|
|
102
|
+
if(method==='replace'&&args[0]?.sourceRegex==='trailing-slashes'&&args[1]==='')return target.replace(/\/+$/,'');
|
|
103
|
+
if(method==='replaceAll'&&args.length===2&&typeof args[0]==='string'&&args[0].length&&typeof args[1]==='string'){const result=target.split(args[0]).join(args[1]);if(result.length>4000)fail();return result;}
|
|
104
|
+
if(method==='replace'&&args[0]?.sourceRegex==='first-character'&&args[1]?.[closure]){const first=call(args[1],[target.charAt(0)],depth+1);if(typeof first!=='string')fail();return first+target.slice(1);}
|
|
79
105
|
}
|
|
80
106
|
}
|
|
81
107
|
return call(ev(n.expression), args, depth + 1);
|
|
@@ -86,9 +112,11 @@ export function staticQuery({ ts, node, module, context, binding, unknown, redac
|
|
|
86
112
|
tick(); if (!fn?.[closure] || depth > 24 || fn.node.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword)) fail();
|
|
87
113
|
const env = { ...fn.env, [writable]: new Set() };
|
|
88
114
|
for (let i = 0; i < fn.node.parameters.length; i++) {
|
|
89
|
-
const p = fn.node.parameters[i]; if (
|
|
90
|
-
|
|
91
|
-
env[writable].add(p.name.text);
|
|
115
|
+
const p = fn.node.parameters[i]; if (p.dotDotDotToken) fail();
|
|
116
|
+
const value=args[i] === undefined && p.initializer ? read(p.initializer, fn.module, env, new Set(), depth + 1) : args[i];
|
|
117
|
+
if(ts.isIdentifier(p.name)){env[p.name.text]=value;env[writable].add(p.name.text);}
|
|
118
|
+
else if(ts.isArrayBindingPattern(p.name)&&Array.isArray(value)&&p.name.elements.length<=8){for(const [index,field] of p.name.elements.entries()){if(ts.isOmittedExpression(field))continue;if(!ts.isIdentifier(field.name)||field.dotDotDotToken)fail();env[field.name.text]=value[index]===undefined&&field.initializer?read(field.initializer,fn.module,env,new Set(),depth+1):value[index];env[writable].add(field.name.text);}}
|
|
119
|
+
else fail();
|
|
92
120
|
}
|
|
93
121
|
if (!ts.isBlock(fn.node.body)) return read(fn.node.body, fn.module, env, new Set(), depth + 1);
|
|
94
122
|
const result = statements(fn.node.body.statements, fn.module, env, depth + 1); return result?.value;
|