filesystem-routing 0.4.2 → 0.5.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.
@@ -1,7 +1,5 @@
1
- // All credit for this work goes to the amazing Next.js team.
2
- // https://github.com/vercel/next.js/blob/canary/packages/next/build/babel/plugins/next-ssg-transform.ts
3
- // This is adapted to work with routeData functions. It can be run in two modes, one which preserves the routeData and the Component in the same file, and one which creates a
4
- import { basename, extname } from "node:path";
1
+ import { extname } from "node:path";
2
+ import { pickExports } from "./pick-exports.js";
5
3
  const PICKABLE_EXTENSIONS = ["js", "jsx", "ts", "tsx"];
6
4
  /**
7
5
  * Builds the module id used to import a subset of a route file's exports.
@@ -47,451 +45,17 @@ export function sanitizeChunkFileName(name) {
47
45
  const driveLetter = DRIVE_LETTER_RE.exec(stripped)?.[0] ?? "";
48
46
  return driveLetter + stripped.slice(driveLetter.length).replace(INVALID_CHAR_RE, "_");
49
47
  }
50
- function treeShakeTransform({ types: t }) {
51
- function getIdentifier(path) {
52
- const parentPath = path.parentPath;
53
- if (parentPath.type === "VariableDeclarator") {
54
- const pp = parentPath;
55
- const name = pp.get("id");
56
- return name.node.type === "Identifier" ? name : null;
57
- }
58
- if (parentPath.type === "AssignmentExpression") {
59
- const pp = parentPath;
60
- const name = pp.get("left");
61
- return name.node.type === "Identifier" ? name : null;
62
- }
63
- if (path.node.type === "ArrowFunctionExpression") {
64
- return null;
65
- }
66
- return path.node.id && path.node.id.type === "Identifier" ? path.get("id") : null;
67
- }
68
- function isTypeOnlyReference(path) {
69
- return path.getAncestry().some(parent => {
70
- if (parent.isTSType())
71
- return true;
72
- if (parent.isExportNamedDeclaration())
73
- return parent.node.exportKind === "type";
74
- if (parent.isExportSpecifier())
75
- return parent.node.exportKind === "type";
76
- return false;
77
- });
78
- }
79
- function runtimeReferences(binding) {
80
- return binding.referencePaths.filter(ref => !isTypeOnlyReference(ref));
81
- }
82
- function isIdentifierReferenced(ident, includeTypes = false) {
83
- const b = ident.scope.getBinding(ident.node.name);
84
- if (b) {
85
- const references = b.constantViolations.concat(includeTypes ? b.referencePaths : runtimeReferences(b));
86
- if (!references.length)
87
- return false;
88
- if (b.path.type === "FunctionDeclaration") {
89
- return !references.every(ref => ref.findParent(p => p === b.path));
90
- }
91
- return true;
92
- }
93
- return false;
94
- }
95
- function markFunction(path, state) {
96
- const ident = getIdentifier(path);
97
- if (ident && ident.node && isIdentifierReferenced(ident, true)) {
98
- state.refs.add(ident);
99
- }
100
- }
101
- function markImport(path, state) {
102
- const local = path.get("local");
103
- if (isIdentifierReferenced(local, true)) {
104
- state.refs.add(local);
105
- }
106
- }
107
- return {
108
- visitor: {
109
- Program: {
110
- enter(path, state) {
111
- state.refs = new Set();
112
- state.candidates = new Set();
113
- state.done = false;
114
- path.traverse({
115
- VariableDeclarator(variablePath, variableState) {
116
- if (variablePath.node.id.type === "Identifier") {
117
- const local = variablePath.get("id");
118
- if (isIdentifierReferenced(local, true)) {
119
- variableState.refs.add(local);
120
- }
121
- }
122
- else if (variablePath.node.id.type === "ObjectPattern") {
123
- const pattern = variablePath.get("id");
124
- const properties = pattern.get("properties");
125
- properties.forEach(p => {
126
- const local = p.get(p.node.type === "ObjectProperty"
127
- ? "value"
128
- : p.node.type === "RestElement"
129
- ? "argument"
130
- : (() => {
131
- throw new Error("invariant");
132
- })());
133
- if (isIdentifierReferenced(local, true)) {
134
- variableState.refs.add(local);
135
- }
136
- });
137
- }
138
- else if (variablePath.node.id.type === "ArrayPattern") {
139
- const pattern = variablePath.get("id");
140
- const elements = pattern.get("elements");
141
- elements.forEach(e => {
142
- let local;
143
- if (e.node && e.node.type === "Identifier") {
144
- local = e;
145
- }
146
- else if (e.node && e.node.type === "RestElement") {
147
- local = e.get("argument");
148
- }
149
- else {
150
- return;
151
- }
152
- if (isIdentifierReferenced(local, true)) {
153
- variableState.refs.add(local);
154
- }
155
- });
156
- }
157
- },
158
- ExportDefaultDeclaration(exportNamedPath) {
159
- if (state.opts.pick && !state.opts.pick.includes("default")) {
160
- const decl = exportNamedPath.node.declaration;
161
- // A named function/class declaration creates a module-scope
162
- // binding that picked exports may reference, so only strip
163
- // the `export default`; the sweep below removes the
164
- // declaration again if nothing references it.
165
- if ((t.isFunctionDeclaration(decl) || t.isClassDeclaration(decl)) && decl.id) {
166
- const [newPath] = exportNamedPath.replaceWith(decl);
167
- const id = newPath.get("id");
168
- state.refs.add(id);
169
- state.candidates.add(id);
170
- }
171
- else {
172
- exportNamedPath.remove();
173
- }
174
- }
175
- },
176
- ExportNamedDeclaration(exportNamedPath) {
177
- // No pick list means nothing gets pruned.
178
- if (!state.opts.pick) {
179
- return;
180
- }
181
- // Only module-level exports are route exports; `export` inside a
182
- // TS namespace/module block is unrelated to the pick list.
183
- if (!exportNamedPath.parentPath.isProgram()) {
184
- return;
185
- }
186
- const specifiers = exportNamedPath.get("specifiers");
187
- if (specifiers.length) {
188
- specifiers.forEach(s => {
189
- const exported = s.node.exported;
190
- const exportedName = t.isIdentifier(exported) ? exported.name : exported.value;
191
- if (!state.opts.pick.includes(exportedName)) {
192
- s.remove();
193
- }
194
- });
195
- if (exportNamedPath.node.specifiers.length < 1) {
196
- exportNamedPath.remove();
197
- }
198
- return;
199
- }
200
- const decl = exportNamedPath.get("declaration");
201
- if (decl == null || decl.node == null) {
202
- return;
203
- }
204
- switch (decl.node.type) {
205
- case "FunctionDeclaration":
206
- case "ClassDeclaration": {
207
- const name = decl.node.id?.name;
208
- if (name && state.opts.pick && !state.opts.pick.includes(name)) {
209
- // Keep the declaration in module scope since picked
210
- // exports may reference it; the sweep below removes it
211
- // again if unreferenced.
212
- const [newPath] = exportNamedPath.replaceWith(decl.node);
213
- const id = newPath.get("id");
214
- state.refs.add(id);
215
- state.candidates.add(id);
216
- }
217
- break;
218
- }
219
- case "VariableDeclaration": {
220
- const declNode = decl.node;
221
- // Destructuring declarators are conservatively treated as
222
- // picked (matching the previous behavior of leaving them
223
- // untouched).
224
- const isPicked = (d) => d.id.type !== "Identifier" || state.opts.pick.includes(d.id.name);
225
- if (declNode.declarations.every(isPicked)) {
226
- break;
227
- }
228
- // Split into one declaration per declarator (preserving
229
- // evaluation order) and export only the picked ones.
230
- // Unpicked bindings are kept since picked exports may
231
- // reference them; the sweep below removes them again if
232
- // unreferenced.
233
- const statements = declNode.declarations.map(d => {
234
- const single = t.variableDeclaration(declNode.kind, [d]);
235
- single.declare = declNode.declare;
236
- return isPicked(d) ? t.exportNamedDeclaration(single, []) : single;
237
- });
238
- const newPaths = exportNamedPath.replaceWithMultiple(statements);
239
- for (const p of newPaths) {
240
- if (!p.isVariableDeclaration())
241
- continue;
242
- for (const d of p.get("declarations")) {
243
- if (d.node.id.type === "Identifier") {
244
- const id = d.get("id");
245
- state.refs.add(id);
246
- state.candidates.add(id);
247
- }
248
- }
249
- }
250
- break;
251
- }
252
- default: {
253
- break;
254
- }
255
- }
256
- },
257
- FunctionDeclaration: markFunction,
258
- FunctionExpression: markFunction,
259
- ArrowFunctionExpression: markFunction,
260
- ClassDeclaration: markFunction,
261
- ClassExpression: markFunction,
262
- ImportSpecifier: markImport,
263
- ImportDefaultSpecifier: markImport,
264
- ImportNamespaceSpecifier: markImport,
265
- ImportDeclaration: (path, state) => {
266
- if (path.node.source.value.endsWith(".css") &&
267
- state.opts.pick &&
268
- !state.opts.pick.includes("$css")) {
269
- path.remove();
270
- }
271
- }
272
- }, state);
273
- // References alone cannot distinguish live bindings from dead cycles.
274
- // Build a graph of localized exports and retain only those reachable
275
- // from runtime code outside that graph.
276
- path.scope.crawl();
277
- const tracked = new Map();
278
- const candidateOwners = new Map();
279
- const addOwner = (owners, binding, owner) => {
280
- const paths = owners.get(binding) ?? new Set();
281
- paths.add(owner);
282
- owners.set(binding, paths);
283
- };
284
- for (const identifier of state.refs) {
285
- if (identifier.node?.type !== "Identifier" || !identifier.parentPath)
286
- continue;
287
- const binding = identifier.scope.getBinding(identifier.node.name);
288
- if (binding)
289
- addOwner(tracked, binding, identifier.parentPath);
290
- }
291
- for (const identifier of state.candidates) {
292
- if (!identifier.parentPath)
293
- continue;
294
- const binding = identifier.scope.getBinding(identifier.node.name);
295
- if (!binding)
296
- continue;
297
- addOwner(tracked, binding, identifier.parentPath);
298
- addOwner(candidateOwners, binding, identifier.parentPath);
299
- }
300
- const dependencies = new Map();
301
- const live = new Set();
302
- for (const [binding, owners] of tracked) {
303
- dependencies.set(binding, new Set());
304
- if ([...owners].some(owner => owner.findParent(parent => parent.isExportNamedDeclaration() || parent.isExportDefaultDeclaration()))) {
305
- live.add(binding);
306
- }
307
- }
308
- const ownerBindings = new Map();
309
- for (const [binding, owners] of tracked) {
310
- for (const owner of owners) {
311
- ownerBindings.set(owner, binding);
312
- }
313
- }
314
- const owningBinding = (reference) => {
315
- const owner = reference.find(parent => ownerBindings.has(parent));
316
- return owner && ownerBindings.get(owner);
317
- };
318
- for (const target of tracked.keys()) {
319
- const references = target.constantViolations.concat(runtimeReferences(target));
320
- for (const reference of references) {
321
- const source = owningBinding(reference);
322
- if (source)
323
- dependencies.get(source)?.add(target);
324
- else
325
- live.add(target);
326
- }
327
- }
328
- const pending = [...live];
329
- while (pending.length) {
330
- const source = pending.pop();
331
- for (const dependency of dependencies.get(source) ?? []) {
332
- if (live.has(dependency))
333
- continue;
334
- live.add(dependency);
335
- pending.push(dependency);
336
- }
337
- }
338
- const deadCandidates = new Set([...candidateOwners.keys()].filter(candidate => !live.has(candidate)));
339
- path.traverse({
340
- VariableDeclarator(variablePath) {
341
- if (variablePath.node.id.type !== "Identifier")
342
- return;
343
- const binding = variablePath.scope.getBinding(variablePath.node.id.name);
344
- if (binding && deadCandidates.has(binding))
345
- variablePath.remove();
346
- }
347
- });
348
- for (const [candidate, owners] of candidateOwners) {
349
- if (!deadCandidates.has(candidate))
350
- continue;
351
- for (const owner of owners) {
352
- if (!owner.removed && !owner.isVariableDeclarator())
353
- owner.remove();
354
- }
355
- }
356
- const refs = state.refs;
357
- const refNodes = new Set([...refs].map(ref => ref.node));
358
- let count = 0;
359
- const sweepFunction = (sweepPath) => {
360
- const ident = getIdentifier(sweepPath);
361
- if (ident && ident.node && refNodes.has(ident.node) && !isIdentifierReferenced(ident)) {
362
- ++count;
363
- if (t.isAssignmentExpression(sweepPath.parentPath) ||
364
- t.isVariableDeclarator(sweepPath.parentPath)) {
365
- sweepPath.parentPath.remove();
366
- }
367
- else {
368
- sweepPath.remove();
369
- }
370
- }
371
- };
372
- function sweepImport(sweepPath) {
373
- const local = sweepPath.get("local");
374
- if (refNodes.has(local.node) && !isIdentifierReferenced(local)) {
375
- ++count;
376
- sweepPath.remove();
377
- if (sweepPath.parent.specifiers.length === 0) {
378
- sweepPath.parentPath.remove();
379
- }
380
- }
381
- }
382
- do {
383
- path.scope.crawl();
384
- count = 0;
385
- path.traverse({
386
- VariableDeclarator(variablePath) {
387
- if (variablePath.node.id.type === "Identifier") {
388
- const local = variablePath.get("id");
389
- if (refNodes.has(local.node) && !isIdentifierReferenced(local)) {
390
- ++count;
391
- variablePath.remove();
392
- }
393
- }
394
- else if (variablePath.node.id.type === "ObjectPattern") {
395
- const pattern = variablePath.get("id");
396
- const beforeCount = count;
397
- const properties = pattern.get("properties");
398
- properties.forEach(p => {
399
- const local = p.get(p.node.type === "ObjectProperty"
400
- ? "value"
401
- : p.node.type === "RestElement"
402
- ? "argument"
403
- : (() => {
404
- throw new Error("invariant");
405
- })());
406
- if (refNodes.has(local.node) && !isIdentifierReferenced(local)) {
407
- ++count;
408
- p.remove();
409
- }
410
- });
411
- if (beforeCount !== count && pattern.get("properties").length < 1) {
412
- variablePath.remove();
413
- }
414
- }
415
- else if (variablePath.node.id.type === "ArrayPattern") {
416
- const pattern = variablePath.get("id");
417
- const beforeCount = count;
418
- const elements = pattern.get("elements");
419
- elements.forEach(e => {
420
- let local;
421
- if (e.node && e.node.type === "Identifier") {
422
- local = e;
423
- }
424
- else if (e.node && e.node.type === "RestElement") {
425
- local = e.get("argument");
426
- }
427
- else {
428
- return;
429
- }
430
- if (refNodes.has(local.node) && !isIdentifierReferenced(local)) {
431
- ++count;
432
- e.remove();
433
- }
434
- });
435
- if (beforeCount !== count && pattern.get("elements").length < 1) {
436
- variablePath.remove();
437
- }
438
- }
439
- },
440
- FunctionDeclaration: sweepFunction,
441
- FunctionExpression: sweepFunction,
442
- ArrowFunctionExpression: sweepFunction,
443
- ClassDeclaration: sweepFunction,
444
- ClassExpression: sweepFunction,
445
- ImportSpecifier: sweepImport,
446
- ImportDefaultSpecifier: sweepImport,
447
- ImportNamespaceSpecifier: sweepImport
448
- });
449
- } while (count);
450
- }
451
- }
452
- }
453
- };
454
- }
455
48
  export function treeShake() {
456
- async function transform(id, code) {
457
- const [path, queryString] = id.split("?");
458
- const query = new URLSearchParams(queryString);
459
- if (query.has("pick")) {
460
- const babel = await import("@babel/core");
461
- const transformed = await babel.transformAsync(code, {
462
- plugins: [[treeShakeTransform, { pick: query.getAll("pick") }]],
463
- parserOpts: {
464
- plugins: ["jsx", "typescript"]
465
- },
466
- filename: basename(id),
467
- ast: false,
468
- sourceMaps: true,
469
- configFile: false,
470
- babelrc: false,
471
- sourceFileName: id
472
- });
473
- return transformed;
474
- }
475
- }
476
49
  return {
477
50
  name: "filesystem-routing:tree-shake",
478
51
  enforce: "pre",
479
- async transform(code, id) {
52
+ transform(code, id) {
480
53
  const [path, queryString] = id.split("?");
481
54
  if (!path)
482
55
  return;
483
56
  const query = new URLSearchParams(queryString);
484
- const ext = path.split(".").pop();
485
- if (!ext)
486
- return;
487
- if (query.has("pick") && ["js", "jsx", "ts", "tsx"].includes(ext)) {
488
- const transformed = await transform(id, code);
489
- if (!transformed?.code)
490
- return;
491
- return {
492
- code: transformed.code,
493
- map: transformed.map
494
- };
57
+ if (query.has("pick") && PICKABLE_EXTENSIONS.includes(extname(path).slice(1))) {
58
+ return pickExports(code, id, query.getAll("pick"));
495
59
  }
496
60
  }
497
61
  };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "filesystem-routing",
3
3
  "description": "Router-neutral file-system routing: scans a route directory into a neutral route manifest with pluggable conventions and delivery adapters",
4
4
  "license": "MIT",
5
- "version": "0.4.2",
5
+ "version": "0.5.0",
6
6
  "author": "Ryan Carniato",
7
7
  "homepage": "https://github.com/solidjs/filesystem-routing#readme",
8
8
  "repository": {
@@ -57,11 +57,9 @@
57
57
  "types.d.ts"
58
58
  ],
59
59
  "dependencies": {
60
- "@babel/core": "^7.29.0",
61
- "@types/babel__core": "^7.20.5",
62
- "@types/babel__traverse": "^7.28.0",
63
60
  "@types/micromatch": "^4.0.10",
64
61
  "fast-glob": "^3.3.3",
62
+ "magic-string": "^0.30.21",
65
63
  "micromatch": "^4.0.8",
66
64
  "oxc-parser": "^0.139.0",
67
65
  "radix3": "^1.1.2"
@@ -79,7 +77,9 @@
79
77
  }
80
78
  },
81
79
  "devDependencies": {
80
+ "@babel/core": "^7.29.0",
82
81
  "@changesets/cli": "^2.31.1",
82
+ "@jridgewell/trace-mapping": "^0.3.31",
83
83
  "@tsrx/oxc": "^0.9.0",
84
84
  "@types/node": "^22.10.0",
85
85
  "prettier": "^3.4.1",