filesystem-routing 0.1.1 → 0.2.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/README.md +37 -24
- package/dist/api.d.ts +96 -0
- package/dist/api.js +120 -0
- package/dist/manifest.d.ts +5 -0
- package/dist/vite/index.d.ts +22 -1
- package/dist/vite/index.js +113 -19
- package/dist/vite/tree-shake.d.ts +27 -0
- package/dist/vite/tree-shake.js +51 -1
- package/dist/vite/types.d.ts +3 -1
- package/dist/vite/types.js +15 -9
- package/package.json +7 -2
- package/types.d.ts +4 -2
package/README.md
CHANGED
|
@@ -13,8 +13,8 @@ a delivery adapter that materializes the manifest into code.
|
|
|
13
13
|
| Scanning, filename convention, neutral route manifest | `filesystem-routing` |
|
|
14
14
|
| Nesting + `(group)` stripping | `filesystem-routing/tree` |
|
|
15
15
|
| Vite delivery (virtual module, HMR, code splitting, build inputs) | `filesystem-routing/vite` |
|
|
16
|
-
| `
|
|
17
|
-
|
|
|
16
|
+
| Request dispatch for `GET`/`POST` routes, as fetch middleware | `filesystem-routing/api` |
|
|
17
|
+
| `RouteDefinition` emission for Solid Router (code splitting, typed paths) | `@solidjs/router/fs` |
|
|
18
18
|
|
|
19
19
|
Nothing here is Solid-specific: the core is bundler-agnostic — it never imports
|
|
20
20
|
Vite — the Vite adapter is router-agnostic, and the conventions are the ones
|
|
@@ -47,9 +47,11 @@ const Router = createRouter({ routes: fileRoutes(pageRoutes) });
|
|
|
47
47
|
export const App = () => <Router>{props => <>{props.children}</>}</Router>;
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
The emission adapter
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
The emission adapter ships with the router — `@solidjs/router/fs` for Solid
|
|
51
|
+
Router; other routers mirror the same shape in their own packages. It never
|
|
52
|
+
imports a virtual module itself — the app does, so the adapter resolves
|
|
53
|
+
without the plugin, custom `moduleId`s work, and nothing needs excluding
|
|
54
|
+
from dependency prebundling.
|
|
53
55
|
|
|
54
56
|
Route modules live in `src/routes` (configurable via `fileRoutes({ dir })`).
|
|
55
57
|
A module is a page when it has a default export, and may export a `route`
|
|
@@ -161,25 +163,29 @@ pages get:
|
|
|
161
163
|
- The recognized set is `HEAD`/`GET`/`POST`/`PUT`/`DELETE`/`PATCH`/`OPTIONS`;
|
|
162
164
|
pass an array instead of `true` to change it.
|
|
163
165
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
166
|
+
Dispatch ships as fetch-style middleware — `filesystem-routing/api` matches
|
|
167
|
+
requests against the manifest's handler refs and composes into any
|
|
168
|
+
`(request, next)` chain, an SSR handler's middleware option included:
|
|
167
169
|
|
|
168
170
|
```ts
|
|
169
|
-
//
|
|
171
|
+
// src/middleware.ts
|
|
170
172
|
import routes from "virtual:file-routes";
|
|
173
|
+
import { createAPIHandler } from "filesystem-routing/api";
|
|
171
174
|
|
|
172
|
-
|
|
173
|
-
const ref = entry[`$${request.method}`];
|
|
174
|
-
if (!ref) return new Response(null, { status: 405 });
|
|
175
|
-
const handlers = await ref.import();
|
|
176
|
-
// a HEAD alias imports the GET handler
|
|
177
|
-
return (handlers[request.method] ?? handlers.GET)({ request, params });
|
|
178
|
-
}
|
|
175
|
+
export default [createAPIHandler(routes)];
|
|
179
176
|
```
|
|
180
177
|
|
|
181
|
-
|
|
182
|
-
|
|
178
|
+
Handlers receive the request event of the surrounding scope (the storage
|
|
179
|
+
`provideRequestEvent` establishes — pass `getEvent` to dispatch outside
|
|
180
|
+
one), with the matched `params` written onto it. Unmatched requests (no
|
|
181
|
+
route, or no handler for the method) advance the chain; `HEAD` falls back
|
|
182
|
+
to the `GET` handler; returned strings and JSON values are coerced into
|
|
183
|
+
responses. A `GET` handler may decline by returning `undefined`: a module
|
|
184
|
+
that is also a page falls through to the chain (its component renders
|
|
185
|
+
instead), a handler-only module answers 404. `createAPIMatcher` exposes the
|
|
186
|
+
matching alone for servers with their own dispatch shape.
|
|
187
|
+
|
|
188
|
+
Frameworks typically enable `httpMethods` on the
|
|
183
189
|
server environment's router only (SolidStart pairs it with `components:
|
|
184
190
|
false` in SPA mode, so the server manifest routes requests without shipping
|
|
185
191
|
page modules) — see the per-environment `routers` option below.
|
|
@@ -235,8 +241,14 @@ The scanner produces flat `RouteManifestEntry` objects:
|
|
|
235
241
|
|
|
236
242
|
The Vite adapter serializes the manifest into the virtual module, turning
|
|
237
243
|
`$`-prefixed refs into dynamic imports and `$$`-prefixed refs into static
|
|
238
|
-
imports, each tree-shaken down to the picked exports.
|
|
239
|
-
the
|
|
244
|
+
imports, each tree-shaken down to the picked exports. Splitting is not always
|
|
245
|
+
a win — on a small app the per-chunk overhead can leave the split bundle
|
|
246
|
+
larger — so `fileRoutes({ codeSplitting: false })` delivers `$` refs eagerly
|
|
247
|
+
too (`{ src, require }`, statically imported); emission adapters branch on
|
|
248
|
+
the delivered shape, so no second switch exists on their side. With
|
|
249
|
+
`@solidjs/router` this requires the next release (newer than
|
|
250
|
+
`2.0.0-next.14`) — against `next.14` or older, enabling the option fails at
|
|
251
|
+
first render. The module serves two views of the same entries:
|
|
240
252
|
|
|
241
253
|
```ts
|
|
242
254
|
import routes, { pageRoutes } from "virtual:file-routes";
|
|
@@ -247,10 +259,10 @@ with `(group)` segments stripped, so emission adapters don't each reimplement
|
|
|
247
259
|
the tree (`buildRouteTree` from `filesystem-routing/tree` is the same
|
|
248
260
|
function, for consumers holding only a flat manifest). An emission adapter is
|
|
249
261
|
a function taking manifest entries and returning the router's shape — see
|
|
250
|
-
`fileRoutes` in `@solidjs/router/fs` for Solid Router's,
|
|
251
|
-
other routers can mirror. Adapters take the manifest as an argument
|
|
252
|
-
than importing the virtual module, so they work standalone and with
|
|
253
|
-
module ids.
|
|
262
|
+
`fileRoutes` in `@solidjs/router/fs` for Solid Router's, an
|
|
263
|
+
adapter other routers can mirror. Adapters take the manifest as an argument
|
|
264
|
+
rather than importing the virtual module, so they work standalone and with
|
|
265
|
+
custom module ids.
|
|
254
266
|
|
|
255
267
|
Add `/// <reference types="filesystem-routing/types" />` to type the import.
|
|
256
268
|
|
|
@@ -299,6 +311,7 @@ fileRoutes({
|
|
|
299
311
|
| `httpMethods` | emit `$GET`, `$POST`, … refs for uppercase handler exports; a module with handlers but no default export routes without being a page |
|
|
300
312
|
| `components` | set `false` to route without emitting `$component` refs, keeping page modules out of that environment's bundle |
|
|
301
313
|
| `buildInputs` | environments whose build takes every code-split route module as an entry |
|
|
314
|
+
| `codeSplitting` | set `false` to deliver every module ref as an eager static import — no `lazy()` components, zero dynamic imports, one bundle — for small apps where per-chunk overhead outweighs the split (defaults to `true`; with `@solidjs/router` requires the next release, newer than `2.0.0-next.14`) |
|
|
302
315
|
| `moduleId` | the id the manifest is served from |
|
|
303
316
|
| `optimizeDepsExclude` | escape hatch for packages that import the virtual module, kept out of dep prebundling (defaults to `[]`) |
|
|
304
317
|
| `types` | write a declaration typing the manifest as a literal tuple, kept in step with the route directory |
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The API dispatch adapter: matches requests against the flat manifest's
|
|
3
|
+
* handler refs (`$GET`, `$POST`, …) and serves them as fetch-style
|
|
4
|
+
* middleware, so API routes compose into any `(request, next)` chain — an
|
|
5
|
+
* SSR handler's middleware option, a framework's request pipeline — with no
|
|
6
|
+
* bundler or server involvement.
|
|
7
|
+
*
|
|
8
|
+
* Ported from SolidStart's `server/routes.ts` (the radix3 matcher) and the
|
|
9
|
+
* method dispatch in its `server/handler.ts`.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The event a handler receives. Structurally the core request event —
|
|
13
|
+
* `getRequestEvent()` inside the handler sees the same object — with the
|
|
14
|
+
* matched route params written onto it before dispatch.
|
|
15
|
+
*/
|
|
16
|
+
export interface APIEvent {
|
|
17
|
+
request: Request;
|
|
18
|
+
params?: Record<string, string>;
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* An API handler: an uppercase method export of a route module. Return a
|
|
23
|
+
* `Response` to answer the request; strings and JSON values are coerced
|
|
24
|
+
* (`new Response(text)` / `Response.json(value)`). A `GET` handler may
|
|
25
|
+
* return `undefined` to decline, falling through to the rest of the chain
|
|
26
|
+
* when the module is also a page.
|
|
27
|
+
*/
|
|
28
|
+
export type APIHandler = (event: APIEvent) => unknown;
|
|
29
|
+
/** A manifest entry as the delivery adapter serves it. */
|
|
30
|
+
export interface FileRouteHandlers {
|
|
31
|
+
path: string;
|
|
32
|
+
page?: boolean;
|
|
33
|
+
$component?: unknown;
|
|
34
|
+
$HEAD?: APIHandlerRef;
|
|
35
|
+
$GET?: APIHandlerRef;
|
|
36
|
+
$POST?: APIHandlerRef;
|
|
37
|
+
$PUT?: APIHandlerRef;
|
|
38
|
+
$PATCH?: APIHandlerRef;
|
|
39
|
+
$DELETE?: APIHandlerRef;
|
|
40
|
+
[key: string]: unknown;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A handler module ref as delivered: code-split (`import()`) by default,
|
|
44
|
+
* eager (`require()`) when the delivery adapter runs with code splitting off.
|
|
45
|
+
*/
|
|
46
|
+
export type APIHandlerRef = {
|
|
47
|
+
import(): Promise<Record<string, APIHandler>>;
|
|
48
|
+
} | {
|
|
49
|
+
require(): Record<string, APIHandler>;
|
|
50
|
+
};
|
|
51
|
+
export interface APIMatch {
|
|
52
|
+
handler: APIHandlerRef;
|
|
53
|
+
params?: Record<string, string>;
|
|
54
|
+
/** `true` when the module also renders a page at this path. */
|
|
55
|
+
isPage: boolean;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Builds a matcher over the flat manifest's handler-carrying entries. Route
|
|
59
|
+
* paths translate from the neutral pattern language to the radix tree —
|
|
60
|
+
* `(group)` segments stripped, `*rest` catch-alls, static segments
|
|
61
|
+
* URL-encoded. Optional parameters are rejected (a radix tree cannot
|
|
62
|
+
* represent them) and duplicate paths are a configuration error.
|
|
63
|
+
*/
|
|
64
|
+
export declare function createAPIMatcher(routes: readonly FileRouteHandlers[]): (path: string, method: string) => APIMatch | undefined;
|
|
65
|
+
/** Strips a leading path base: `stripPathBase("/app/x", "/app")` → `/x`. */
|
|
66
|
+
export declare function stripPathBase(path: string, base: string): string;
|
|
67
|
+
export interface APIHandlerOptions {
|
|
68
|
+
/** A path base stripped from the URL before matching. Defaults to `/`. */
|
|
69
|
+
base?: string;
|
|
70
|
+
/**
|
|
71
|
+
* How the adapter reaches the request event it hands to handlers. Defaults
|
|
72
|
+
* to the core request-event scope (the storage `provideRequestEvent`
|
|
73
|
+
* establishes), which is where a middleware chain runs — override it when
|
|
74
|
+
* dispatching outside one, or to hand handlers a richer event.
|
|
75
|
+
*/
|
|
76
|
+
getEvent?: () => APIEvent;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Fetch-style middleware serving the manifest's API routes:
|
|
80
|
+
*
|
|
81
|
+
* ```ts
|
|
82
|
+
* import routes from "virtual:file-routes";
|
|
83
|
+
* import { createAPIHandler } from "filesystem-routing/api";
|
|
84
|
+
*
|
|
85
|
+
* export default [createAPIHandler(routes)];
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* Dispatch preserves SolidStart's semantics: unmatched requests (no route,
|
|
89
|
+
* or no handler for the method) advance the chain; `HEAD` falls back to the
|
|
90
|
+
* `GET` handler; matched params are written to `event.params` before the
|
|
91
|
+
* handler runs. A handler returning `undefined` declines the request —
|
|
92
|
+
* allowed for `GET` only (anything else throws): a page module falls
|
|
93
|
+
* through to the chain (its component renders instead), a handler-only
|
|
94
|
+
* module answers 404.
|
|
95
|
+
*/
|
|
96
|
+
export declare function createAPIHandler(routes: readonly FileRouteHandlers[], options?: APIHandlerOptions): (request: Request, next: (request?: Request) => Response | Promise<Response>) => Promise<Response>;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createRouter } from "radix3";
|
|
2
|
+
function containsHTTP(route) {
|
|
3
|
+
return route.$HEAD || route.$GET || route.$POST || route.$PUT || route.$PATCH || route.$DELETE;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Builds a matcher over the flat manifest's handler-carrying entries. Route
|
|
7
|
+
* paths translate from the neutral pattern language to the radix tree —
|
|
8
|
+
* `(group)` segments stripped, `*rest` catch-alls, static segments
|
|
9
|
+
* URL-encoded. Optional parameters are rejected (a radix tree cannot
|
|
10
|
+
* represent them) and duplicate paths are a configuration error.
|
|
11
|
+
*/
|
|
12
|
+
export function createAPIMatcher(routes) {
|
|
13
|
+
const router = createRouter({
|
|
14
|
+
routes: routes.reduce((memo, route) => {
|
|
15
|
+
if (!containsHTTP(route))
|
|
16
|
+
return memo;
|
|
17
|
+
const path = route.path
|
|
18
|
+
.replace(/\([^)/]+\)/g, "")
|
|
19
|
+
.replace(/\/+/g, "/")
|
|
20
|
+
.replace(/\*([^/]*)/g, (_, m) => `**:${m}`)
|
|
21
|
+
.split("/")
|
|
22
|
+
.map(s => (s.startsWith(":") || s.startsWith("*") ? s : encodeURIComponent(s)))
|
|
23
|
+
.join("/");
|
|
24
|
+
if (/:[^/]*\?/g.test(path)) {
|
|
25
|
+
throw new Error(`Optional parameters are not supported in API routes: ${path}`);
|
|
26
|
+
}
|
|
27
|
+
if (memo[path]) {
|
|
28
|
+
throw new Error(`Duplicate API routes for "${path}" found at "${memo[path].route.path}" and "${route.path}"`);
|
|
29
|
+
}
|
|
30
|
+
memo[path] = { route };
|
|
31
|
+
return memo;
|
|
32
|
+
}, {})
|
|
33
|
+
});
|
|
34
|
+
return (path, method) => {
|
|
35
|
+
const match = router.lookup(path);
|
|
36
|
+
if (!match || !match.route)
|
|
37
|
+
return;
|
|
38
|
+
const route = match.route;
|
|
39
|
+
// A `GET` handler answers `HEAD` unless the module handles it itself.
|
|
40
|
+
const handler = method === "HEAD" ? route.$HEAD || route.$GET : route[`$${method}`];
|
|
41
|
+
if (handler === undefined)
|
|
42
|
+
return;
|
|
43
|
+
return {
|
|
44
|
+
handler: handler,
|
|
45
|
+
params: match.params,
|
|
46
|
+
isPage: route.page === true && route.$component !== undefined
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Strips a leading path base: `stripPathBase("/app/x", "/app")` → `/x`. */
|
|
51
|
+
export function stripPathBase(path, base) {
|
|
52
|
+
if (!base || base === "/")
|
|
53
|
+
return path;
|
|
54
|
+
const normalizedBase = base.endsWith("/") ? base.slice(0, -1) : base;
|
|
55
|
+
if (path === normalizedBase)
|
|
56
|
+
return "/";
|
|
57
|
+
if (path.startsWith(`${normalizedBase}/`))
|
|
58
|
+
return path.slice(normalizedBase.length);
|
|
59
|
+
return path;
|
|
60
|
+
}
|
|
61
|
+
const RequestContext = Symbol.for("solid.RequestContext");
|
|
62
|
+
function getScopedEvent() {
|
|
63
|
+
const event = globalThis[RequestContext]?.getStore();
|
|
64
|
+
if (!event) {
|
|
65
|
+
throw new Error("API route dispatched outside a request-event scope. Compose the handler " +
|
|
66
|
+
"inside `provideRequestEvent` (any `ssr.middleware`-style chain already is), " +
|
|
67
|
+
"or pass `getEvent`.");
|
|
68
|
+
}
|
|
69
|
+
return event;
|
|
70
|
+
}
|
|
71
|
+
function toResponse(result) {
|
|
72
|
+
if (result instanceof Response)
|
|
73
|
+
return result;
|
|
74
|
+
if (typeof result === "string")
|
|
75
|
+
return new Response(result);
|
|
76
|
+
return Response.json(result);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Fetch-style middleware serving the manifest's API routes:
|
|
80
|
+
*
|
|
81
|
+
* ```ts
|
|
82
|
+
* import routes from "virtual:file-routes";
|
|
83
|
+
* import { createAPIHandler } from "filesystem-routing/api";
|
|
84
|
+
*
|
|
85
|
+
* export default [createAPIHandler(routes)];
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* Dispatch preserves SolidStart's semantics: unmatched requests (no route,
|
|
89
|
+
* or no handler for the method) advance the chain; `HEAD` falls back to the
|
|
90
|
+
* `GET` handler; matched params are written to `event.params` before the
|
|
91
|
+
* handler runs. A handler returning `undefined` declines the request —
|
|
92
|
+
* allowed for `GET` only (anything else throws): a page module falls
|
|
93
|
+
* through to the chain (its component renders instead), a handler-only
|
|
94
|
+
* module answers 404.
|
|
95
|
+
*/
|
|
96
|
+
export function createAPIHandler(routes, options = {}) {
|
|
97
|
+
const match = createAPIMatcher(routes);
|
|
98
|
+
const getEvent = options.getEvent ?? getScopedEvent;
|
|
99
|
+
return async (request, next) => {
|
|
100
|
+
const url = new URL(request.url);
|
|
101
|
+
const matched = match(stripPathBase(url.pathname, options.base ?? "/"), request.method);
|
|
102
|
+
if (!matched)
|
|
103
|
+
return next();
|
|
104
|
+
const mod = "require" in matched.handler ? matched.handler.require() : await matched.handler.import();
|
|
105
|
+
const fn = request.method === "HEAD" ? mod.HEAD || mod.GET : mod[request.method];
|
|
106
|
+
if (typeof fn !== "function")
|
|
107
|
+
return next();
|
|
108
|
+
const event = getEvent();
|
|
109
|
+
event.params = matched.params || {};
|
|
110
|
+
const result = await fn(event);
|
|
111
|
+
if (result !== undefined)
|
|
112
|
+
return toResponse(result);
|
|
113
|
+
if (request.method !== "GET") {
|
|
114
|
+
throw new Error(`API handler for ${request.method} "${request.url}" did not return a response.`);
|
|
115
|
+
}
|
|
116
|
+
if (!matched.isPage)
|
|
117
|
+
return new Response(null, { status: 404 });
|
|
118
|
+
return next();
|
|
119
|
+
};
|
|
120
|
+
}
|
package/dist/manifest.d.ts
CHANGED
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
* under decides how:
|
|
17
17
|
* - keys prefixed `$` become lazy refs: `{ src, import: () => import(...) }`
|
|
18
18
|
* - keys prefixed `$$` become eager refs: `{ require: () => ({ ...exports }) }`
|
|
19
|
+
*
|
|
20
|
+
* A delivery adapter configured without code splitting (the Vite adapter's
|
|
21
|
+
* `codeSplitting: false`) materializes `$` refs eagerly too, as
|
|
22
|
+
* `{ src, require }` — the delivered shape, not the key prefix, is the
|
|
23
|
+
* contract consumers branch on.
|
|
19
24
|
*/
|
|
20
25
|
export interface ModuleRef {
|
|
21
26
|
/** Absolute path of the source module. */
|
package/dist/vite/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { type PageFileSystemRouterConfig } from "../convention.ts";
|
|
|
3
3
|
import { BaseFileSystemRouter } from "../router.ts";
|
|
4
4
|
import { DEFAULT_EXTENSIONS, moduleId } from "./constants.ts";
|
|
5
5
|
export { DEFAULT_EXTENSIONS, moduleId };
|
|
6
|
-
export { treeShake } from "./tree-shake.ts";
|
|
6
|
+
export { sanitizeChunkFileName, toPickId, treeShake } from "./tree-shake.ts";
|
|
7
7
|
export { fileSystemWatcher } from "./fs-watcher.ts";
|
|
8
8
|
export interface FileRoutesOptions extends Pick<PageFileSystemRouterConfig, "components" | "httpMethods" | "toPath" | "toRoute"> {
|
|
9
9
|
/** Route directory, relative to the Vite root. Defaults to `src/routes`. */
|
|
@@ -32,6 +32,21 @@ export interface FileRoutesOptions extends Pick<PageFileSystemRouterConfig, "com
|
|
|
32
32
|
* framework already configures.
|
|
33
33
|
*/
|
|
34
34
|
buildInputs?: string | string[];
|
|
35
|
+
/**
|
|
36
|
+
* Deliver `$`-prefixed module refs as code-split dynamic imports, each
|
|
37
|
+
* route its own chunk. The default. Set to `false` to deliver every ref
|
|
38
|
+
* eagerly instead — static imports, `require()`-shaped refs, zero dynamic
|
|
39
|
+
* imports in the output — for the apps where splitting loses: small apps
|
|
40
|
+
* whose per-chunk overhead outweighs the split, and targets that want a
|
|
41
|
+
* single bundle. Adapters need no switch of their own: refs are
|
|
42
|
+
* self-describing (`import()` is code-split, `require()` is eager), and
|
|
43
|
+
* emission adapters branch on the shape they are handed.
|
|
44
|
+
*
|
|
45
|
+
* With `@solidjs/router`, `codeSplitting: false` requires the next release
|
|
46
|
+
* (newer than `2.0.0-next.14`): its adapter must branch on eager refs, and
|
|
47
|
+
* against `next.14` or older, enabling this option fails at first render.
|
|
48
|
+
*/
|
|
49
|
+
codeSplitting?: boolean;
|
|
35
50
|
/**
|
|
36
51
|
* Packages that import the virtual module and must therefore stay out of
|
|
37
52
|
* esbuild's dependency prebundling, which cannot resolve it. Emission
|
|
@@ -65,5 +80,11 @@ export interface FileRoutesOptions extends Pick<PageFileSystemRouterConfig, "com
|
|
|
65
80
|
* flat manifest, and `pageRoutes` is the page entries nested by path with
|
|
66
81
|
* `(group)` segments stripped, so emission adapters don't each reimplement
|
|
67
82
|
* the tree.
|
|
83
|
+
*
|
|
84
|
+
* With `httpMethods` on, one router serves both sides of an SSR app: the
|
|
85
|
+
* handler refs it emits reach server-consumer environments only, and client
|
|
86
|
+
* manifests drop them (and handler-only API entries with them) so handler
|
|
87
|
+
* modules never enter a client build. An environment given its own router
|
|
88
|
+
* via `routers` is explicit and serves that router's manifest untouched.
|
|
68
89
|
*/
|
|
69
90
|
export declare function fileRoutes(options?: FileRoutesOptions): PluginOption[];
|
package/dist/vite/index.js
CHANGED
|
@@ -5,11 +5,50 @@ import { normalizePath } from "../router.js";
|
|
|
5
5
|
import { buildRouteTree } from "../tree.js";
|
|
6
6
|
import { DEFAULT_EXTENSIONS, moduleId } from "./constants.js";
|
|
7
7
|
import { fileSystemWatcher } from "./fs-watcher.js";
|
|
8
|
-
import { treeShake } from "./tree-shake.js";
|
|
8
|
+
import { sanitizeChunkFileName, toPickId, treeShake } from "./tree-shake.js";
|
|
9
9
|
import { serializeTypes } from "./types.js";
|
|
10
10
|
export { DEFAULT_EXTENSIONS, moduleId };
|
|
11
|
-
export { treeShake } from "./tree-shake.js";
|
|
11
|
+
export { sanitizeChunkFileName, toPickId, treeShake } from "./tree-shake.js";
|
|
12
12
|
export { fileSystemWatcher } from "./fs-watcher.js";
|
|
13
|
+
/**
|
|
14
|
+
* Handler refs are the `$`-prefixed all-uppercase keys the convention emits
|
|
15
|
+
* (`$GET`, `$POST`, ...). They exist for server dispatch only.
|
|
16
|
+
*/
|
|
17
|
+
const HANDLER_REF = /^\$[A-Z]+$/;
|
|
18
|
+
/**
|
|
19
|
+
* Whether a Vite environment runs on the server. `configEnvironment` sees
|
|
20
|
+
* `consumer` only when something set it explicitly, so fall back to Vite's
|
|
21
|
+
* own default: the `client` environment is the one client consumer.
|
|
22
|
+
*/
|
|
23
|
+
const isServerConsumer = (name, consumer) => (consumer ?? (name === "client" ? "client" : "server")) === "server";
|
|
24
|
+
/**
|
|
25
|
+
* The client's view of a shared server manifest: handler refs removed. A
|
|
26
|
+
* client bundle can never invoke a request handler, but serializing its ref
|
|
27
|
+
* would pull the handler module — and the server-only code it imports —
|
|
28
|
+
* into the client build. Handler-only entries (API routes) disappear
|
|
29
|
+
* entirely; pages that also export handlers keep their page refs.
|
|
30
|
+
*/
|
|
31
|
+
function stripHandlerRefs(routes) {
|
|
32
|
+
const stripped = [];
|
|
33
|
+
for (const route of routes) {
|
|
34
|
+
const handlerKeys = Object.keys(route).filter(key => HANDLER_REF.test(key));
|
|
35
|
+
if (!handlerKeys.length) {
|
|
36
|
+
stripped.push(route);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const rest = { ...route };
|
|
40
|
+
for (const key of handlerKeys)
|
|
41
|
+
delete rest[key];
|
|
42
|
+
// Entries carry `$component: undefined` placeholders; only a ref with a
|
|
43
|
+
// value keeps a handler-only entry alive. Otherwise not even the API
|
|
44
|
+
// path string reaches the client bundle.
|
|
45
|
+
if (rest.page ||
|
|
46
|
+
Object.keys(rest).some(key => key.startsWith("$") && rest[key] !== undefined)) {
|
|
47
|
+
stripped.push(rest);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return stripped;
|
|
51
|
+
}
|
|
13
52
|
/**
|
|
14
53
|
* The Vite delivery adapter for `filesystem-routing`.
|
|
15
54
|
*
|
|
@@ -22,9 +61,16 @@ export { fileSystemWatcher } from "./fs-watcher.js";
|
|
|
22
61
|
* flat manifest, and `pageRoutes` is the page entries nested by path with
|
|
23
62
|
* `(group)` segments stripped, so emission adapters don't each reimplement
|
|
24
63
|
* the tree.
|
|
64
|
+
*
|
|
65
|
+
* With `httpMethods` on, one router serves both sides of an SSR app: the
|
|
66
|
+
* handler refs it emits reach server-consumer environments only, and client
|
|
67
|
+
* manifests drop them (and handler-only API entries with them) so handler
|
|
68
|
+
* modules never enter a client build. An environment given its own router
|
|
69
|
+
* via `routers` is explicit and serves that router's manifest untouched.
|
|
25
70
|
*/
|
|
26
71
|
export function fileRoutes(options = {}) {
|
|
27
72
|
const virtualId = options.moduleId ?? moduleId;
|
|
73
|
+
const codeSplitting = options.codeSplitting !== false;
|
|
28
74
|
const buildInputs = options.buildInputs === undefined
|
|
29
75
|
? []
|
|
30
76
|
: Array.isArray(options.buildInputs)
|
|
@@ -47,7 +93,7 @@ export function fileRoutes(options = {}) {
|
|
|
47
93
|
if (!file || !router)
|
|
48
94
|
return;
|
|
49
95
|
const routes = (await router.getRoutes()) ?? [];
|
|
50
|
-
const contents = serializeTypes(virtualId, routes, buildRouteTree(routes.filter(route => route.page)), dirname(file));
|
|
96
|
+
const contents = serializeTypes(virtualId, routes, buildRouteTree(routes.filter(route => route.page)), dirname(file), codeSplitting);
|
|
51
97
|
// Only touch the file when it actually changes: every environment calls
|
|
52
98
|
// this, and rewriting it would churn the watcher for no reason.
|
|
53
99
|
let previous;
|
|
@@ -61,20 +107,27 @@ export function fileRoutes(options = {}) {
|
|
|
61
107
|
writeFileSync(file, contents);
|
|
62
108
|
}
|
|
63
109
|
/** The id a route module ref is loaded from: its source plus its picks. */
|
|
64
|
-
const toModuleId = (ref) =>
|
|
110
|
+
const toModuleId = (ref) => toPickId(ref.src, ref.pick);
|
|
65
111
|
return [
|
|
66
112
|
{
|
|
67
113
|
name: "filesystem-routing",
|
|
68
114
|
enforce: "pre",
|
|
69
115
|
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
116
|
return {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
117
|
+
build: {
|
|
118
|
+
rollupOptions: {
|
|
119
|
+
output: {
|
|
120
|
+
// Keeps route chunks named after their file rather than after
|
|
121
|
+
// the `?pick=...` id that addresses them. See toPickId.
|
|
122
|
+
sanitizeFileName: sanitizeChunkFileName
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
// Packages importing the virtual module (which only this plugin can
|
|
127
|
+
// resolve) must stay out of esbuild prebundling.
|
|
128
|
+
...(options.optimizeDepsExclude?.length
|
|
129
|
+
? { optimizeDeps: { exclude: options.optimizeDepsExclude } }
|
|
130
|
+
: {})
|
|
78
131
|
};
|
|
79
132
|
},
|
|
80
133
|
configResolved(config) {
|
|
@@ -94,16 +147,27 @@ export function fileRoutes(options = {}) {
|
|
|
94
147
|
await writeTypes();
|
|
95
148
|
},
|
|
96
149
|
async configEnvironment(name, _config, env) {
|
|
150
|
+
// Without code splitting no ref becomes a dynamic import, so no
|
|
151
|
+
// route module needs to be an entry: they are all statically
|
|
152
|
+
// reachable from the virtual module.
|
|
153
|
+
if (!codeSplitting)
|
|
154
|
+
return;
|
|
97
155
|
if (env.command !== "build" || !buildInputs.includes(name))
|
|
98
156
|
return;
|
|
99
157
|
const router = getRouter(name);
|
|
100
158
|
if (!router)
|
|
101
159
|
return;
|
|
160
|
+
// A client environment on the shared router never serializes
|
|
161
|
+
// handler refs (see load), so their modules must not become its
|
|
162
|
+
// build entries either.
|
|
163
|
+
const routes = options.routers?.[name] || isServerConsumer(name, _config.consumer)
|
|
164
|
+
? await router.getRoutes()
|
|
165
|
+
: stripHandlerRefs(await router.getRoutes());
|
|
102
166
|
// Every code-split route module is an entry of its own, so the
|
|
103
167
|
// manifest's dynamic imports resolve to real chunks. `$$` refs are
|
|
104
168
|
// inlined into the manifest and need no entry.
|
|
105
169
|
const input = [];
|
|
106
|
-
for (const route of
|
|
170
|
+
for (const route of routes) {
|
|
107
171
|
for (const [key, ref] of Object.entries(route)) {
|
|
108
172
|
if (ref && key.startsWith("$") && !key.startsWith("$$")) {
|
|
109
173
|
input.push(toModuleId(ref));
|
|
@@ -124,8 +188,17 @@ export function fileRoutes(options = {}) {
|
|
|
124
188
|
const root = this.environment.config.root;
|
|
125
189
|
const isBuild = this.environment.mode === "build";
|
|
126
190
|
const js = jsCode();
|
|
127
|
-
const
|
|
128
|
-
const
|
|
191
|
+
const environmentName = this.environment.name;
|
|
192
|
+
const router = getRouter(environmentName);
|
|
193
|
+
let routes = (router ? await router.getRoutes() : []) ?? [];
|
|
194
|
+
// The shared router serves every environment, so the split is this
|
|
195
|
+
// adapter's job: client consumers get the manifest without handler
|
|
196
|
+
// refs. An explicit per-environment router already IS the split —
|
|
197
|
+
// serve whatever it emits.
|
|
198
|
+
if (!options.routers?.[environmentName] &&
|
|
199
|
+
!isServerConsumer(environmentName, this.environment.config.consumer)) {
|
|
200
|
+
routes = stripHandlerRefs(routes);
|
|
201
|
+
}
|
|
129
202
|
const serializeEntry = (entry) => JSON.stringify(entry, (key, value) => {
|
|
130
203
|
if (value === undefined)
|
|
131
204
|
return undefined;
|
|
@@ -143,6 +216,16 @@ export function fileRoutes(options = {}) {
|
|
|
143
216
|
}
|
|
144
217
|
else if (key.startsWith("$")) {
|
|
145
218
|
const buildId = toModuleId(value);
|
|
219
|
+
// With code splitting off the ref is delivered eagerly: a
|
|
220
|
+
// namespace import (named imports would fail on the synthetic
|
|
221
|
+
// `$css` pick) behind the `require()` shape eager refs carry,
|
|
222
|
+
// plus the `src` lazy refs already expose.
|
|
223
|
+
if (!codeSplitting) {
|
|
224
|
+
return {
|
|
225
|
+
src: relative(root, buildId),
|
|
226
|
+
require: `_$() => (${js.addNamespaceImport(buildId)})$_`
|
|
227
|
+
};
|
|
228
|
+
}
|
|
146
229
|
return {
|
|
147
230
|
src: relative(root, buildId),
|
|
148
231
|
build: isBuild ? `_$() => import('${buildId}')$_` : undefined,
|
|
@@ -185,6 +268,7 @@ export const pageRoutes = ${serializeTree(tree)};
|
|
|
185
268
|
}
|
|
186
269
|
function jsCode() {
|
|
187
270
|
const imports = new Map();
|
|
271
|
+
const namespaceImports = new Map();
|
|
188
272
|
let vars = 0;
|
|
189
273
|
function addNamedImport(name, source) {
|
|
190
274
|
let names = imports.get(source);
|
|
@@ -201,15 +285,25 @@ function jsCode() {
|
|
|
201
285
|
names[name] = alias;
|
|
202
286
|
return alias;
|
|
203
287
|
}
|
|
288
|
+
function addNamespaceImport(source) {
|
|
289
|
+
const existing = namespaceImports.get(source);
|
|
290
|
+
if (existing)
|
|
291
|
+
return existing;
|
|
292
|
+
const alias = "routeModule" + namespaceImports.size;
|
|
293
|
+
namespaceImports.set(source, alias);
|
|
294
|
+
return alias;
|
|
295
|
+
}
|
|
204
296
|
const getImportStatements = () => {
|
|
205
|
-
return [
|
|
206
|
-
.map(([source, names]) => `import { ${Object.entries(names)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
.
|
|
297
|
+
return [
|
|
298
|
+
...[...imports.entries()].map(([source, names]) => `import { ${Object.entries(names)
|
|
299
|
+
.map(([name, alias]) => `${name} as ${alias}`)
|
|
300
|
+
.join(", ")} } from '${source}';`),
|
|
301
|
+
...[...namespaceImports.entries()].map(([source, alias]) => `import * as ${alias} from '${source}';`)
|
|
302
|
+
].join("\n");
|
|
210
303
|
};
|
|
211
304
|
return {
|
|
212
305
|
addNamedImport,
|
|
306
|
+
addNamespaceImport,
|
|
213
307
|
getImportStatements
|
|
214
308
|
};
|
|
215
309
|
}
|
|
@@ -1,2 +1,29 @@
|
|
|
1
1
|
import type { Plugin } from "vite";
|
|
2
|
+
/**
|
|
3
|
+
* Builds the module id used to import a subset of a route file's exports.
|
|
4
|
+
*
|
|
5
|
+
* The `pick` list has to live in the query so that the same file can be
|
|
6
|
+
* instantiated once per export subset (the client picks `default`/`$css`,
|
|
7
|
+
* the server picks its HTTP handlers). That query would otherwise leave the
|
|
8
|
+
* id ending in `?pick=GET`, which silently excludes route files from any
|
|
9
|
+
* plugin whose filter is anchored on the file extension -- a very common
|
|
10
|
+
* default, e.g. unplugin-macros' `/\.[cm]?[jt]sx?$/`.
|
|
11
|
+
*
|
|
12
|
+
* The trailing `lang.<ext>` marker is the same convention Vue SFCs use
|
|
13
|
+
* (`?vue&type=script&lang.ts`) and puts a real extension back at the end of
|
|
14
|
+
* the id, so those filters match again.
|
|
15
|
+
*
|
|
16
|
+
* @see https://github.com/solidjs/solid-start/issues/1918
|
|
17
|
+
*/
|
|
18
|
+
export declare function toPickId(src: string, pick: string[]): string;
|
|
19
|
+
/**
|
|
20
|
+
* Rollup's default chunk name sanitizer, plus a pass that drops the
|
|
21
|
+
* `?pick=...&lang` tail so route chunks keep the short, stable filenames they
|
|
22
|
+
* had before the `lang` marker was introduced (`index-<hash>.js`, not
|
|
23
|
+
* `index.tsx_pick_default_pick__css_lang-<hash>.js`). Client chunk filenames
|
|
24
|
+
* are public URLs, so this is not purely cosmetic.
|
|
25
|
+
*
|
|
26
|
+
* @see https://github.com/rollup/rollup/blob/master/src/utils/sanitizeFileName.ts
|
|
27
|
+
*/
|
|
28
|
+
export declare function sanitizeChunkFileName(name: string): string;
|
|
2
29
|
export declare function treeShake(): Plugin;
|
package/dist/vite/tree-shake.js
CHANGED
|
@@ -1,7 +1,52 @@
|
|
|
1
1
|
// All credit for this work goes to the amazing Next.js team.
|
|
2
2
|
// https://github.com/vercel/next.js/blob/canary/packages/next/build/babel/plugins/next-ssg-transform.ts
|
|
3
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";
|
|
4
|
+
import { basename, extname } from "node:path";
|
|
5
|
+
const PICKABLE_EXTENSIONS = ["js", "jsx", "ts", "tsx"];
|
|
6
|
+
/**
|
|
7
|
+
* Builds the module id used to import a subset of a route file's exports.
|
|
8
|
+
*
|
|
9
|
+
* The `pick` list has to live in the query so that the same file can be
|
|
10
|
+
* instantiated once per export subset (the client picks `default`/`$css`,
|
|
11
|
+
* the server picks its HTTP handlers). That query would otherwise leave the
|
|
12
|
+
* id ending in `?pick=GET`, which silently excludes route files from any
|
|
13
|
+
* plugin whose filter is anchored on the file extension -- a very common
|
|
14
|
+
* default, e.g. unplugin-macros' `/\.[cm]?[jt]sx?$/`.
|
|
15
|
+
*
|
|
16
|
+
* The trailing `lang.<ext>` marker is the same convention Vue SFCs use
|
|
17
|
+
* (`?vue&type=script&lang.ts`) and puts a real extension back at the end of
|
|
18
|
+
* the id, so those filters match again.
|
|
19
|
+
*
|
|
20
|
+
* @see https://github.com/solidjs/solid-start/issues/1918
|
|
21
|
+
*/
|
|
22
|
+
export function toPickId(src, pick) {
|
|
23
|
+
const query = pick.map(p => `pick=${p}`).join("&");
|
|
24
|
+
const ext = extname(src).slice(1);
|
|
25
|
+
return PICKABLE_EXTENSIONS.includes(ext) ? `${src}?${query}&lang.${ext}` : `${src}?${query}`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Matches the tail that {@link toPickId} appends, as it appears in a chunk
|
|
29
|
+
* name. Bundlers derive a chunk name from the module id by taking the
|
|
30
|
+
* basename minus its extension, so `index.tsx?pick=default&pick=$css&lang.tsx`
|
|
31
|
+
* arrives here as `index.tsx?pick=default&pick=$css&lang`.
|
|
32
|
+
*/
|
|
33
|
+
const PICK_CHUNK_NAME_RE = /\.[cm]?[jt]sx?\?pick=[^?]*&lang$/;
|
|
34
|
+
const INVALID_CHAR_RE = /[\u0000-\u001F"#$&*+,:;<=>?[\]^`{|}\u007F]/g;
|
|
35
|
+
const DRIVE_LETTER_RE = /^[a-z]:/i;
|
|
36
|
+
/**
|
|
37
|
+
* Rollup's default chunk name sanitizer, plus a pass that drops the
|
|
38
|
+
* `?pick=...&lang` tail so route chunks keep the short, stable filenames they
|
|
39
|
+
* had before the `lang` marker was introduced (`index-<hash>.js`, not
|
|
40
|
+
* `index.tsx_pick_default_pick__css_lang-<hash>.js`). Client chunk filenames
|
|
41
|
+
* are public URLs, so this is not purely cosmetic.
|
|
42
|
+
*
|
|
43
|
+
* @see https://github.com/rollup/rollup/blob/master/src/utils/sanitizeFileName.ts
|
|
44
|
+
*/
|
|
45
|
+
export function sanitizeChunkFileName(name) {
|
|
46
|
+
const stripped = name.replace(PICK_CHUNK_NAME_RE, "");
|
|
47
|
+
const driveLetter = DRIVE_LETTER_RE.exec(stripped)?.[0] ?? "";
|
|
48
|
+
return driveLetter + stripped.slice(driveLetter.length).replace(INVALID_CHAR_RE, "_");
|
|
49
|
+
}
|
|
5
50
|
function treeShakeTransform({ types: t }) {
|
|
6
51
|
function getIdentifier(path) {
|
|
7
52
|
const parentPath = path.parentPath;
|
|
@@ -133,6 +178,11 @@ function treeShakeTransform({ types: t }) {
|
|
|
133
178
|
if (!state.opts.pick) {
|
|
134
179
|
return;
|
|
135
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
|
+
}
|
|
136
186
|
const specifiers = exportNamedPath.get("specifiers");
|
|
137
187
|
if (specifiers.length) {
|
|
138
188
|
specifiers.forEach(s => {
|
package/dist/vite/types.d.ts
CHANGED
|
@@ -7,4 +7,6 @@ import type { RouteTreeEntry } from "../tree.ts";
|
|
|
7
7
|
*/
|
|
8
8
|
export declare function serializeTypes(moduleId: string, routes: readonly RouteManifestEntry[], pageRoutes: readonly RouteTreeEntry[],
|
|
9
9
|
/** Directory the declaration is written to; module specifiers are relative to it. */
|
|
10
|
-
from: string
|
|
10
|
+
from: string,
|
|
11
|
+
/** Whether lazy refs are delivered code-split (the default) or eagerly. */
|
|
12
|
+
codeSplitting?: boolean): string;
|
package/dist/vite/types.js
CHANGED
|
@@ -23,6 +23,8 @@ const REF_TYPES = ` /** A code-split ref: the delivery adapter emits it as a dy
|
|
|
23
23
|
|
|
24
24
|
/** An eager ref: its picked exports are imported statically. */
|
|
25
25
|
export interface FileRouteEagerRef<M = Record<string, unknown>> {
|
|
26
|
+
/** The source module path — present when the convention emitted the ref lazy and \`codeSplitting: false\` delivered it eagerly. */
|
|
27
|
+
src?: string;
|
|
26
28
|
require(): M;
|
|
27
29
|
}
|
|
28
30
|
|
|
@@ -30,7 +32,7 @@ const REF_TYPES = ` /** A code-split ref: the delivery adapter emits it as a dy
|
|
|
30
32
|
export interface FileRouteEntry {
|
|
31
33
|
path: string;
|
|
32
34
|
page?: boolean;
|
|
33
|
-
$component?: FileRouteLazyRef<any>;
|
|
35
|
+
$component?: FileRouteLazyRef<any> | FileRouteEagerRef<any>;
|
|
34
36
|
$$route?: FileRouteEagerRef<any>;
|
|
35
37
|
[key: string]: unknown;
|
|
36
38
|
}
|
|
@@ -64,7 +66,7 @@ const isEagerRefKey = (key) => key.startsWith("$$");
|
|
|
64
66
|
* types intact. Absent refs are typed `?: undefined` rather than omitted, so
|
|
65
67
|
* an adapter can read `entry.$$route` uniformly across the tuple.
|
|
66
68
|
*/
|
|
67
|
-
function entryType(entry, indent, nested, from) {
|
|
69
|
+
function entryType(entry, indent, nested, from, codeSplitting) {
|
|
68
70
|
const inner = indent + " ";
|
|
69
71
|
const fields = [`path: ${JSON.stringify(entry.path)}`];
|
|
70
72
|
if (nested)
|
|
@@ -74,7 +76,9 @@ function entryType(entry, indent, nested, from) {
|
|
|
74
76
|
// so go by the value, not the key.
|
|
75
77
|
const refKeys = Object.keys(entry).filter(key => key.startsWith("$") && entry[key] !== undefined);
|
|
76
78
|
for (const key of refKeys) {
|
|
77
|
-
|
|
79
|
+
// With code splitting off, the delivery adapter materializes lazy refs
|
|
80
|
+
// eagerly, so the declaration describes them as delivered.
|
|
81
|
+
const kind = isEagerRefKey(key) || !codeSplitting ? "eager" : "lazy";
|
|
78
82
|
fields.push(`${key}: ${refType(entry[key], kind, from)}`);
|
|
79
83
|
}
|
|
80
84
|
// Reading an absent ref is how adapters branch, so keep the key present.
|
|
@@ -85,17 +89,17 @@ function entryType(entry, indent, nested, from) {
|
|
|
85
89
|
if (nested) {
|
|
86
90
|
const children = entry.children;
|
|
87
91
|
fields.push(children?.length
|
|
88
|
-
? `children: ${tupleType(children, inner, true, from)}`
|
|
92
|
+
? `children: ${tupleType(children, inner, true, from, codeSplitting)}`
|
|
89
93
|
: `children?: undefined`);
|
|
90
94
|
}
|
|
91
95
|
return `{\n${fields.map(field => `${inner}${field};`).join("\n")}\n${indent}}`;
|
|
92
96
|
}
|
|
93
|
-
function tupleType(entries, indent, nested, from) {
|
|
97
|
+
function tupleType(entries, indent, nested, from, codeSplitting) {
|
|
94
98
|
if (!entries.length)
|
|
95
99
|
return "readonly []";
|
|
96
100
|
const inner = indent + " ";
|
|
97
101
|
const members = entries
|
|
98
|
-
.map(entry => `${inner}${entryType(entry, inner, nested, from)}`)
|
|
102
|
+
.map(entry => `${inner}${entryType(entry, inner, nested, from, codeSplitting)}`)
|
|
99
103
|
.join(",\n");
|
|
100
104
|
return `readonly [\n${members}\n${indent}]`;
|
|
101
105
|
}
|
|
@@ -106,18 +110,20 @@ function tupleType(entries, indent, nested, from) {
|
|
|
106
110
|
*/
|
|
107
111
|
export function serializeTypes(moduleId, routes, pageRoutes,
|
|
108
112
|
/** Directory the declaration is written to; module specifiers are relative to it. */
|
|
109
|
-
from
|
|
113
|
+
from,
|
|
114
|
+
/** Whether lazy refs are delivered code-split (the default) or eagerly. */
|
|
115
|
+
codeSplitting = true) {
|
|
110
116
|
return `${HEADER}
|
|
111
117
|
|
|
112
118
|
declare module ${JSON.stringify(moduleId)} {
|
|
113
119
|
${REF_TYPES}
|
|
114
120
|
|
|
115
121
|
/** The flat route manifest, in scan order. */
|
|
116
|
-
const routes: ${tupleType(routes, " ", false, from)};
|
|
122
|
+
const routes: ${tupleType(routes, " ", false, from, codeSplitting)};
|
|
117
123
|
export default routes;
|
|
118
124
|
|
|
119
125
|
/** The page entries, nested by path with grouping segments stripped. */
|
|
120
|
-
export const pageRoutes: ${tupleType(pageRoutes, " ", true, from)};
|
|
126
|
+
export const pageRoutes: ${tupleType(pageRoutes, " ", true, from, codeSplitting)};
|
|
121
127
|
}
|
|
122
128
|
`;
|
|
123
129
|
}
|
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.
|
|
5
|
+
"version": "0.2.1",
|
|
6
6
|
"author": "Ryan Carniato",
|
|
7
7
|
"homepage": "https://github.com/solidjs/filesystem-routing#readme",
|
|
8
8
|
"repository": {
|
|
@@ -40,6 +40,10 @@
|
|
|
40
40
|
"types": "./dist/vite/index.d.ts",
|
|
41
41
|
"default": "./dist/vite/index.js"
|
|
42
42
|
},
|
|
43
|
+
"./api": {
|
|
44
|
+
"types": "./dist/api.d.ts",
|
|
45
|
+
"default": "./dist/api.js"
|
|
46
|
+
},
|
|
43
47
|
"./types": {
|
|
44
48
|
"types": "./types.d.ts"
|
|
45
49
|
}
|
|
@@ -55,7 +59,8 @@
|
|
|
55
59
|
"@types/micromatch": "^4.0.10",
|
|
56
60
|
"fast-glob": "^3.3.3",
|
|
57
61
|
"micromatch": "^4.0.8",
|
|
58
|
-
"oxc-parser": "^0.139.0"
|
|
62
|
+
"oxc-parser": "^0.139.0",
|
|
63
|
+
"radix3": "^1.1.2"
|
|
59
64
|
},
|
|
60
65
|
"peerDependencies": {
|
|
61
66
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
package/types.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ declare module "virtual:file-routes" {
|
|
|
22
22
|
|
|
23
23
|
/** An eager ref: its picked exports are imported statically. */
|
|
24
24
|
export interface FileRouteEagerRef {
|
|
25
|
+
/** The source module path — present when the convention emitted the ref lazy and `codeSplitting: false` delivered it eagerly. */
|
|
26
|
+
src?: string;
|
|
25
27
|
require(): Record<string, unknown>;
|
|
26
28
|
}
|
|
27
29
|
|
|
@@ -34,8 +36,8 @@ declare module "virtual:file-routes" {
|
|
|
34
36
|
path: string;
|
|
35
37
|
/** `true` when the module renders a page. */
|
|
36
38
|
page?: boolean;
|
|
37
|
-
/** The page component module
|
|
38
|
-
$component?: FileRouteLazyRef;
|
|
39
|
+
/** The page component module: code-split by default, an eager ref under `codeSplitting: false`. */
|
|
40
|
+
$component?: FileRouteLazyRef | FileRouteEagerRef;
|
|
39
41
|
/** The route config (`route` export), when the module has one. */
|
|
40
42
|
$$route?: FileRouteEagerRef;
|
|
41
43
|
/** Refs added by convention extensions, e.g. `$GET` handlers. */
|