@tinvar/renderer 0.0.0-stage → 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/LICENSE +21 -0
- package/bin/dev-renderer.js +117 -0
- package/bin/renderer-probe.js +52 -0
- package/bin/renderer.js +327 -0
- package/package.json +44 -4
- package/src/asset-origin.js +306 -0
- package/src/build-profile.js +108 -0
- package/src/can-branch.js +1666 -0
- package/src/classify.js +157 -0
- package/src/client-chunks.js +54 -0
- package/src/client-props.js +540 -0
- package/src/codec.js +297 -0
- package/src/dev-app.js +45 -0
- package/src/dev-guard.js +33 -0
- package/src/dev-server.js +460 -0
- package/src/eslint.js +469 -0
- package/src/head.js +524 -0
- package/src/html-stream.js +479 -0
- package/src/log.js +131 -0
- package/src/mint-client.js +142 -0
- package/src/panel-styles.js +254 -0
- package/src/pool.js +664 -0
- package/src/protocol.js +71 -0
- package/src/query-client.js +198 -0
- package/src/query-map.js +116 -0
- package/src/render-session.js +580 -0
- package/src/sandbox.js +710 -0
- package/src/server.js +466 -0
- package/src/socket-group.js +72 -0
- package/src/vite.js +2416 -0
- package/src/vitest.d.ts +20 -0
- package/src/vitest.js +179 -0
- package/src/warm.d.ts +9 -0
- package/src/warm.js +60 -0
- package/src/workbench.d.ts +146 -0
- package/src/workbench.js +754 -0
- package/README.md +0 -3
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
// No function handed from a server component to a client one, refused by
|
|
2
|
+
// the build.
|
|
3
|
+
//
|
|
4
|
+
// tinvar-spec.md, "Client components": "Props across the boundary must be
|
|
5
|
+
// serializable: Data objects, primitives, promises (see queries) and server
|
|
6
|
+
// actions. Closures are refused at build time where detectable and at render
|
|
7
|
+
// time otherwise."
|
|
8
|
+
//
|
|
9
|
+
// At render, React refuses a function prop to a client component as it
|
|
10
|
+
// serializes the tree, and the renderer classifies the failure
|
|
11
|
+
// NOT_SERIALIZABLE (classify.js): the page fails for the visitor who reached
|
|
12
|
+
// it, in whichever branch of it held the function. The build can see most
|
|
13
|
+
// of these first, where they are written: a client component is a module the
|
|
14
|
+
// RSC plugin made a client reference of, and a function written into its
|
|
15
|
+
// props is syntax.
|
|
16
|
+
//
|
|
17
|
+
// So, over the rsc environment's graph once it is whole, each of the
|
|
18
|
+
// application's server modules is read with the bundler's parser, and each
|
|
19
|
+
// JSX element whose tag is imported is resolved: through the import, and
|
|
20
|
+
// through any re-exports on the way -- a kit's barrel -- to the module that
|
|
21
|
+
// defines it. Where that module is a client reference, every prop written to
|
|
22
|
+
// the element is read for a function:
|
|
23
|
+
// - a function or class written in place: `onClick={() => ...}`;
|
|
24
|
+
// - a name bound in the file to one -- a function declaration, or a
|
|
25
|
+
// `const`, `let` or `var` initialized with one -- in the scopes around
|
|
26
|
+
// the element;
|
|
27
|
+
// - `fn.bind(...)`, which makes a function;
|
|
28
|
+
// - any of those inside an object or array written in place, either branch
|
|
29
|
+
// of a conditional, either side of `&&`, `||` or `??`;
|
|
30
|
+
// and a function written as a child, a render prop. A server action is no
|
|
31
|
+
// function here: under `react-server` an `@actions/*` reference is the
|
|
32
|
+
// action's name (action-server.js), and Tinvar has no `'use server'`
|
|
33
|
+
// functions -- the renderer decodes no client calls (tinvar-spec.md, "No
|
|
34
|
+
// client input") -- so one of those is refused like any other function.
|
|
35
|
+
//
|
|
36
|
+
// What it cannot see, React still refuses at render: a function reached
|
|
37
|
+
// through a parameter, an import or a spread, and a tag whose module the
|
|
38
|
+
// build cannot read. Those are unknowns, not refusals: a server component
|
|
39
|
+
// may hand a function to another server component, and widening an unknown
|
|
40
|
+
// to "client" would refuse that. What it can see, it refuses, with every
|
|
41
|
+
// place named; and an application module it cannot parse fails the build.
|
|
42
|
+
|
|
43
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
44
|
+
import { extname, relative } from 'node:path';
|
|
45
|
+
import { getPluginApi } from '@vitejs/plugin-rsc';
|
|
46
|
+
import { parseAst } from 'vite';
|
|
47
|
+
|
|
48
|
+
const SOURCE = /\.(?:[cm]?[jt]sx?)$/;
|
|
49
|
+
|
|
50
|
+
/** What wraps a value and leaves it the same value. */
|
|
51
|
+
const SAME_VALUE = new Set([
|
|
52
|
+
'TSAsExpression',
|
|
53
|
+
'TSSatisfiesExpression',
|
|
54
|
+
'TSNonNullExpression',
|
|
55
|
+
'TSTypeAssertion',
|
|
56
|
+
'TSInstantiationExpression',
|
|
57
|
+
'ParenthesizedExpression',
|
|
58
|
+
'ChainExpression',
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
/** Why the build refuses a module's props. */
|
|
62
|
+
export class UnserializableProp extends Error {
|
|
63
|
+
/** @param {string} message */
|
|
64
|
+
constructor(message) {
|
|
65
|
+
super(message);
|
|
66
|
+
this.name = 'UnserializableProp';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The plugin. Only the rsc build has the server component graph, and the
|
|
72
|
+
* client references the RSC plugin made of it.
|
|
73
|
+
*
|
|
74
|
+
* @returns {import('vite').Plugin}
|
|
75
|
+
*/
|
|
76
|
+
export function serializableProps() {
|
|
77
|
+
/** @type {import('vite').ResolvedConfig | null} */
|
|
78
|
+
let resolved = null;
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
name: 'tinvar:serializable-props',
|
|
82
|
+
apply: 'build',
|
|
83
|
+
configResolved(config) {
|
|
84
|
+
resolved = config;
|
|
85
|
+
},
|
|
86
|
+
applyToEnvironment(environment) {
|
|
87
|
+
return environment.name === 'rsc';
|
|
88
|
+
},
|
|
89
|
+
async generateBundle() {
|
|
90
|
+
const manager = resolved === null ? undefined : getPluginApi(resolved)?.manager;
|
|
91
|
+
if (manager === undefined) {
|
|
92
|
+
throw new Error('[tinvar] serializableProps() needs @vitejs/plugin-rsc in the same config.');
|
|
93
|
+
}
|
|
94
|
+
const references = new Set(Object.keys(manager.clientReferenceMetaMap));
|
|
95
|
+
const root = resolved?.root ?? process.cwd();
|
|
96
|
+
|
|
97
|
+
/** @type {string[]} */
|
|
98
|
+
const found = [];
|
|
99
|
+
const clients = clientExports(
|
|
100
|
+
references,
|
|
101
|
+
async (specifier, importer) => (await this.resolve(specifier, importer))?.id ?? null,
|
|
102
|
+
);
|
|
103
|
+
for (const id of this.getModuleIds()) {
|
|
104
|
+
const file = fileOf(id);
|
|
105
|
+
if (id.startsWith('\0') || references.has(id) || isDependency(file) || !SOURCE.test(file)) continue;
|
|
106
|
+
if (!existsSync(file)) continue;
|
|
107
|
+
|
|
108
|
+
const source = readFileSync(file, 'utf8');
|
|
109
|
+
for (const violation of await functionProps(source, file, (specifier, name) =>
|
|
110
|
+
clients(specifier, name, id),
|
|
111
|
+
)) {
|
|
112
|
+
found.push(`${relative(root, file)}:${violation.line} ${violation.message}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (found.length > 0) {
|
|
117
|
+
throw new UnserializableProp(
|
|
118
|
+
`[tinvar] A server component hands a client component a function, which React refuses as the page renders (NOT_SERIALIZABLE):\n ${found.join('\n ')}\n` +
|
|
119
|
+
'Hand it data, or an action from @actions/*, and let the client component make the function it needs.',
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Whether a name imported from a module is a client component: the module
|
|
128
|
+
* is a client reference, or re-exports the name from one. Answers are kept
|
|
129
|
+
* for the build, since every page imports the same kit.
|
|
130
|
+
*
|
|
131
|
+
* @param {Set<string>} references the client references' module ids
|
|
132
|
+
* @param {(specifier: string, importer: string) => Promise<string | null>} resolveId
|
|
133
|
+
* @returns {(specifier: string, name: string, importer: string) => Promise<boolean>}
|
|
134
|
+
*/
|
|
135
|
+
export function clientExports(references, resolveId) {
|
|
136
|
+
/** @type {Map<string, Promise<boolean>>} */
|
|
137
|
+
const answers = new Map();
|
|
138
|
+
|
|
139
|
+
/** @type {(id: string, name: string, seen: Set<string>) => Promise<boolean>} */
|
|
140
|
+
const exportsClient = (id, name, seen) => {
|
|
141
|
+
const key = `${id}\0${name}`;
|
|
142
|
+
// The path first: a cycle comes back to an answer still being worked
|
|
143
|
+
// out, and awaiting it would await itself.
|
|
144
|
+
if (seen.has(key)) return Promise.resolve(false);
|
|
145
|
+
const known = answers.get(key);
|
|
146
|
+
if (known !== undefined) return known;
|
|
147
|
+
seen.add(key);
|
|
148
|
+
|
|
149
|
+
const answer = (async () => {
|
|
150
|
+
if (references.has(id)) return true;
|
|
151
|
+
const file = fileOf(id);
|
|
152
|
+
if (id.startsWith('\0') || !SOURCE.test(file) || !existsSync(file)) return false;
|
|
153
|
+
|
|
154
|
+
let program;
|
|
155
|
+
try {
|
|
156
|
+
program = parse(readFileSync(file, 'utf8'), file);
|
|
157
|
+
} catch {
|
|
158
|
+
// The bundler read it, under its own options; what this cannot, it
|
|
159
|
+
// leaves to React at render.
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** @type {Map<string, { from: string, name: string }>} what the module imports by name */
|
|
164
|
+
const imported = new Map();
|
|
165
|
+
for (const node of program.body) {
|
|
166
|
+
if (node.type !== 'ImportDeclaration' || node.importKind === 'type') continue;
|
|
167
|
+
for (const specifier of node.specifiers) {
|
|
168
|
+
if (specifier.type === 'ImportSpecifier') {
|
|
169
|
+
imported.set(specifier.local.name, { from: node.source.value, name: nameOf(specifier.imported) });
|
|
170
|
+
} else if (specifier.type === 'ImportDefaultSpecifier') {
|
|
171
|
+
imported.set(specifier.local.name, { from: node.source.value, name: 'default' });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const follow = async (/** @type {string} */ specifier, /** @type {string} */ inner) => {
|
|
177
|
+
const target = await resolveId(specifier, id);
|
|
178
|
+
return target !== null && exportsClient(target, inner, seen);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
for (const node of program.body) {
|
|
182
|
+
if (node.type === 'ExportNamedDeclaration' && node.exportKind !== 'type') {
|
|
183
|
+
for (const specifier of node.specifiers ?? []) {
|
|
184
|
+
if (nameOf(specifier.exported) !== name) continue;
|
|
185
|
+
const local = nameOf(specifier.local);
|
|
186
|
+
if (node.source) return follow(node.source.value, local);
|
|
187
|
+
const through = imported.get(local);
|
|
188
|
+
return through === undefined ? false : follow(through.from, through.name);
|
|
189
|
+
}
|
|
190
|
+
} else if (node.type === 'ExportDefaultDeclaration' && name === 'default') {
|
|
191
|
+
const value = node.declaration;
|
|
192
|
+
const through = value?.type === 'Identifier' ? imported.get(value.name) : undefined;
|
|
193
|
+
return through === undefined ? false : follow(through.from, through.name);
|
|
194
|
+
} else if (node.type === 'ExportAllDeclaration' && node.exported === null && name !== 'default') {
|
|
195
|
+
if (await follow(node.source.value, name)) return true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
})();
|
|
200
|
+
answers.set(key, answer);
|
|
201
|
+
return answer;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
return async (specifier, name, importer) => {
|
|
205
|
+
const id = await resolveId(specifier, importer);
|
|
206
|
+
return id !== null && exportsClient(id, name, new Set());
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Every function a module's server components write into a client
|
|
212
|
+
* component's props.
|
|
213
|
+
*
|
|
214
|
+
* @param {string} source the module's own source, as written
|
|
215
|
+
* @param {string} file its path, which says how to parse it
|
|
216
|
+
* @param {(specifier: string, name: string) => Promise<boolean>} isClient whether
|
|
217
|
+
* the name imported from the specifier is a client component
|
|
218
|
+
* @returns {Promise<Array<{ line: number, message: string }>>}
|
|
219
|
+
* @throws {UnserializableProp} for a module that cannot be parsed
|
|
220
|
+
*/
|
|
221
|
+
export async function functionProps(source, file, isClient) {
|
|
222
|
+
let program;
|
|
223
|
+
try {
|
|
224
|
+
program = parse(source, file);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
throw new UnserializableProp(
|
|
227
|
+
`[tinvar] ${file} cannot be parsed, so the props it hands client components cannot be checked: ${/** @type {Error} */ (error).message}`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
// A client module renders in the browser: what it hands its children is
|
|
231
|
+
// never serialized.
|
|
232
|
+
if (program.body.some((node) => node.type === 'ExpressionStatement' && node.directive === 'use client')) {
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** @type {Map<string, { from: string, name: string | null }>} each value imported: by name, or whole (null) */
|
|
237
|
+
const imported = new Map();
|
|
238
|
+
for (const node of program.body) {
|
|
239
|
+
if (node.type !== 'ImportDeclaration' || node.importKind === 'type') continue;
|
|
240
|
+
for (const specifier of node.specifiers) {
|
|
241
|
+
if (specifier.importKind === 'type') continue;
|
|
242
|
+
imported.set(specifier.local.name, {
|
|
243
|
+
from: node.source.value,
|
|
244
|
+
name:
|
|
245
|
+
specifier.type === 'ImportSpecifier'
|
|
246
|
+
? nameOf(specifier.imported)
|
|
247
|
+
: specifier.type === 'ImportDefaultSpecifier'
|
|
248
|
+
? 'default'
|
|
249
|
+
: null,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** @type {Array<{ element: any, tag: string, ancestors: any[], from: string, name: string }>} */
|
|
255
|
+
const elements = [];
|
|
256
|
+
walk(program, [], (node, ancestors) => {
|
|
257
|
+
if (node.type !== 'JSXElement') return;
|
|
258
|
+
const tag = node.openingElement.name;
|
|
259
|
+
let local;
|
|
260
|
+
let name;
|
|
261
|
+
if (tag.type === 'JSXIdentifier') {
|
|
262
|
+
// A lowercase tag is the platform's element, never a component.
|
|
263
|
+
if (!/^[A-Z_$]/.test(tag.name)) return;
|
|
264
|
+
local = tag.name;
|
|
265
|
+
} else if (tag.type === 'JSXMemberExpression' && tag.object.type === 'JSXIdentifier') {
|
|
266
|
+
local = tag.object.name;
|
|
267
|
+
name = tag.property.name;
|
|
268
|
+
} else {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const from = imported.get(local);
|
|
272
|
+
if (from === undefined || shadowed(local, ancestors)) return;
|
|
273
|
+
const exported = from.name === null ? name : name === undefined ? from.name : null;
|
|
274
|
+
if (exported === undefined || exported === null) return;
|
|
275
|
+
elements.push({
|
|
276
|
+
element: node,
|
|
277
|
+
tag: name === undefined ? local : `${local}.${name}`,
|
|
278
|
+
ancestors,
|
|
279
|
+
from: from.from,
|
|
280
|
+
name: exported,
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
/** @type {Array<{ line: number, message: string }>} */
|
|
285
|
+
const found = [];
|
|
286
|
+
for (const { element, tag, ancestors, from, name } of elements) {
|
|
287
|
+
/** @type {Array<{ prop: string, node: any }>} */
|
|
288
|
+
const functions = [];
|
|
289
|
+
for (const attribute of element.openingElement.attributes) {
|
|
290
|
+
if (attribute.type !== 'JSXAttribute' || attribute.value?.type !== 'JSXExpressionContainer') continue;
|
|
291
|
+
const fn = functionIn(attribute.value.expression, [...ancestors, element]);
|
|
292
|
+
if (fn !== null) functions.push({ prop: nameOf(attribute.name), node: fn });
|
|
293
|
+
}
|
|
294
|
+
for (const child of element.children) {
|
|
295
|
+
if (child.type !== 'JSXExpressionContainer') continue;
|
|
296
|
+
const fn = functionIn(child.expression, [...ancestors, element]);
|
|
297
|
+
if (fn !== null) functions.push({ prop: 'children', node: fn });
|
|
298
|
+
}
|
|
299
|
+
if (functions.length === 0 || !(await isClient(from, name))) continue;
|
|
300
|
+
|
|
301
|
+
for (const { prop, node } of functions) {
|
|
302
|
+
found.push({
|
|
303
|
+
line: lineOf(source, node.start),
|
|
304
|
+
message: `hands <${tag}>, a client component from '${from}', a function as '${prop}'.`,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return found;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* The function an expression is, or holds where it is written -- null for
|
|
313
|
+
* one it is not, or cannot be seen to be.
|
|
314
|
+
*
|
|
315
|
+
* @param {any} expression
|
|
316
|
+
* @param {any[]} ancestors the nodes around it, outermost first
|
|
317
|
+
* @returns {any | null}
|
|
318
|
+
*/
|
|
319
|
+
function functionIn(expression, ancestors) {
|
|
320
|
+
let node = expression;
|
|
321
|
+
while (node && SAME_VALUE.has(node.type)) node = node.expression;
|
|
322
|
+
if (!node) return null;
|
|
323
|
+
|
|
324
|
+
switch (node.type) {
|
|
325
|
+
case 'ArrowFunctionExpression':
|
|
326
|
+
case 'FunctionExpression':
|
|
327
|
+
case 'ClassExpression':
|
|
328
|
+
return node;
|
|
329
|
+
case 'Identifier': {
|
|
330
|
+
const binding = bindingOf(node.name, ancestors);
|
|
331
|
+
return binding === null ? null : node;
|
|
332
|
+
}
|
|
333
|
+
case 'CallExpression': {
|
|
334
|
+
const callee = node.callee;
|
|
335
|
+
const bound =
|
|
336
|
+
callee.type === 'MemberExpression' &&
|
|
337
|
+
!callee.computed &&
|
|
338
|
+
callee.property.type === 'Identifier' &&
|
|
339
|
+
callee.property.name === 'bind';
|
|
340
|
+
return bound ? node : null;
|
|
341
|
+
}
|
|
342
|
+
case 'ObjectExpression':
|
|
343
|
+
for (const property of node.properties) {
|
|
344
|
+
if (property.type !== 'Property') continue;
|
|
345
|
+
if (property.method || property.kind === 'get' || property.kind === 'set') return property;
|
|
346
|
+
const inner = functionIn(property.value, ancestors);
|
|
347
|
+
if (inner !== null) return inner;
|
|
348
|
+
}
|
|
349
|
+
return null;
|
|
350
|
+
case 'ArrayExpression':
|
|
351
|
+
for (const element of node.elements) {
|
|
352
|
+
const inner =
|
|
353
|
+
element === null || element.type === 'SpreadElement' ? null : functionIn(element, ancestors);
|
|
354
|
+
if (inner !== null) return inner;
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
case 'ConditionalExpression':
|
|
358
|
+
return functionIn(node.consequent, ancestors) ?? functionIn(node.alternate, ancestors);
|
|
359
|
+
case 'LogicalExpression':
|
|
360
|
+
return functionIn(node.left, ancestors) ?? functionIn(node.right, ancestors);
|
|
361
|
+
default:
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The function a name is bound to, in the nearest scope around a node that
|
|
368
|
+
* declares it: a function or class declaration, or a variable initialized
|
|
369
|
+
* with a function or class. Null for anything else -- a parameter, an
|
|
370
|
+
* import, a value -- and for a name nothing here declares.
|
|
371
|
+
*
|
|
372
|
+
* @param {string} name
|
|
373
|
+
* @param {any[]} ancestors outermost first
|
|
374
|
+
* @returns {any | null}
|
|
375
|
+
*/
|
|
376
|
+
function bindingOf(name, ancestors) {
|
|
377
|
+
for (let index = ancestors.length - 1; index >= 0; index--) {
|
|
378
|
+
const scope = ancestors[index];
|
|
379
|
+
if (isFunction(scope)) {
|
|
380
|
+
if (scope.params.some((/** @type {any} */ param) => declares(param, name))) return null;
|
|
381
|
+
if (scope.id?.name === name && scope.type !== 'FunctionDeclaration') return scope;
|
|
382
|
+
}
|
|
383
|
+
if (scope.type === 'CatchClause' && scope.param && declares(scope.param, name)) return null;
|
|
384
|
+
if (scope.type === 'ForStatement' || scope.type === 'ForInStatement' || scope.type === 'ForOfStatement') {
|
|
385
|
+
const declaration = scope.init ?? scope.left;
|
|
386
|
+
if (
|
|
387
|
+
declaration?.type === 'VariableDeclaration' &&
|
|
388
|
+
declaration.declarations.some((d) => declares(d.id, name))
|
|
389
|
+
) {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const body =
|
|
394
|
+
scope.type === 'Program' || scope.type === 'BlockStatement' || scope.type === 'StaticBlock'
|
|
395
|
+
? scope.body
|
|
396
|
+
: scope.type === 'SwitchCase'
|
|
397
|
+
? scope.consequent
|
|
398
|
+
: null;
|
|
399
|
+
if (body === null) continue;
|
|
400
|
+
|
|
401
|
+
for (let statement of body) {
|
|
402
|
+
if (
|
|
403
|
+
(statement.type === 'ExportNamedDeclaration' || statement.type === 'ExportDefaultDeclaration') &&
|
|
404
|
+
statement.declaration
|
|
405
|
+
) {
|
|
406
|
+
statement = statement.declaration;
|
|
407
|
+
}
|
|
408
|
+
if (
|
|
409
|
+
(statement.type === 'FunctionDeclaration' || statement.type === 'ClassDeclaration') &&
|
|
410
|
+
statement.id?.name === name
|
|
411
|
+
) {
|
|
412
|
+
return statement;
|
|
413
|
+
}
|
|
414
|
+
if (statement.type === 'VariableDeclaration') {
|
|
415
|
+
for (const declarator of statement.declarations) {
|
|
416
|
+
if (!declares(declarator.id, name)) continue;
|
|
417
|
+
if (declarator.id.type !== 'Identifier') return null;
|
|
418
|
+
let init = declarator.init;
|
|
419
|
+
while (init && SAME_VALUE.has(init.type)) init = init.expression;
|
|
420
|
+
return init &&
|
|
421
|
+
(init.type === 'ArrowFunctionExpression' ||
|
|
422
|
+
init.type === 'FunctionExpression' ||
|
|
423
|
+
init.type === 'ClassExpression')
|
|
424
|
+
? init
|
|
425
|
+
: null;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (statement.type === 'ImportDeclaration' && statement.specifiers.some((s) => s.local.name === name)) {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Whether a component's local name is declared again between it and the module's imports. */
|
|
437
|
+
function shadowed(name, ancestors) {
|
|
438
|
+
for (const scope of ancestors) {
|
|
439
|
+
if (scope.type === 'Program') continue;
|
|
440
|
+
if (isFunction(scope) && scope.params.some((/** @type {any} */ param) => declares(param, name)))
|
|
441
|
+
return true;
|
|
442
|
+
const body = scope.type === 'BlockStatement' ? scope.body : null;
|
|
443
|
+
if (body === null) continue;
|
|
444
|
+
for (const statement of body) {
|
|
445
|
+
if (
|
|
446
|
+
statement.type === 'VariableDeclaration' &&
|
|
447
|
+
statement.declarations.some((d) => declares(d.id, name))
|
|
448
|
+
) {
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
if (
|
|
452
|
+
(statement.type === 'FunctionDeclaration' || statement.type === 'ClassDeclaration') &&
|
|
453
|
+
statement.id?.name === name
|
|
454
|
+
) {
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Whether a binding pattern declares a name. */
|
|
463
|
+
function declares(pattern, name) {
|
|
464
|
+
if (!pattern) return false;
|
|
465
|
+
switch (pattern.type) {
|
|
466
|
+
case 'Identifier':
|
|
467
|
+
return pattern.name === name;
|
|
468
|
+
case 'AssignmentPattern':
|
|
469
|
+
return declares(pattern.left, name);
|
|
470
|
+
case 'RestElement':
|
|
471
|
+
return declares(pattern.argument, name);
|
|
472
|
+
case 'ArrayPattern':
|
|
473
|
+
return pattern.elements.some((/** @type {any} */ element) => declares(element, name));
|
|
474
|
+
case 'ObjectPattern':
|
|
475
|
+
return pattern.properties.some((/** @type {any} */ property) =>
|
|
476
|
+
declares(property.type === 'RestElement' ? property : property.value, name),
|
|
477
|
+
);
|
|
478
|
+
case 'TSParameterProperty':
|
|
479
|
+
return declares(pattern.parameter, name);
|
|
480
|
+
default:
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function isFunction(node) {
|
|
486
|
+
return (
|
|
487
|
+
node.type === 'FunctionDeclaration' ||
|
|
488
|
+
node.type === 'FunctionExpression' ||
|
|
489
|
+
node.type === 'ArrowFunctionExpression'
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** An identifier's name, or a string literal's value: how an import or export names a binding. */
|
|
494
|
+
function nameOf(node) {
|
|
495
|
+
if (node.type === 'JSXNamespacedName') return `${node.namespace.name}:${node.name.name}`;
|
|
496
|
+
return node.type === 'Identifier' || node.type === 'JSXIdentifier' ? node.name : String(node.value);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function lineOf(source, offset) {
|
|
500
|
+
return typeof offset === 'number' ? source.slice(0, offset).split('\n').length : 0;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function parse(source, file) {
|
|
504
|
+
const extension = extname(file);
|
|
505
|
+
const lang = extension === '.tsx' ? 'tsx' : /^\.[cm]?ts$/.test(extension) ? 'ts' : 'jsx';
|
|
506
|
+
return parseAst(source, { lang }, file);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** A module id as the file it was read from: no query, no hash. */
|
|
510
|
+
function fileOf(id) {
|
|
511
|
+
return id.replace(/[?#].*$/, '');
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** Whether a module's file is a dependency's: anywhere under a node_modules directory. */
|
|
515
|
+
function isDependency(file) {
|
|
516
|
+
return file.split(/[\\/]/).includes('node_modules');
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Depth-first over every node, with the nodes around each, outermost first.
|
|
521
|
+
*
|
|
522
|
+
* @param {any} node
|
|
523
|
+
* @param {any[]} ancestors
|
|
524
|
+
* @param {(node: any, ancestors: any[]) => void} visit
|
|
525
|
+
*/
|
|
526
|
+
function walk(node, ancestors, visit) {
|
|
527
|
+
if (node === null || typeof node !== 'object') return;
|
|
528
|
+
if (Array.isArray(node)) {
|
|
529
|
+
for (const child of node) walk(child, ancestors, visit);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
if (typeof node.type !== 'string') return;
|
|
533
|
+
visit(node, ancestors);
|
|
534
|
+
const inner = [...ancestors, node];
|
|
535
|
+
for (const key of Object.keys(node)) {
|
|
536
|
+
if (key === 'type' || key === 'start' || key === 'end' || key === 'range' || key === 'loc') continue;
|
|
537
|
+
const child = node[key];
|
|
538
|
+
if (child !== null && typeof child === 'object') walk(child, inner, visit);
|
|
539
|
+
}
|
|
540
|
+
}
|