@solidjs/start 2.0.4 → 2.0.6
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/CHANGELOG.md +54 -0
- package/dist/config/boundary-modules.d.ts +15 -9
- package/dist/config/boundary-modules.js +24 -21
- package/dist/config/fs-router.js +24 -28
- package/dist/directives/compile.d.ts +3 -1
- package/dist/directives/compile.js +5 -2
- package/dist/directives/get-hierarchical-name.d.ts +2 -0
- package/dist/directives/get-hierarchical-name.js +104 -0
- package/dist/directives/index.js +6 -2
- package/dist/directives/plugin.d.ts +3 -0
- package/dist/directives/plugin.js +173 -138
- package/dist/directives/validate.d.ts +17 -0
- package/dist/directives/validate.js +176 -0
- package/dist/fns/handler.js +128 -10
- package/dist/fns/serialization.d.ts +25 -4
- package/dist/fns/serialization.js +258 -93
- package/dist/fns/shared.js +3 -2
- package/package.json +3 -3
- package/dist/directives/get-descriptive-name.d.ts +0 -2
- package/dist/directives/get-descriptive-name.js +0 -37
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import * as t from "@babel/types";
|
|
2
2
|
import { bubbleFunctionDeclaration } from "./bubble-function-declaration.js";
|
|
3
3
|
import { generateUniqueName } from "./generate-unique-name.js";
|
|
4
|
-
import {
|
|
4
|
+
import { getHierarchicalName } from "./get-hierarchical-name.js";
|
|
5
5
|
import { getImportIdentifier } from "./get-import-identifier.js";
|
|
6
6
|
import { getRootStatementPath } from "./get-root-statement-path.js";
|
|
7
7
|
import { isStatementTopLevel } from "./is-statement-top-level.js";
|
|
8
8
|
import { isPathValid, unwrapPath } from "./paths.js";
|
|
9
9
|
import { removeUnusedVariables } from "./remove-unused-variables.js";
|
|
10
|
+
import xxHash32 from "./xxhash32.js";
|
|
11
|
+
import { assertHoistable, assertNoMethodDirectives, collectMisplacedDirectives, } from "./validate.js";
|
|
10
12
|
function isValidFunction(node) {
|
|
11
13
|
return t.isArrowFunctionExpression(node) || t.isFunctionExpression(node);
|
|
12
14
|
}
|
|
@@ -41,12 +43,19 @@ function isFunctionDirectiveValid(ctx, path) {
|
|
|
41
43
|
}
|
|
42
44
|
return false;
|
|
43
45
|
}
|
|
44
|
-
function createID(ctx,
|
|
45
|
-
const
|
|
46
|
+
function createID(ctx, path) {
|
|
47
|
+
const name = getHierarchicalName(path);
|
|
48
|
+
// Two functions can still share a name path, such as two arrows passed to
|
|
49
|
+
// the same call, so repeats are numbered.
|
|
50
|
+
const seen = ctx.names.get(name) ?? 0;
|
|
51
|
+
ctx.names.set(name, seen + 1);
|
|
52
|
+
const unique = seen === 0 ? name : `${name}$${seen}`;
|
|
53
|
+
ctx.count++;
|
|
46
54
|
if (ctx.env === "development") {
|
|
47
|
-
return `${
|
|
55
|
+
return `${ctx.hash}-${unique}`;
|
|
48
56
|
}
|
|
49
|
-
|
|
57
|
+
// Production ids stay opaque, so source names are not shipped to the browser.
|
|
58
|
+
return `${ctx.hash}-${xxHash32(unique).toString(16)}`;
|
|
50
59
|
}
|
|
51
60
|
function transformFunction(ctx, path, direct) {
|
|
52
61
|
if (!direct) {
|
|
@@ -55,10 +64,13 @@ function transformFunction(ctx, path, direct) {
|
|
|
55
64
|
}
|
|
56
65
|
cleanFunctionDirectives(ctx, path);
|
|
57
66
|
}
|
|
67
|
+
// The function is about to be moved to the top level of the module, so
|
|
68
|
+
// everything it reads has to still resolve from there.
|
|
69
|
+
assertHoistable(path, ctx.directive);
|
|
58
70
|
// First, get root statement
|
|
59
71
|
const rootStatement = getRootStatementPath(path);
|
|
60
72
|
// Create a unique ID for the function
|
|
61
|
-
const fnID = createID(ctx,
|
|
73
|
+
const fnID = createID(ctx, path);
|
|
62
74
|
if (ctx.mode === "server") {
|
|
63
75
|
// Create a "source" function on the root-level
|
|
64
76
|
const sourceReference = t.callExpression(getImportIdentifier(ctx.imports, path, ctx.definitions.register), [t.stringLiteral(fnID), path.node]);
|
|
@@ -109,163 +121,186 @@ function traceBinding(path, name) {
|
|
|
109
121
|
return undefined;
|
|
110
122
|
}
|
|
111
123
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
124
|
+
/**
|
|
125
|
+
* An export a `"${directive}"` module cannot serve is left out of the client
|
|
126
|
+
* build, so client code importing it gets a missing binding. The transform
|
|
127
|
+
* cannot tell a wrapped function such as `query(fn, key)` from a plain value,
|
|
128
|
+
* so this reports the export instead of failing the build.
|
|
129
|
+
*/
|
|
130
|
+
function droppedExport(ctx, path, detail) {
|
|
131
|
+
const line = path.node.loc?.start.line;
|
|
132
|
+
ctx.warnings.push(`A "${ctx.directive}" module can only serve the functions it declares, so this export is left out of the client build` +
|
|
133
|
+
`${line == null ? "" : ` (line ${line})`}. ${detail}`);
|
|
134
|
+
}
|
|
135
|
+
function isTypeOnlyDeclaration(node) {
|
|
136
|
+
switch (node?.type) {
|
|
137
|
+
case "TSInterfaceDeclaration":
|
|
138
|
+
case "TSTypeAliasDeclaration":
|
|
139
|
+
case "TSDeclareFunction":
|
|
140
|
+
case "TSModuleDeclaration":
|
|
141
|
+
return true;
|
|
142
|
+
default:
|
|
143
|
+
return false;
|
|
118
144
|
}
|
|
119
145
|
}
|
|
120
|
-
function
|
|
121
|
-
|
|
146
|
+
function resolveExportedFunction(path, name) {
|
|
147
|
+
const binding = traceBinding(path, name);
|
|
148
|
+
if (binding && isPathValid(binding.path, t.isVariableDeclarator)) {
|
|
149
|
+
return unwrapPath(binding.path.get("init"), isValidFunction);
|
|
150
|
+
}
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Every function a `"use server"` module exports, in source order, with the
|
|
155
|
+
* names it is exported under. Both modes walk this list in the same order,
|
|
156
|
+
* which is what keeps the ids generated on the two sides pointing at each
|
|
157
|
+
* other.
|
|
158
|
+
*/
|
|
159
|
+
function collectModuleExports(ctx, program) {
|
|
160
|
+
const entries = [];
|
|
161
|
+
const byNode = new Map();
|
|
162
|
+
function add(name, path) {
|
|
163
|
+
const existing = byNode.get(path.node);
|
|
164
|
+
if (existing) {
|
|
165
|
+
existing.names.push(name);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const entry = { names: [name], path };
|
|
169
|
+
byNode.set(path.node, entry);
|
|
170
|
+
entries.push(entry);
|
|
171
|
+
}
|
|
122
172
|
program.traverse({
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
bubbleFunctionDeclaration(child);
|
|
173
|
+
ExportAllDeclaration(path) {
|
|
174
|
+
if (path.node.exportKind === "type") {
|
|
175
|
+
return;
|
|
127
176
|
}
|
|
177
|
+
droppedExport(ctx, path, '"export * from" re-exports nothing the client can call. Re-export from a module without the directive instead.');
|
|
128
178
|
},
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const id = unwrapPath(path.get("declaration"), t.isIdentifier);
|
|
137
|
-
if (id) {
|
|
138
|
-
const binding = traceBinding(path, id.node.name);
|
|
139
|
-
if (binding) {
|
|
140
|
-
bindings.add(binding);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
},
|
|
144
|
-
ExportNamedDeclaration(path) {
|
|
145
|
-
if (path.node.source || path.node.exportKind === "type") {
|
|
146
|
-
return;
|
|
179
|
+
ExportDefaultDeclaration(path) {
|
|
180
|
+
const declaration = path.get("declaration");
|
|
181
|
+
const id = unwrapPath(declaration, t.isIdentifier);
|
|
182
|
+
if (id) {
|
|
183
|
+
const fn = resolveExportedFunction(path, id.node.name);
|
|
184
|
+
if (fn) {
|
|
185
|
+
add("default", fn);
|
|
147
186
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const binding = traceBinding(specifier, specifier.node.local.name);
|
|
151
|
-
if (binding) {
|
|
152
|
-
bindings.add(binding);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
187
|
+
else {
|
|
188
|
+
droppedExport(ctx, path, `"${id.node.name}" is not a function declared in this module.`);
|
|
155
189
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// `export default async function () {}` is only a declaration by
|
|
193
|
+
// position and has no binding to trace, so read it as an expression.
|
|
194
|
+
if (isPathValid(declaration, t.isFunctionDeclaration) && !declaration.node.id) {
|
|
195
|
+
const node = declaration.node;
|
|
196
|
+
declaration.replaceWith(t.functionExpression(null, node.params, node.body, node.generator, node.async));
|
|
197
|
+
}
|
|
198
|
+
const fn = unwrapPath(path.get("declaration"), isValidFunction);
|
|
199
|
+
if (fn) {
|
|
200
|
+
add("default", fn);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
droppedExport(ctx, path, "The default export is not a function declared in this module.");
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
ExportNamedDeclaration(path) {
|
|
207
|
+
if (path.node.exportKind === "type") {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (path.node.source) {
|
|
211
|
+
droppedExport(ctx, path, "Re-exporting from another module is not supported. Export a function declared here that calls it instead.");
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
for (const specifier of path.get("specifiers")) {
|
|
215
|
+
if (!isPathValid(specifier, t.isExportSpecifier)) {
|
|
216
|
+
droppedExport(ctx, specifier, "Only named exports of functions are supported.");
|
|
217
|
+
continue;
|
|
168
218
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
for (const binding of bindings) {
|
|
172
|
-
transformBindingForServer(ctx, binding);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
176
|
-
// Trace bindings
|
|
177
|
-
const uniqueBindings = new Set();
|
|
178
|
-
const exportedBindings = new Map();
|
|
179
|
-
program.traverse({
|
|
180
|
-
ExportDefaultDeclaration(path) {
|
|
181
|
-
const id = unwrapPath(path.get("declaration"), t.isIdentifier);
|
|
182
|
-
if (id) {
|
|
183
|
-
const binding = traceBinding(path, id.node.name);
|
|
184
|
-
if (binding) {
|
|
185
|
-
uniqueBindings.add(binding);
|
|
186
|
-
exportedBindings.set("default", binding);
|
|
187
|
-
}
|
|
219
|
+
if (specifier.node.exportKind === "type") {
|
|
220
|
+
continue;
|
|
188
221
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (
|
|
192
|
-
|
|
222
|
+
const local = specifier.node.local.name;
|
|
223
|
+
const fn = resolveExportedFunction(specifier, local);
|
|
224
|
+
if (!fn) {
|
|
225
|
+
droppedExport(ctx, specifier, `"${local}" is not a function declared in this module.`);
|
|
226
|
+
continue;
|
|
193
227
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
228
|
+
const exported = specifier.node.exported;
|
|
229
|
+
add(t.isIdentifier(exported) ? exported.name : exported.value, fn);
|
|
230
|
+
}
|
|
231
|
+
const declaration = path.get("declaration");
|
|
232
|
+
if (isPathValid(declaration, t.isVariableDeclaration)) {
|
|
233
|
+
for (const declarator of declaration.get("declarations")) {
|
|
234
|
+
const left = unwrapPath(declarator.get("id"), t.isIdentifier);
|
|
235
|
+
if (!left) {
|
|
236
|
+
droppedExport(ctx, declarator, "Destructured exports are not supported.");
|
|
237
|
+
continue;
|
|
204
238
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
// Check if left is identifier
|
|
210
|
-
const left = unwrapPath(declaration.get("id"), t.isIdentifier);
|
|
211
|
-
if (left) {
|
|
212
|
-
const binding = traceBinding(left, left.node.name);
|
|
213
|
-
if (binding) {
|
|
214
|
-
uniqueBindings.add(binding);
|
|
215
|
-
exportedBindings.set(left.node.name, binding);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
239
|
+
const fn = resolveExportedFunction(left, left.node.name);
|
|
240
|
+
if (!fn) {
|
|
241
|
+
droppedExport(ctx, declarator, `"${left.node.name}" is not a function.`);
|
|
242
|
+
continue;
|
|
218
243
|
}
|
|
219
|
-
|
|
220
|
-
},
|
|
221
|
-
});
|
|
222
|
-
// generate ids for each unique binding
|
|
223
|
-
const sourceIDs = new Map();
|
|
224
|
-
for (const binding of uniqueBindings) {
|
|
225
|
-
if (isPathValid(binding.path, t.isVariableDeclarator)) {
|
|
226
|
-
const init = unwrapPath(binding.path.get("init"), isValidFunction);
|
|
227
|
-
if (init) {
|
|
228
|
-
sourceIDs.set(binding, createID(ctx, getDescriptiveName(init, "anonymous")));
|
|
244
|
+
add(left.node.name, fn);
|
|
229
245
|
}
|
|
230
246
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const specifiers = [];
|
|
236
|
-
const declarationMap = new Map();
|
|
237
|
-
// Declare all client functions
|
|
238
|
-
for (const [exported, binding] of exportedBindings) {
|
|
239
|
-
let currentIdentifier = declarationMap.get(binding);
|
|
240
|
-
if (!currentIdentifier) {
|
|
241
|
-
currentIdentifier = generateUniqueName(program, "fn");
|
|
242
|
-
const fnID = sourceIDs.get(binding);
|
|
243
|
-
if (fnID) {
|
|
244
|
-
declarations.push(t.variableDeclarator(currentIdentifier, t.callExpression(getImportIdentifier(ctx.imports, program, ctx.definitions.clone), [
|
|
245
|
-
t.stringLiteral(fnID),
|
|
246
|
-
])));
|
|
247
|
-
declarationMap.set(binding, currentIdentifier);
|
|
248
|
-
}
|
|
247
|
+
else if (declaration.node &&
|
|
248
|
+
!isPathValid(declaration, t.isFunctionDeclaration) &&
|
|
249
|
+
!isTypeOnlyDeclaration(declaration.node)) {
|
|
250
|
+
droppedExport(ctx, declaration, "Only functions can be exported.");
|
|
249
251
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
return entries;
|
|
255
|
+
}
|
|
256
|
+
function transformModuleLevelDirective(ctx, program) {
|
|
257
|
+
cleanDirectives(program, ctx.directive);
|
|
258
|
+
program.traverse({
|
|
259
|
+
FunctionDeclaration(child) {
|
|
260
|
+
// We only need to move top-level functions
|
|
261
|
+
if (isStatementTopLevel(child)) {
|
|
262
|
+
bubbleFunctionDeclaration(child);
|
|
252
263
|
}
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
program.scope.crawl();
|
|
267
|
+
const entries = collectModuleExports(ctx, program);
|
|
268
|
+
if (ctx.mode === "server") {
|
|
269
|
+
for (const entry of entries) {
|
|
270
|
+
transformFunction(ctx, entry.path, true);
|
|
253
271
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const ids = entries.map(entry => createID(ctx, entry.path));
|
|
275
|
+
// clear body
|
|
276
|
+
program.node.body = [];
|
|
277
|
+
const declarations = [];
|
|
278
|
+
const specifiers = [];
|
|
279
|
+
for (let i = 0, len = entries.length; i < len; i++) {
|
|
280
|
+
const local = generateUniqueName(program, "fn");
|
|
281
|
+
declarations.push(t.variableDeclarator(local, t.callExpression(getImportIdentifier(ctx.imports, program, ctx.definitions.clone), [
|
|
282
|
+
t.stringLiteral(ids[i]),
|
|
283
|
+
])));
|
|
284
|
+
for (const name of entries[i].names) {
|
|
285
|
+
specifiers.push(t.exportSpecifier(local, t.stringLiteral(name)));
|
|
260
286
|
}
|
|
261
|
-
program.pushContainer("body", body);
|
|
262
287
|
}
|
|
288
|
+
const body = [];
|
|
289
|
+
if (declarations.length > 0) {
|
|
290
|
+
body.push(t.variableDeclaration("const", declarations));
|
|
291
|
+
}
|
|
292
|
+
if (specifiers.length > 0) {
|
|
293
|
+
body.push(t.exportNamedDeclaration(null, specifiers, null));
|
|
294
|
+
}
|
|
295
|
+
program.pushContainer("body", body);
|
|
263
296
|
}
|
|
264
297
|
export function directivesPlugin() {
|
|
265
298
|
return {
|
|
266
299
|
name: "solid-start:directives",
|
|
267
300
|
visitor: {
|
|
268
301
|
Program(program, ctx) {
|
|
302
|
+
assertNoMethodDirectives(program, ctx.opts.directive);
|
|
303
|
+
ctx.opts.warnings.push(...collectMisplacedDirectives(program, ctx.opts.directive));
|
|
269
304
|
const isModuleLevel = isDirectiveValid(ctx.opts, program.node.directives);
|
|
270
305
|
if (isModuleLevel) {
|
|
271
306
|
transformModuleLevelDirective(ctx.opts, program);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type * as babel from "@babel/core";
|
|
2
|
+
import * as t from "@babel/types";
|
|
3
|
+
type HoistableFunction = t.ArrowFunctionExpression | t.FunctionExpression;
|
|
4
|
+
export declare function assertHoistable(path: babel.NodePath<HoistableFunction>, directive: string): void;
|
|
5
|
+
/**
|
|
6
|
+
* A directive only applies to a function body. The transform ignores one in a
|
|
7
|
+
* method, which ships the method body and every module it imports to the
|
|
8
|
+
* browser.
|
|
9
|
+
*/
|
|
10
|
+
export declare function assertNoMethodDirectives(program: babel.NodePath<t.Program>, directive: string): void;
|
|
11
|
+
/**
|
|
12
|
+
* A directive string that is not in a directive prologue does nothing. It is
|
|
13
|
+
* almost always meant to be one, so report it. Otherwise the module compiles as
|
|
14
|
+
* if it had no server functions.
|
|
15
|
+
*/
|
|
16
|
+
export declare function collectMisplacedDirectives(program: babel.NodePath<t.Program>, directive: string): string[];
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import * as t from "@babel/types";
|
|
2
|
+
/**
|
|
3
|
+
* A server function is moved to the top level of its module. Anything it reads
|
|
4
|
+
* from an enclosing scope stops resolving once it is moved. These checks report
|
|
5
|
+
* that at build time. Without them the code compiles and then fails at runtime,
|
|
6
|
+
* or stops parsing.
|
|
7
|
+
*/
|
|
8
|
+
function isTypeOnlyPosition(path, boundary) {
|
|
9
|
+
let current = path;
|
|
10
|
+
while (current && current !== boundary) {
|
|
11
|
+
if (current.node.type.startsWith("TS") || current.node.type.startsWith("Type")) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
current = current.parentPath;
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The nearest ancestor that decides what `this` and `arguments` mean.
|
|
20
|
+
* Arrow functions are transparent. Every other function form is not.
|
|
21
|
+
*/
|
|
22
|
+
function getThisBoundary(path) {
|
|
23
|
+
let previous = path;
|
|
24
|
+
let current = path.parentPath;
|
|
25
|
+
while (current) {
|
|
26
|
+
switch (current.node.type) {
|
|
27
|
+
case "FunctionExpression":
|
|
28
|
+
case "FunctionDeclaration":
|
|
29
|
+
case "ObjectMethod":
|
|
30
|
+
case "ClassMethod":
|
|
31
|
+
case "ClassPrivateMethod":
|
|
32
|
+
case "StaticBlock":
|
|
33
|
+
case "Program":
|
|
34
|
+
return current;
|
|
35
|
+
case "ClassProperty":
|
|
36
|
+
case "ClassPrivateProperty":
|
|
37
|
+
// Only the initializer is bound to the instance.
|
|
38
|
+
if (previous.node === current.node.value) {
|
|
39
|
+
return current;
|
|
40
|
+
}
|
|
41
|
+
break;
|
|
42
|
+
default:
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
previous = current;
|
|
46
|
+
current = current.parentPath;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
function findAncestor(path, boundary, types) {
|
|
51
|
+
let current = path.parentPath;
|
|
52
|
+
while (current && current !== boundary) {
|
|
53
|
+
if (types.includes(current.node.type)) {
|
|
54
|
+
return current;
|
|
55
|
+
}
|
|
56
|
+
current = current.parentPath;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
export function assertHoistable(path, directive) {
|
|
61
|
+
const program = path.scope.getProgramParent().path;
|
|
62
|
+
// A function expression keeps its own `this` and `arguments` wherever it is
|
|
63
|
+
// moved. An arrow takes both from where it is written.
|
|
64
|
+
const isArrow = path.node.type === "ArrowFunctionExpression";
|
|
65
|
+
const boundary = getThisBoundary(path);
|
|
66
|
+
// An arrow written at the top level already reads `this` from the module.
|
|
67
|
+
const lexicalSelfIsModule = boundary === null || boundary.node.type === "Program";
|
|
68
|
+
/**
|
|
69
|
+
* Whether `this` or `arguments` at this position is bound by something that
|
|
70
|
+
* stays behind when the server function moves. A function expression keeps
|
|
71
|
+
* both wherever it lands, and a nested function binds its own.
|
|
72
|
+
*/
|
|
73
|
+
function isLexicallyOutside(child) {
|
|
74
|
+
const binder = getThisBoundary(child);
|
|
75
|
+
if (binder && binder !== path && binder.isDescendant(path)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
if (binder === path && !isArrow) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return !lexicalSelfIsModule;
|
|
82
|
+
}
|
|
83
|
+
function unsupported(target, what, hint) {
|
|
84
|
+
throw target.buildCodeFrameError(`${what} cannot be used inside a "${directive}" function, because the function is moved to the top level of the module. ${hint}`);
|
|
85
|
+
}
|
|
86
|
+
// `super` and private members are reported first. When both apply to the same
|
|
87
|
+
// expression, such as `this.#value`, they are the more specific cause.
|
|
88
|
+
path.traverse({
|
|
89
|
+
Super(child) {
|
|
90
|
+
const home = findAncestor(child, path, ["ObjectMethod", "ClassMethod", "ClassPrivateMethod"]);
|
|
91
|
+
if (!home) {
|
|
92
|
+
unsupported(child, "`super`", "Move the server function out of the method and pass what it needs as an argument.");
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
PrivateName(child) {
|
|
96
|
+
const owner = findAncestor(child, path, ["ClassBody"]);
|
|
97
|
+
if (!owner) {
|
|
98
|
+
unsupported(child, "A private class member", "Read it outside the server function and pass it as an argument.");
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
path.traverse({
|
|
103
|
+
ThisExpression(child) {
|
|
104
|
+
if (!isLexicallyOutside(child)) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
unsupported(child, "`this`", "Pass the value it refers to as an argument instead.");
|
|
108
|
+
},
|
|
109
|
+
Identifier(child) {
|
|
110
|
+
if (!child.isReferencedIdentifier() || isTypeOnlyPosition(child, path)) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const { name } = child.node;
|
|
114
|
+
if (name === "arguments") {
|
|
115
|
+
if (!isLexicallyOutside(child)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
unsupported(child, "`arguments`", "Declare the parameters the server function needs instead.");
|
|
119
|
+
}
|
|
120
|
+
const binding = child.scope.getBinding(name);
|
|
121
|
+
// No binding means a global, which is still a global after the move.
|
|
122
|
+
if (!binding) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// Declared by the function itself, so it moves along with it.
|
|
126
|
+
if (binding.path === path || binding.path.isDescendant(path)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Declared at the top level of the module, where the function lands.
|
|
130
|
+
if (binding.scope.path === program) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
throw child.buildCodeFrameError(`"${name}" is declared outside the "${directive}" function that uses it, and the function is moved to the top level of the module, so "${name}" is not in scope when it runs. Pass it as an argument instead.`);
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* A directive only applies to a function body. The transform ignores one in a
|
|
139
|
+
* method, which ships the method body and every module it imports to the
|
|
140
|
+
* browser.
|
|
141
|
+
*/
|
|
142
|
+
export function assertNoMethodDirectives(program, directive) {
|
|
143
|
+
function check(child) {
|
|
144
|
+
for (const current of child.node.body.directives) {
|
|
145
|
+
if (current.value.value === directive) {
|
|
146
|
+
throw child.buildCodeFrameError(`"${directive}" is not supported in a method. Move the body into a function and call it from the method:\n` +
|
|
147
|
+
` const handler = async () => { "${directive}"; /* ... */ };`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
program.traverse({
|
|
152
|
+
ObjectMethod: check,
|
|
153
|
+
ClassMethod: check,
|
|
154
|
+
ClassPrivateMethod: check,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* A directive string that is not in a directive prologue does nothing. It is
|
|
159
|
+
* almost always meant to be one, so report it. Otherwise the module compiles as
|
|
160
|
+
* if it had no server functions.
|
|
161
|
+
*/
|
|
162
|
+
export function collectMisplacedDirectives(program, directive) {
|
|
163
|
+
const warnings = [];
|
|
164
|
+
program.traverse({
|
|
165
|
+
ExpressionStatement(child) {
|
|
166
|
+
const expression = child.node.expression;
|
|
167
|
+
if (!t.isStringLiteral(expression) || expression.value !== directive) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const line = child.node.loc?.start.line;
|
|
171
|
+
warnings.push(`"${directive}" on line ${line ?? "?"} is ignored because it is not the first statement of a function body or of the module. ` +
|
|
172
|
+
`Move it to the top of the body, before any other statement.`);
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
return warnings;
|
|
176
|
+
}
|