@tinvar/renderer 0.0.0-stage → 0.4.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/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
package/src/eslint.js
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
// The lint rule that keeps state out of server components.
|
|
2
|
+
//
|
|
3
|
+
// tinvar-spec.md, "Renderer hardening": "Per-render isolation. Each render
|
|
4
|
+
// runs in its own AsyncLocalStorage context. The renderer never caches query
|
|
5
|
+
// results across requests. A lint rule rejects mutable module-level state in
|
|
6
|
+
// server files."
|
|
7
|
+
//
|
|
8
|
+
// A server module is evaluated once per renderer process, and every render
|
|
9
|
+
// that process serves -- every user's -- runs against that one instance.
|
|
10
|
+
// What the framework holds for a render lives in the render's context, but a
|
|
11
|
+
// `let` at the top of a page, or a Map a component fills, sits outside it:
|
|
12
|
+
// what one user's render leaves there, the next user's render reads. The
|
|
13
|
+
// render context cannot stop that, so the source is held to it before it
|
|
14
|
+
// ships.
|
|
15
|
+
//
|
|
16
|
+
// A server file is a module of the rsc graph. The graph exists whole only
|
|
17
|
+
// while the bundler has it (vite.js), so the config names the directories
|
|
18
|
+
// whose modules are server components -- `configs.server` covers the app's
|
|
19
|
+
// pages and components -- and the rule leaves out what @vitejs/plugin-rsc
|
|
20
|
+
// leaves out of the graph: a module with a 'use client' directive in its
|
|
21
|
+
// prologue, which the rsc environment replaces with a client reference. A
|
|
22
|
+
// declaration file holds no code and is left out too.
|
|
23
|
+
//
|
|
24
|
+
// Mutable module state is:
|
|
25
|
+
// - a top-level `let` or `var`, reported where it is declared -- exported,
|
|
26
|
+
// with a message of its own, since every importer reads it live;
|
|
27
|
+
// - any other top-level binding -- a const, a function, a class, an import
|
|
28
|
+
// -- reassigned anywhere in the file;
|
|
29
|
+
// - a top-level binding whose value the file mutates anywhere: a property
|
|
30
|
+
// assigned, deleted or incremented; the value handed to Object.assign(),
|
|
31
|
+
// Object.defineProperty() or the like as the target; or a call of a
|
|
32
|
+
// method that changes an array, Map, Set or WeakMap: push, pop, shift,
|
|
33
|
+
// unshift, splice, sort, reverse, fill, copyWithin, set, add, delete,
|
|
34
|
+
// clear. A write through it to a nested value counts the same;
|
|
35
|
+
// - a variable a closure keeps from when the module loaded -- one declared
|
|
36
|
+
// in a function invoked at the top as it is written, or in a top-level
|
|
37
|
+
// block -- that a function made there reassigns or mutates;
|
|
38
|
+
// - a write to a global: `globalThis.x = ...`, `process.env.X = ...`.
|
|
39
|
+
//
|
|
40
|
+
// Building a constant at the top -- `const ROUTES = new Map()` and then
|
|
41
|
+
// `ROUTES.set(...)` -- is reported too, as the spec words it: the rule cannot
|
|
42
|
+
// tell set-up at load from a write a render makes, and a value built whole in
|
|
43
|
+
// its initializer needs no write.
|
|
44
|
+
//
|
|
45
|
+
// Frozen and `as const` values. A constant is only reported when the file
|
|
46
|
+
// writes it, so one it never writes is never reported, frozen or not.
|
|
47
|
+
// Object.freeze() of an object or array literal exempts writes to the frozen
|
|
48
|
+
// value's own properties: in a module, which is strict code, they throw
|
|
49
|
+
// rather than change anything. A write through it to a nested value is still
|
|
50
|
+
// reported, since freezing is shallow, and so is a write to a frozen Map, Set
|
|
51
|
+
// or instance, whose methods freezing does not stop. `as const` exempts
|
|
52
|
+
// nothing: TypeScript already refuses a write to one, so a write this rule
|
|
53
|
+
// sees went past the type through a cast, and it changes the value at
|
|
54
|
+
// runtime all the same.
|
|
55
|
+
//
|
|
56
|
+
// What it cannot see: state behind a function -- a factory called at the top
|
|
57
|
+
// that returns a closure, a memoizer from a dependency, a method that writes
|
|
58
|
+
// `this` -- and a binding reached through an alias (`const a = list;
|
|
59
|
+
// a.push()`). It reads one file's syntax, not what its code does when it
|
|
60
|
+
// runs.
|
|
61
|
+
//
|
|
62
|
+
// The plugin's other rule, for a query called inside a can() branch, is in
|
|
63
|
+
// can-branch.js.
|
|
64
|
+
|
|
65
|
+
import { authorizeQueriesInCanBranches } from './can-branch.js';
|
|
66
|
+
|
|
67
|
+
/** Methods that change the array, Map, Set or WeakMap they are called on. */
|
|
68
|
+
const MUTATORS = new Set([
|
|
69
|
+
'push',
|
|
70
|
+
'pop',
|
|
71
|
+
'shift',
|
|
72
|
+
'unshift',
|
|
73
|
+
'splice',
|
|
74
|
+
'sort',
|
|
75
|
+
'reverse',
|
|
76
|
+
'fill',
|
|
77
|
+
'copyWithin',
|
|
78
|
+
'set',
|
|
79
|
+
'add',
|
|
80
|
+
'delete',
|
|
81
|
+
'clear',
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
/** Those an array has, which a frozen array refuses by throwing. */
|
|
85
|
+
const ARRAY_MUTATORS = new Set([
|
|
86
|
+
'push',
|
|
87
|
+
'pop',
|
|
88
|
+
'shift',
|
|
89
|
+
'unshift',
|
|
90
|
+
'splice',
|
|
91
|
+
'sort',
|
|
92
|
+
'reverse',
|
|
93
|
+
'fill',
|
|
94
|
+
'copyWithin',
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
/** Built-ins that write onto the value they are handed first. */
|
|
98
|
+
const TARGET_WRITERS = new Map([
|
|
99
|
+
['Object', new Set(['assign', 'defineProperty', 'defineProperties', 'setPrototypeOf'])],
|
|
100
|
+
['Reflect', new Set(['set', 'defineProperty', 'deleteProperty', 'setPrototypeOf'])],
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
/** Nodes around an expression that leave its value as it is: TypeScript's assertions, an optional chain. */
|
|
104
|
+
const WRAPPERS = new Set([
|
|
105
|
+
'ChainExpression',
|
|
106
|
+
'TSAsExpression',
|
|
107
|
+
'TSNonNullExpression',
|
|
108
|
+
'TSSatisfiesExpression',
|
|
109
|
+
'TSTypeAssertion',
|
|
110
|
+
'TSInstantiationExpression',
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
const DECLARATION_FILE = /\.d\.[cm]?ts$/;
|
|
114
|
+
|
|
115
|
+
/** The modules `configs.server` holds to the rule: the app's pages and components. */
|
|
116
|
+
export const SERVER_FILES = [
|
|
117
|
+
'resources/pages/**/*.{js,jsx,mjs,ts,tsx,mts}',
|
|
118
|
+
'resources/components/**/*.{js,jsx,mjs,ts,tsx,mts}',
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const WHY =
|
|
122
|
+
'A server module is loaded once and every render shares it, so what one render leaves there the next one reads, and one user\'s data can reach another user\'s page (tinvar-spec.md, "Per-render isolation").';
|
|
123
|
+
|
|
124
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
125
|
+
export const noMutableModuleState = {
|
|
126
|
+
meta: {
|
|
127
|
+
type: 'problem',
|
|
128
|
+
docs: {
|
|
129
|
+
description:
|
|
130
|
+
"Disallow mutable module-level state in server components: every render shares it, so one user's data can reach another's render.",
|
|
131
|
+
},
|
|
132
|
+
schema: [],
|
|
133
|
+
messages: {
|
|
134
|
+
topLevel: `'{{name}}' is a top-level \`{{kind}}\`. ${WHY} Keep a per-render value inside the component, or make this a \`const\` nothing writes; a module that only runs in the browser says so with 'use client'.`,
|
|
135
|
+
exported: `'{{name}}' is an exported top-level \`{{kind}}\`, which every importer reads live. ${WHY} Export a \`const\` nothing writes, or a function that builds the value for each render.`,
|
|
136
|
+
reassigned: `'{{name}}' is module state, and this reassigns it. ${WHY} Keep a per-render value inside the component.`,
|
|
137
|
+
mutated: `'{{name}}' is module state, and this mutates it. ${WHY} Build the value inside the render, or build it whole where it is declared and never write it.`,
|
|
138
|
+
global: `This writes to '{{name}}', a global. One renderer process serves every render, so what this render leaves there the next one reads, and one user's data can reach another user's page (tinvar-spec.md, "Per-render isolation").`,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
create(context) {
|
|
142
|
+
if (DECLARATION_FILE.test(context.filename)) return {};
|
|
143
|
+
const sourceCode = context.sourceCode;
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
// At the end, when every node has its parent and the scopes are whole.
|
|
147
|
+
'Program:exit'(program) {
|
|
148
|
+
if (isClientModule(program)) return;
|
|
149
|
+
check(context, sourceCode, program);
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Whether @vitejs/plugin-rsc takes the module for a client reference: a
|
|
157
|
+
* 'use client' directive anywhere in its prologue, as its hasDirective()
|
|
158
|
+
* reads it. A string later in the file is no directive, to it or here.
|
|
159
|
+
*/
|
|
160
|
+
function isClientModule(program) {
|
|
161
|
+
return program.body.some(
|
|
162
|
+
(/** @type {any} */ statement) =>
|
|
163
|
+
statement.type === 'ExpressionStatement' && statement.directive === 'use client',
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @param {import('eslint').Rule.RuleContext} context
|
|
169
|
+
* @param {import('eslint').SourceCode} sourceCode
|
|
170
|
+
* @param {any} program
|
|
171
|
+
*/
|
|
172
|
+
function check(context, sourceCode, program) {
|
|
173
|
+
const globalScope = /** @type {any} */ (sourceCode.scopeManager).globalScope;
|
|
174
|
+
// The module's own scope: a module, or the function CommonJS wraps it in.
|
|
175
|
+
// A script has none, and its top-level bindings are the global scope's.
|
|
176
|
+
const top =
|
|
177
|
+
globalScope.childScopes.find(
|
|
178
|
+
(/** @type {any} */ scope) =>
|
|
179
|
+
scope.block === program && (scope.type === 'module' || scope.type === 'function'),
|
|
180
|
+
) ?? globalScope;
|
|
181
|
+
|
|
182
|
+
const isGlobal = (/** @type {any} */ identifier) => {
|
|
183
|
+
for (let scope = sourceCode.getScope(identifier); scope; scope = scope.upper) {
|
|
184
|
+
const variable = /** @type {any} */ (scope.set.get(identifier.name));
|
|
185
|
+
if (variable !== undefined && variable.isValueVariable !== false) return variable.defs.length === 0;
|
|
186
|
+
}
|
|
187
|
+
return true;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/** Reports a write through one reference to a binding that outlives the render. */
|
|
191
|
+
const reportWrite = (/** @type {any} */ reference, /** @type {string} */ name, frozen) => {
|
|
192
|
+
if (reference.isValueReference === false) return;
|
|
193
|
+
if (reference.isWrite()) {
|
|
194
|
+
if (reference.init) return;
|
|
195
|
+
context.report({ node: reference.identifier, messageId: 'reassigned', data: { name } });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const mutation = mutationOf(reference.identifier, isGlobal);
|
|
199
|
+
if (mutation === null) return;
|
|
200
|
+
// A frozen literal's own properties throw when written, so nothing changes.
|
|
201
|
+
if (
|
|
202
|
+
frozen !== null &&
|
|
203
|
+
mutation.level === 0 &&
|
|
204
|
+
(mutation.method === null || (frozen === 'array' && ARRAY_MUTATORS.has(mutation.method)))
|
|
205
|
+
) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
context.report({ node: mutation.node, messageId: 'mutated', data: { name } });
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// The module's top level: every binding, every write, wherever it is made.
|
|
212
|
+
for (const variable of top.variables) {
|
|
213
|
+
if (variable.isValueVariable === false || variable.defs.length === 0) continue;
|
|
214
|
+
|
|
215
|
+
const declared = variable.defs.find(
|
|
216
|
+
(/** @type {any} */ def) =>
|
|
217
|
+
def.type === 'Variable' &&
|
|
218
|
+
(def.parent.kind === 'let' || def.parent.kind === 'var') &&
|
|
219
|
+
!def.parent.declare,
|
|
220
|
+
);
|
|
221
|
+
if (declared !== undefined) {
|
|
222
|
+
// One report, where it is declared: its writes are what the `let` is for.
|
|
223
|
+
context.report({
|
|
224
|
+
node: declared.name,
|
|
225
|
+
messageId: isExported(variable, declared) ? 'exported' : 'topLevel',
|
|
226
|
+
data: { name: variable.name, kind: declared.parent.kind },
|
|
227
|
+
});
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const frozen = frozenLiteral(variable, isGlobal);
|
|
232
|
+
for (const reference of variable.references) reportWrite(reference, variable.name, frozen);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// What a closure made at load keeps: written by a function made there, it
|
|
236
|
+
// lives, and changes, as long as the module.
|
|
237
|
+
for (const scope of /** @type {any} */ (sourceCode.scopeManager).scopes) {
|
|
238
|
+
if (scope === top || scope === globalScope || !runsAtLoad(scope, top)) continue;
|
|
239
|
+
for (const variable of /** @type {any} */ (scope).variables) {
|
|
240
|
+
if (variable.isValueVariable === false || variable.defs.length === 0) continue;
|
|
241
|
+
const frozen = frozenLiteral(variable, isGlobal);
|
|
242
|
+
for (const reference of variable.references) {
|
|
243
|
+
if (fromClosure(reference.from, scope)) reportWrite(reference, variable.name, frozen);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Globals: what the parser could not resolve, and the built-ins and
|
|
249
|
+
// configured globals, which are declared nowhere in the file.
|
|
250
|
+
const globals = [
|
|
251
|
+
...globalScope.through,
|
|
252
|
+
...globalScope.variables
|
|
253
|
+
.filter((/** @type {any} */ variable) => variable.defs.length === 0)
|
|
254
|
+
.flatMap((/** @type {any} */ variable) => variable.references),
|
|
255
|
+
];
|
|
256
|
+
for (const reference of globals) {
|
|
257
|
+
if (reference.isValueReference === false) continue;
|
|
258
|
+
const mutation = reference.isWrite()
|
|
259
|
+
? { node: reference.identifier }
|
|
260
|
+
: mutationOf(reference.identifier, isGlobal);
|
|
261
|
+
if (mutation !== null) {
|
|
262
|
+
context.report({ node: mutation.node, messageId: 'global', data: { name: reference.identifier.name } });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Whether a reference changes the value its binding holds, rather than the
|
|
269
|
+
* binding: a write to a property of it, or of anything reached through it,
|
|
270
|
+
* a mutating method called on either, or either handed to a built-in that
|
|
271
|
+
* writes onto it.
|
|
272
|
+
*
|
|
273
|
+
* `level` counts the properties between the binding and the value changed:
|
|
274
|
+
* 0 for `list.push(x)` and `config.mode = x`, 1 for `state.items.push(x)`.
|
|
275
|
+
* A frozen literal's exemption holds at 0 alone.
|
|
276
|
+
*
|
|
277
|
+
* @param {any} identifier
|
|
278
|
+
* @param {(identifier: any) => boolean} isGlobal
|
|
279
|
+
* @returns {{ node: any, level: number, method: string | null } | null}
|
|
280
|
+
*/
|
|
281
|
+
function mutationOf(identifier, isGlobal) {
|
|
282
|
+
let node = identifier;
|
|
283
|
+
let member = null;
|
|
284
|
+
let depth = 0;
|
|
285
|
+
for (let parent = node.parent; parent; parent = node.parent) {
|
|
286
|
+
if (WRAPPERS.has(parent.type) && parent.expression === node) {
|
|
287
|
+
node = parent;
|
|
288
|
+
} else if (parent.type === 'MemberExpression' && parent.object === node) {
|
|
289
|
+
node = parent;
|
|
290
|
+
member = parent;
|
|
291
|
+
depth += 1;
|
|
292
|
+
} else {
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const parent = node.parent;
|
|
298
|
+
if (member !== null) {
|
|
299
|
+
if (isWriteTarget(node, parent)) return { node, level: depth - 1, method: null };
|
|
300
|
+
if (parent?.type === 'CallExpression' && parent.callee === node && !writesTarget(parent, isGlobal)) {
|
|
301
|
+
const method = propertyName(member);
|
|
302
|
+
if (method !== null && MUTATORS.has(method)) return { node: parent, level: depth - 1, method };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (parent?.type === 'CallExpression' && parent.arguments[0] === node && writesTarget(parent, isGlobal)) {
|
|
306
|
+
return { node: parent, level: depth, method: null };
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Whether `node`, a member expression, is what its parent assigns, deletes, increments or destructures into. */
|
|
312
|
+
function isWriteTarget(node, parent) {
|
|
313
|
+
switch (parent?.type) {
|
|
314
|
+
case 'AssignmentExpression':
|
|
315
|
+
case 'AssignmentPattern':
|
|
316
|
+
case 'ForInStatement':
|
|
317
|
+
case 'ForOfStatement':
|
|
318
|
+
return parent.left === node;
|
|
319
|
+
case 'UpdateExpression':
|
|
320
|
+
case 'RestElement':
|
|
321
|
+
return parent.argument === node;
|
|
322
|
+
case 'UnaryExpression':
|
|
323
|
+
return parent.operator === 'delete' && parent.argument === node;
|
|
324
|
+
case 'ArrayPattern':
|
|
325
|
+
return parent.elements.includes(node);
|
|
326
|
+
case 'Property':
|
|
327
|
+
return parent.value === node && parent.parent?.type === 'ObjectPattern';
|
|
328
|
+
default:
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** `Object.assign(target, ...)` and its like, with `Object` or `Reflect` the built-in, not a binding of the file's. */
|
|
334
|
+
function writesTarget(call, isGlobal) {
|
|
335
|
+
const callee = unwrap(call.callee);
|
|
336
|
+
if (callee?.type !== 'MemberExpression' || callee.object.type !== 'Identifier') return false;
|
|
337
|
+
const methods = TARGET_WRITERS.get(callee.object.name);
|
|
338
|
+
const method = propertyName(callee);
|
|
339
|
+
return methods !== undefined && method !== null && methods.has(method) && isGlobal(callee.object);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** A member's property as written, or null when it is computed from something other than a literal. */
|
|
343
|
+
function propertyName(member) {
|
|
344
|
+
const property = member.property;
|
|
345
|
+
if (!member.computed) return property.type === 'Identifier' ? property.name : null;
|
|
346
|
+
if (property.type === 'Literal' && typeof property.value === 'string') return property.value;
|
|
347
|
+
if (property.type === 'TemplateLiteral' && property.expressions.length === 0) {
|
|
348
|
+
return property.quasis[0]?.value.cooked ?? null;
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function unwrap(node) {
|
|
354
|
+
while (node && WRAPPERS.has(node.type)) node = node.expression;
|
|
355
|
+
return node;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* 'object' or 'array' for a const bound to Object.freeze() of that literal,
|
|
360
|
+
* through any TypeScript assertion around either; null for anything else,
|
|
361
|
+
* a frozen Map or instance among them.
|
|
362
|
+
*/
|
|
363
|
+
function frozenLiteral(variable, isGlobal) {
|
|
364
|
+
if (variable.defs.length !== 1) return null;
|
|
365
|
+
const def = variable.defs[0];
|
|
366
|
+
if (def.type !== 'Variable' || def.parent.kind !== 'const' || def.node.id.type !== 'Identifier')
|
|
367
|
+
return null;
|
|
368
|
+
|
|
369
|
+
const init = unwrap(def.node.init);
|
|
370
|
+
if (init?.type !== 'CallExpression') return null;
|
|
371
|
+
const callee = unwrap(init.callee);
|
|
372
|
+
if (
|
|
373
|
+
callee?.type !== 'MemberExpression' ||
|
|
374
|
+
callee.object.type !== 'Identifier' ||
|
|
375
|
+
callee.object.name !== 'Object' ||
|
|
376
|
+
propertyName(callee) !== 'freeze' ||
|
|
377
|
+
!isGlobal(callee.object)
|
|
378
|
+
) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const value = unwrap(init.arguments[0]);
|
|
383
|
+
if (value?.type === 'ObjectExpression') return 'object';
|
|
384
|
+
if (value?.type === 'ArrayExpression') return 'array';
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Whether a top-level `let` or `var` is exported: in its declaration, or by an export list naming it. */
|
|
389
|
+
function isExported(variable, def) {
|
|
390
|
+
if (def.parent.parent?.type === 'ExportNamedDeclaration') return true;
|
|
391
|
+
return variable.references.some(
|
|
392
|
+
(/** @type {any} */ reference) =>
|
|
393
|
+
reference.identifier.parent?.type === 'ExportSpecifier' &&
|
|
394
|
+
reference.identifier.parent.local === reference.identifier,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Whether a scope's code runs once, as the module loads: the module's own,
|
|
400
|
+
* and a block, a class body, a static field or block, or a function invoked
|
|
401
|
+
* where it is written, inside it. Any other function runs when called -- a
|
|
402
|
+
* render's own variables are its own.
|
|
403
|
+
*/
|
|
404
|
+
function runsAtLoad(scope, top) {
|
|
405
|
+
for (let current = scope; current !== top; current = current.upper) {
|
|
406
|
+
if (current === null || current.type === 'global') return false;
|
|
407
|
+
if (current.type === 'function' && !isInvokedInPlace(current.block)) return false;
|
|
408
|
+
if (current.type === 'class-field-initializer' && current.block.parent?.static !== true) return false;
|
|
409
|
+
}
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Whether a reference is made from inside a function nested in `scope`, which may run long after it. */
|
|
414
|
+
function fromClosure(from, scope) {
|
|
415
|
+
for (let current = from; current !== null && current !== scope; current = current.upper) {
|
|
416
|
+
if (current.type === 'function' && !isInvokedInPlace(current.block)) return true;
|
|
417
|
+
if (current.type === 'class-field-initializer' && current.block.parent?.static !== true) return true;
|
|
418
|
+
}
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** `(() => { ... })()`, `(function () { ... }).call(this)`: a function run once, where it is written. */
|
|
423
|
+
function isInvokedInPlace(fn) {
|
|
424
|
+
if (fn.type !== 'FunctionExpression' && fn.type !== 'ArrowFunctionExpression') return false;
|
|
425
|
+
let node = fn;
|
|
426
|
+
while (node.parent && WRAPPERS.has(node.parent.type)) node = node.parent;
|
|
427
|
+
const parent = node.parent;
|
|
428
|
+
if ((parent?.type === 'CallExpression' || parent?.type === 'NewExpression') && parent.callee === node) {
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
return (
|
|
432
|
+
parent?.type === 'MemberExpression' &&
|
|
433
|
+
parent.object === node &&
|
|
434
|
+
(propertyName(parent) === 'call' || propertyName(parent) === 'apply') &&
|
|
435
|
+
parent.parent?.type === 'CallExpression' &&
|
|
436
|
+
parent.parent.callee === parent
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* The plugin: `import tinvar from '@tinvar/renderer/eslint'`, then
|
|
442
|
+
* `tinvar.configs.server` in a flat config, or a rule by name --
|
|
443
|
+
* `'@tinvar/no-mutable-module-state'`,
|
|
444
|
+
* `'@tinvar/authorize-queries-in-can-branches'` -- in a block of the app's
|
|
445
|
+
* own for another layout. The second reads what `tinvar codegen` wrote
|
|
446
|
+
* (QUERY_MAP) from where ESLint runs, or from its `queryMap` option.
|
|
447
|
+
*/
|
|
448
|
+
const plugin = {
|
|
449
|
+
meta: { name: '@tinvar/renderer/eslint', namespace: '@tinvar' },
|
|
450
|
+
rules: {
|
|
451
|
+
'no-mutable-module-state': noMutableModuleState,
|
|
452
|
+
'authorize-queries-in-can-branches': authorizeQueriesInCanBranches,
|
|
453
|
+
},
|
|
454
|
+
configs: /** @type {Record<string, object>} */ ({}),
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
plugin.configs.server = {
|
|
458
|
+
name: '@tinvar/server',
|
|
459
|
+
files: SERVER_FILES,
|
|
460
|
+
plugins: { '@tinvar': plugin },
|
|
461
|
+
rules: {
|
|
462
|
+
'@tinvar/no-mutable-module-state': 'error',
|
|
463
|
+
'@tinvar/authorize-queries-in-can-branches': 'error',
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
export { authorizeQueriesInCanBranches };
|
|
468
|
+
export const { rules, configs } = plugin;
|
|
469
|
+
export default plugin;
|