filesystem-routing 0.1.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.
@@ -0,0 +1,215 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, relative, resolve } from "node:path";
3
+ import { PageFileSystemRouter } from "../convention.js";
4
+ import { normalizePath } from "../router.js";
5
+ import { buildRouteTree } from "../tree.js";
6
+ import { DEFAULT_EXTENSIONS, moduleId } from "./constants.js";
7
+ import { fileSystemWatcher } from "./fs-watcher.js";
8
+ import { treeShake } from "./tree-shake.js";
9
+ import { serializeTypes } from "./types.js";
10
+ export { DEFAULT_EXTENSIONS, moduleId };
11
+ export { treeShake } from "./tree-shake.js";
12
+ export { fileSystemWatcher } from "./fs-watcher.js";
13
+ /**
14
+ * The Vite delivery adapter for `filesystem-routing`.
15
+ *
16
+ * Serializes the neutral route manifest into the virtual module — module refs
17
+ * become code-split dynamic imports (`$`-prefixed keys) or eagerly required
18
+ * static imports (`$$`-prefixed keys) — and keeps it hot as route files are
19
+ * added, changed and removed.
20
+ *
21
+ * The module serves two views of the same entries: the default export is the
22
+ * flat manifest, and `pageRoutes` is the page entries nested by path with
23
+ * `(group)` segments stripped, so emission adapters don't each reimplement
24
+ * the tree.
25
+ */
26
+ export function fileRoutes(options = {}) {
27
+ const virtualId = options.moduleId ?? moduleId;
28
+ const buildInputs = options.buildInputs === undefined
29
+ ? []
30
+ : Array.isArray(options.buildInputs)
31
+ ? options.buildInputs
32
+ : [options.buildInputs];
33
+ let defaultRouter = options.router;
34
+ let root = process.cwd();
35
+ const getRouter = (environment) => options.routers?.[environment] ?? defaultRouter;
36
+ /**
37
+ * The manifest the declaration describes. With per-environment routers the
38
+ * browser's is the one an app's routes are built from, so it wins.
39
+ */
40
+ const typesRouter = () => getRouter("client");
41
+ const typesFile = () => options.types === undefined || options.types === false
42
+ ? undefined
43
+ : resolve(root, options.types === true ? "file-routes.d.ts" : options.types);
44
+ async function writeTypes() {
45
+ const file = typesFile();
46
+ const router = typesRouter();
47
+ if (!file || !router)
48
+ return;
49
+ const routes = (await router.getRoutes()) ?? [];
50
+ const contents = serializeTypes(virtualId, routes, buildRouteTree(routes.filter(route => route.page)), dirname(file));
51
+ // Only touch the file when it actually changes: every environment calls
52
+ // this, and rewriting it would churn the watcher for no reason.
53
+ let previous;
54
+ try {
55
+ previous = readFileSync(file, "utf-8");
56
+ }
57
+ catch { }
58
+ if (previous === contents)
59
+ return;
60
+ mkdirSync(dirname(file), { recursive: true });
61
+ writeFileSync(file, contents);
62
+ }
63
+ /** The id a route module ref is loaded from: its source plus its picks. */
64
+ const toModuleId = (ref) => `${ref.src}?${ref.pick.map(pick => `pick=${pick}`).join("&")}`;
65
+ return [
66
+ {
67
+ name: "filesystem-routing",
68
+ enforce: "pre",
69
+ config() {
70
+ // Packages importing the virtual module (which only this plugin can
71
+ // resolve) must stay out of esbuild prebundling.
72
+ if (!options.optimizeDepsExclude?.length)
73
+ return;
74
+ return {
75
+ optimizeDeps: {
76
+ exclude: options.optimizeDepsExclude
77
+ }
78
+ };
79
+ },
80
+ configResolved(config) {
81
+ root = config.root;
82
+ if (!defaultRouter) {
83
+ defaultRouter = new PageFileSystemRouter({
84
+ dir: normalizePath(resolve(config.root, options.dir ?? "src/routes")),
85
+ extensions: options.extensions ?? DEFAULT_EXTENSIONS,
86
+ components: options.components,
87
+ httpMethods: options.httpMethods,
88
+ toPath: options.toPath,
89
+ toRoute: options.toRoute
90
+ });
91
+ }
92
+ },
93
+ async buildStart() {
94
+ await writeTypes();
95
+ },
96
+ async configEnvironment(name, _config, env) {
97
+ if (env.command !== "build" || !buildInputs.includes(name))
98
+ return;
99
+ const router = getRouter(name);
100
+ if (!router)
101
+ return;
102
+ // Every code-split route module is an entry of its own, so the
103
+ // manifest's dynamic imports resolve to real chunks. `$$` refs are
104
+ // inlined into the manifest and need no entry.
105
+ const input = [];
106
+ for (const route of await router.getRoutes()) {
107
+ for (const [key, ref] of Object.entries(route)) {
108
+ if (ref && key.startsWith("$") && !key.startsWith("$$")) {
109
+ input.push(toModuleId(ref));
110
+ }
111
+ }
112
+ }
113
+ if (!input.length)
114
+ return;
115
+ return { build: { rollupOptions: { input } } };
116
+ },
117
+ resolveId(source) {
118
+ if (source === virtualId)
119
+ return virtualId;
120
+ },
121
+ async load(loadedId) {
122
+ if (loadedId !== virtualId)
123
+ return;
124
+ const root = this.environment.config.root;
125
+ const isBuild = this.environment.mode === "build";
126
+ const js = jsCode();
127
+ const router = getRouter(this.environment.name);
128
+ const routes = (router ? await router.getRoutes() : []) ?? [];
129
+ const serializeEntry = (entry) => JSON.stringify(entry, (key, value) => {
130
+ if (value === undefined)
131
+ return undefined;
132
+ if (key.startsWith("$$")) {
133
+ const buildId = toModuleId(value);
134
+ const refs = {};
135
+ for (const pick of value.pick) {
136
+ refs[pick] = js.addNamedImport(pick, buildId);
137
+ }
138
+ return {
139
+ require: `_$() => ({ ${Object.entries(refs)
140
+ .map(([pick, namedImport]) => `'${pick}': ${namedImport}`)
141
+ .join(", ")} })$_`
142
+ };
143
+ }
144
+ else if (key.startsWith("$")) {
145
+ const buildId = toModuleId(value);
146
+ return {
147
+ src: relative(root, buildId),
148
+ build: isBuild ? `_$() => import('${buildId}')$_` : undefined,
149
+ import: `_$() => import('${buildId}')$_`
150
+ };
151
+ }
152
+ return value;
153
+ })
154
+ .replaceAll('"_$(', "(")
155
+ .replaceAll(')$_"', ")");
156
+ // Entries are emitted once and both views reference them, so the
157
+ // nested view costs its own paths and nothing else.
158
+ const bindings = routes.map((route, index) => `const route${index} = ${serializeEntry(route)};`);
159
+ const tree = buildRouteTree(routes
160
+ .map((route, index) => ({ path: route.path, page: route.page, index }))
161
+ .filter(stub => stub.page));
162
+ const serializeTree = (nodes) => `[${nodes
163
+ .map(node => {
164
+ const fields = [
165
+ `...route${node.index}`,
166
+ `id: ${JSON.stringify(node.id)}`,
167
+ `path: ${JSON.stringify(node.path)}`
168
+ ];
169
+ if (node.children)
170
+ fields.push(`children: ${serializeTree(node.children)}`);
171
+ return `{ ${fields.join(", ")} }`;
172
+ })
173
+ .join(", ")}]`;
174
+ return `${js.getImportStatements()}
175
+ ${bindings.join("\n")}
176
+ const routes = [${routes.map((_, index) => `route${index}`).join(", ")}];
177
+ export default routes;
178
+ export const pageRoutes = ${serializeTree(tree)};
179
+ `;
180
+ }
181
+ },
182
+ treeShake(),
183
+ fileSystemWatcher(getRouter, virtualId, writeTypes)
184
+ ];
185
+ }
186
+ function jsCode() {
187
+ const imports = new Map();
188
+ let vars = 0;
189
+ function addNamedImport(name, source) {
190
+ let names = imports.get(source);
191
+ if (!names) {
192
+ names = {};
193
+ imports.set(source, names);
194
+ }
195
+ // The same export can be reached through more than one ref; reuse the
196
+ // binding instead of importing it twice.
197
+ const existing = names[name];
198
+ if (existing)
199
+ return existing;
200
+ const alias = "routeData" + vars++;
201
+ names[name] = alias;
202
+ return alias;
203
+ }
204
+ const getImportStatements = () => {
205
+ return [...imports.entries()]
206
+ .map(([source, names]) => `import { ${Object.entries(names)
207
+ .map(([name, alias]) => `${name} as ${alias}`)
208
+ .join(", ")} } from '${source}';`)
209
+ .join("\n");
210
+ };
211
+ return {
212
+ addNamedImport,
213
+ getImportStatements
214
+ };
215
+ }
@@ -0,0 +1,2 @@
1
+ import type { Plugin } from "vite";
2
+ export declare function treeShake(): Plugin;
@@ -0,0 +1,448 @@
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 } from "node:path";
5
+ function treeShakeTransform({ types: t }) {
6
+ function getIdentifier(path) {
7
+ const parentPath = path.parentPath;
8
+ if (parentPath.type === "VariableDeclarator") {
9
+ const pp = parentPath;
10
+ const name = pp.get("id");
11
+ return name.node.type === "Identifier" ? name : null;
12
+ }
13
+ if (parentPath.type === "AssignmentExpression") {
14
+ const pp = parentPath;
15
+ const name = pp.get("left");
16
+ return name.node.type === "Identifier" ? name : null;
17
+ }
18
+ if (path.node.type === "ArrowFunctionExpression") {
19
+ return null;
20
+ }
21
+ return path.node.id && path.node.id.type === "Identifier" ? path.get("id") : null;
22
+ }
23
+ function isTypeOnlyReference(path) {
24
+ return path.getAncestry().some(parent => {
25
+ if (parent.isTSType())
26
+ return true;
27
+ if (parent.isExportNamedDeclaration())
28
+ return parent.node.exportKind === "type";
29
+ if (parent.isExportSpecifier())
30
+ return parent.node.exportKind === "type";
31
+ return false;
32
+ });
33
+ }
34
+ function runtimeReferences(binding) {
35
+ return binding.referencePaths.filter(ref => !isTypeOnlyReference(ref));
36
+ }
37
+ function isIdentifierReferenced(ident, includeTypes = false) {
38
+ const b = ident.scope.getBinding(ident.node.name);
39
+ if (b) {
40
+ const references = b.constantViolations.concat(includeTypes ? b.referencePaths : runtimeReferences(b));
41
+ if (!references.length)
42
+ return false;
43
+ if (b.path.type === "FunctionDeclaration") {
44
+ return !references.every(ref => ref.findParent(p => p === b.path));
45
+ }
46
+ return true;
47
+ }
48
+ return false;
49
+ }
50
+ function markFunction(path, state) {
51
+ const ident = getIdentifier(path);
52
+ if (ident && ident.node && isIdentifierReferenced(ident, true)) {
53
+ state.refs.add(ident);
54
+ }
55
+ }
56
+ function markImport(path, state) {
57
+ const local = path.get("local");
58
+ if (isIdentifierReferenced(local, true)) {
59
+ state.refs.add(local);
60
+ }
61
+ }
62
+ return {
63
+ visitor: {
64
+ Program: {
65
+ enter(path, state) {
66
+ state.refs = new Set();
67
+ state.candidates = new Set();
68
+ state.done = false;
69
+ path.traverse({
70
+ VariableDeclarator(variablePath, variableState) {
71
+ if (variablePath.node.id.type === "Identifier") {
72
+ const local = variablePath.get("id");
73
+ if (isIdentifierReferenced(local, true)) {
74
+ variableState.refs.add(local);
75
+ }
76
+ }
77
+ else if (variablePath.node.id.type === "ObjectPattern") {
78
+ const pattern = variablePath.get("id");
79
+ const properties = pattern.get("properties");
80
+ properties.forEach(p => {
81
+ const local = p.get(p.node.type === "ObjectProperty"
82
+ ? "value"
83
+ : p.node.type === "RestElement"
84
+ ? "argument"
85
+ : (() => {
86
+ throw new Error("invariant");
87
+ })());
88
+ if (isIdentifierReferenced(local, true)) {
89
+ variableState.refs.add(local);
90
+ }
91
+ });
92
+ }
93
+ else if (variablePath.node.id.type === "ArrayPattern") {
94
+ const pattern = variablePath.get("id");
95
+ const elements = pattern.get("elements");
96
+ elements.forEach(e => {
97
+ let local;
98
+ if (e.node && e.node.type === "Identifier") {
99
+ local = e;
100
+ }
101
+ else if (e.node && e.node.type === "RestElement") {
102
+ local = e.get("argument");
103
+ }
104
+ else {
105
+ return;
106
+ }
107
+ if (isIdentifierReferenced(local, true)) {
108
+ variableState.refs.add(local);
109
+ }
110
+ });
111
+ }
112
+ },
113
+ ExportDefaultDeclaration(exportNamedPath) {
114
+ if (state.opts.pick && !state.opts.pick.includes("default")) {
115
+ const decl = exportNamedPath.node.declaration;
116
+ // A named function/class declaration creates a module-scope
117
+ // binding that picked exports may reference, so only strip
118
+ // the `export default`; the sweep below removes the
119
+ // declaration again if nothing references it.
120
+ if ((t.isFunctionDeclaration(decl) || t.isClassDeclaration(decl)) && decl.id) {
121
+ const [newPath] = exportNamedPath.replaceWith(decl);
122
+ const id = newPath.get("id");
123
+ state.refs.add(id);
124
+ state.candidates.add(id);
125
+ }
126
+ else {
127
+ exportNamedPath.remove();
128
+ }
129
+ }
130
+ },
131
+ ExportNamedDeclaration(exportNamedPath) {
132
+ // No pick list means nothing gets pruned.
133
+ if (!state.opts.pick) {
134
+ return;
135
+ }
136
+ const specifiers = exportNamedPath.get("specifiers");
137
+ if (specifiers.length) {
138
+ specifiers.forEach(s => {
139
+ const exported = s.node.exported;
140
+ const exportedName = t.isIdentifier(exported) ? exported.name : exported.value;
141
+ if (!state.opts.pick.includes(exportedName)) {
142
+ s.remove();
143
+ }
144
+ });
145
+ if (exportNamedPath.node.specifiers.length < 1) {
146
+ exportNamedPath.remove();
147
+ }
148
+ return;
149
+ }
150
+ const decl = exportNamedPath.get("declaration");
151
+ if (decl == null || decl.node == null) {
152
+ return;
153
+ }
154
+ switch (decl.node.type) {
155
+ case "FunctionDeclaration":
156
+ case "ClassDeclaration": {
157
+ const name = decl.node.id?.name;
158
+ if (name && state.opts.pick && !state.opts.pick.includes(name)) {
159
+ // Keep the declaration in module scope since picked
160
+ // exports may reference it; the sweep below removes it
161
+ // again if unreferenced.
162
+ const [newPath] = exportNamedPath.replaceWith(decl.node);
163
+ const id = newPath.get("id");
164
+ state.refs.add(id);
165
+ state.candidates.add(id);
166
+ }
167
+ break;
168
+ }
169
+ case "VariableDeclaration": {
170
+ const declNode = decl.node;
171
+ // Destructuring declarators are conservatively treated as
172
+ // picked (matching the previous behavior of leaving them
173
+ // untouched).
174
+ const isPicked = (d) => d.id.type !== "Identifier" || state.opts.pick.includes(d.id.name);
175
+ if (declNode.declarations.every(isPicked)) {
176
+ break;
177
+ }
178
+ // Split into one declaration per declarator (preserving
179
+ // evaluation order) and export only the picked ones.
180
+ // Unpicked bindings are kept since picked exports may
181
+ // reference them; the sweep below removes them again if
182
+ // unreferenced.
183
+ const statements = declNode.declarations.map(d => {
184
+ const single = t.variableDeclaration(declNode.kind, [d]);
185
+ single.declare = declNode.declare;
186
+ return isPicked(d) ? t.exportNamedDeclaration(single, []) : single;
187
+ });
188
+ const newPaths = exportNamedPath.replaceWithMultiple(statements);
189
+ for (const p of newPaths) {
190
+ if (!p.isVariableDeclaration())
191
+ continue;
192
+ for (const d of p.get("declarations")) {
193
+ if (d.node.id.type === "Identifier") {
194
+ const id = d.get("id");
195
+ state.refs.add(id);
196
+ state.candidates.add(id);
197
+ }
198
+ }
199
+ }
200
+ break;
201
+ }
202
+ default: {
203
+ break;
204
+ }
205
+ }
206
+ },
207
+ FunctionDeclaration: markFunction,
208
+ FunctionExpression: markFunction,
209
+ ArrowFunctionExpression: markFunction,
210
+ ClassDeclaration: markFunction,
211
+ ClassExpression: markFunction,
212
+ ImportSpecifier: markImport,
213
+ ImportDefaultSpecifier: markImport,
214
+ ImportNamespaceSpecifier: markImport,
215
+ ImportDeclaration: (path, state) => {
216
+ if (path.node.source.value.endsWith(".css") &&
217
+ state.opts.pick &&
218
+ !state.opts.pick.includes("$css")) {
219
+ path.remove();
220
+ }
221
+ }
222
+ }, state);
223
+ // References alone cannot distinguish live bindings from dead cycles.
224
+ // Build a graph of localized exports and retain only those reachable
225
+ // from runtime code outside that graph.
226
+ path.scope.crawl();
227
+ const tracked = new Map();
228
+ const candidateOwners = new Map();
229
+ const addOwner = (owners, binding, owner) => {
230
+ const paths = owners.get(binding) ?? new Set();
231
+ paths.add(owner);
232
+ owners.set(binding, paths);
233
+ };
234
+ for (const identifier of state.refs) {
235
+ if (identifier.node?.type !== "Identifier" || !identifier.parentPath)
236
+ continue;
237
+ const binding = identifier.scope.getBinding(identifier.node.name);
238
+ if (binding)
239
+ addOwner(tracked, binding, identifier.parentPath);
240
+ }
241
+ for (const identifier of state.candidates) {
242
+ if (!identifier.parentPath)
243
+ continue;
244
+ const binding = identifier.scope.getBinding(identifier.node.name);
245
+ if (!binding)
246
+ continue;
247
+ addOwner(tracked, binding, identifier.parentPath);
248
+ addOwner(candidateOwners, binding, identifier.parentPath);
249
+ }
250
+ const dependencies = new Map();
251
+ const live = new Set();
252
+ for (const [binding, owners] of tracked) {
253
+ dependencies.set(binding, new Set());
254
+ if ([...owners].some(owner => owner.findParent(parent => parent.isExportNamedDeclaration() || parent.isExportDefaultDeclaration()))) {
255
+ live.add(binding);
256
+ }
257
+ }
258
+ const ownerBindings = new Map();
259
+ for (const [binding, owners] of tracked) {
260
+ for (const owner of owners) {
261
+ ownerBindings.set(owner, binding);
262
+ }
263
+ }
264
+ const owningBinding = (reference) => {
265
+ const owner = reference.find(parent => ownerBindings.has(parent));
266
+ return owner && ownerBindings.get(owner);
267
+ };
268
+ for (const target of tracked.keys()) {
269
+ const references = target.constantViolations.concat(runtimeReferences(target));
270
+ for (const reference of references) {
271
+ const source = owningBinding(reference);
272
+ if (source)
273
+ dependencies.get(source)?.add(target);
274
+ else
275
+ live.add(target);
276
+ }
277
+ }
278
+ const pending = [...live];
279
+ while (pending.length) {
280
+ const source = pending.pop();
281
+ for (const dependency of dependencies.get(source) ?? []) {
282
+ if (live.has(dependency))
283
+ continue;
284
+ live.add(dependency);
285
+ pending.push(dependency);
286
+ }
287
+ }
288
+ const deadCandidates = new Set([...candidateOwners.keys()].filter(candidate => !live.has(candidate)));
289
+ path.traverse({
290
+ VariableDeclarator(variablePath) {
291
+ if (variablePath.node.id.type !== "Identifier")
292
+ return;
293
+ const binding = variablePath.scope.getBinding(variablePath.node.id.name);
294
+ if (binding && deadCandidates.has(binding))
295
+ variablePath.remove();
296
+ }
297
+ });
298
+ for (const [candidate, owners] of candidateOwners) {
299
+ if (!deadCandidates.has(candidate))
300
+ continue;
301
+ for (const owner of owners) {
302
+ if (!owner.removed && !owner.isVariableDeclarator())
303
+ owner.remove();
304
+ }
305
+ }
306
+ const refs = state.refs;
307
+ const refNodes = new Set([...refs].map(ref => ref.node));
308
+ let count = 0;
309
+ const sweepFunction = (sweepPath) => {
310
+ const ident = getIdentifier(sweepPath);
311
+ if (ident && ident.node && refNodes.has(ident.node) && !isIdentifierReferenced(ident)) {
312
+ ++count;
313
+ if (t.isAssignmentExpression(sweepPath.parentPath) ||
314
+ t.isVariableDeclarator(sweepPath.parentPath)) {
315
+ sweepPath.parentPath.remove();
316
+ }
317
+ else {
318
+ sweepPath.remove();
319
+ }
320
+ }
321
+ };
322
+ function sweepImport(sweepPath) {
323
+ const local = sweepPath.get("local");
324
+ if (refNodes.has(local.node) && !isIdentifierReferenced(local)) {
325
+ ++count;
326
+ sweepPath.remove();
327
+ if (sweepPath.parent.specifiers.length === 0) {
328
+ sweepPath.parentPath.remove();
329
+ }
330
+ }
331
+ }
332
+ do {
333
+ path.scope.crawl();
334
+ count = 0;
335
+ path.traverse({
336
+ VariableDeclarator(variablePath) {
337
+ if (variablePath.node.id.type === "Identifier") {
338
+ const local = variablePath.get("id");
339
+ if (refNodes.has(local.node) && !isIdentifierReferenced(local)) {
340
+ ++count;
341
+ variablePath.remove();
342
+ }
343
+ }
344
+ else if (variablePath.node.id.type === "ObjectPattern") {
345
+ const pattern = variablePath.get("id");
346
+ const beforeCount = count;
347
+ const properties = pattern.get("properties");
348
+ properties.forEach(p => {
349
+ const local = p.get(p.node.type === "ObjectProperty"
350
+ ? "value"
351
+ : p.node.type === "RestElement"
352
+ ? "argument"
353
+ : (() => {
354
+ throw new Error("invariant");
355
+ })());
356
+ if (refNodes.has(local.node) && !isIdentifierReferenced(local)) {
357
+ ++count;
358
+ p.remove();
359
+ }
360
+ });
361
+ if (beforeCount !== count && pattern.get("properties").length < 1) {
362
+ variablePath.remove();
363
+ }
364
+ }
365
+ else if (variablePath.node.id.type === "ArrayPattern") {
366
+ const pattern = variablePath.get("id");
367
+ const beforeCount = count;
368
+ const elements = pattern.get("elements");
369
+ elements.forEach(e => {
370
+ let local;
371
+ if (e.node && e.node.type === "Identifier") {
372
+ local = e;
373
+ }
374
+ else if (e.node && e.node.type === "RestElement") {
375
+ local = e.get("argument");
376
+ }
377
+ else {
378
+ return;
379
+ }
380
+ if (refNodes.has(local.node) && !isIdentifierReferenced(local)) {
381
+ ++count;
382
+ e.remove();
383
+ }
384
+ });
385
+ if (beforeCount !== count && pattern.get("elements").length < 1) {
386
+ variablePath.remove();
387
+ }
388
+ }
389
+ },
390
+ FunctionDeclaration: sweepFunction,
391
+ FunctionExpression: sweepFunction,
392
+ ArrowFunctionExpression: sweepFunction,
393
+ ClassDeclaration: sweepFunction,
394
+ ClassExpression: sweepFunction,
395
+ ImportSpecifier: sweepImport,
396
+ ImportDefaultSpecifier: sweepImport,
397
+ ImportNamespaceSpecifier: sweepImport
398
+ });
399
+ } while (count);
400
+ }
401
+ }
402
+ }
403
+ };
404
+ }
405
+ export function treeShake() {
406
+ async function transform(id, code) {
407
+ const [path, queryString] = id.split("?");
408
+ const query = new URLSearchParams(queryString);
409
+ if (query.has("pick")) {
410
+ const babel = await import("@babel/core");
411
+ const transformed = await babel.transformAsync(code, {
412
+ plugins: [[treeShakeTransform, { pick: query.getAll("pick") }]],
413
+ parserOpts: {
414
+ plugins: ["jsx", "typescript"]
415
+ },
416
+ filename: basename(id),
417
+ ast: false,
418
+ sourceMaps: true,
419
+ configFile: false,
420
+ babelrc: false,
421
+ sourceFileName: id
422
+ });
423
+ return transformed;
424
+ }
425
+ }
426
+ return {
427
+ name: "filesystem-routing:tree-shake",
428
+ enforce: "pre",
429
+ async transform(code, id) {
430
+ const [path, queryString] = id.split("?");
431
+ if (!path)
432
+ return;
433
+ const query = new URLSearchParams(queryString);
434
+ const ext = path.split(".").pop();
435
+ if (!ext)
436
+ return;
437
+ if (query.has("pick") && ["js", "jsx", "ts", "tsx"].includes(ext)) {
438
+ const transformed = await transform(id, code);
439
+ if (!transformed?.code)
440
+ return;
441
+ return {
442
+ code: transformed.code,
443
+ map: transformed.map
444
+ };
445
+ }
446
+ }
447
+ };
448
+ }
@@ -0,0 +1,10 @@
1
+ import type { RouteManifestEntry } from "../manifest.ts";
2
+ import type { RouteTreeEntry } from "../tree.ts";
3
+ /**
4
+ * Builds the ambient declaration for `moduleId`. It is self-contained and
5
+ * replaces `filesystem-routing/types` — reference one or the other, never
6
+ * both, or TypeScript sees two declarations of the same module.
7
+ */
8
+ export declare function serializeTypes(moduleId: string, routes: readonly RouteManifestEntry[], pageRoutes: readonly RouteTreeEntry[],
9
+ /** Directory the declaration is written to; module specifiers are relative to it. */
10
+ from: string): string;