filesystem-routing 0.4.0 → 0.4.2
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 +4 -2
- package/dist/api.d.ts +13 -6
- package/dist/api.js +11 -7
- package/dist/convention.js +3 -2
- package/dist/router.d.ts +8 -1
- package/dist/router.js +46 -18
- package/dist/tree.js +3 -1
- package/dist/vite/index.js +16 -8
- package/dist/vite/types.js +8 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -165,7 +165,8 @@ pages get:
|
|
|
165
165
|
|
|
166
166
|
Dispatch ships as fetch-style middleware — `filesystem-routing/api` matches
|
|
167
167
|
requests against the manifest's handler refs and composes into any
|
|
168
|
-
`(request, next)` chain, an SSR handler's middleware option
|
|
168
|
+
`(request, next)` or `(event, next)` chain, an SSR handler's middleware option
|
|
169
|
+
(`@solidjs/vite-plugin`'s `start.middleware`) included:
|
|
169
170
|
|
|
170
171
|
```ts
|
|
171
172
|
// src/middleware.ts
|
|
@@ -175,7 +176,8 @@ import { createAPIHandler } from "filesystem-routing/api";
|
|
|
175
176
|
export default [createAPIHandler(routes)];
|
|
176
177
|
```
|
|
177
178
|
|
|
178
|
-
Handlers receive the
|
|
179
|
+
Handlers receive the event the chain passes in or, in a `(request, next)`
|
|
180
|
+
chain, the request event of the surrounding scope (the storage
|
|
179
181
|
`provideRequestEvent` establishes — pass `getEvent` to dispatch outside
|
|
180
182
|
one), with the matched `params` written onto it. Unmatched requests (no
|
|
181
183
|
route, or no handler for the method) advance the chain; `HEAD` falls back
|
package/dist/api.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The API dispatch adapter: matches requests against the flat manifest's
|
|
3
3
|
* handler refs (`$GET`, `$POST`, …) and serves them as fetch-style
|
|
4
|
-
* middleware, so API routes compose into any `(request, next)`
|
|
5
|
-
* SSR handler's middleware option, a framework's
|
|
6
|
-
* bundler or server involvement.
|
|
4
|
+
* middleware, so API routes compose into any `(request, next)` or
|
|
5
|
+
* `(event, next)` chain — an SSR handler's middleware option, a framework's
|
|
6
|
+
* request pipeline — with no bundler or server involvement.
|
|
7
7
|
*
|
|
8
8
|
* Ported from SolidStart's `server/routes.ts` (the radix3 matcher) and the
|
|
9
9
|
* method dispatch in its `server/handler.ts`.
|
|
@@ -69,7 +69,8 @@ export interface APIHandlerOptions {
|
|
|
69
69
|
base?: string;
|
|
70
70
|
/**
|
|
71
71
|
* How the adapter reaches the request event it hands to handlers. Defaults
|
|
72
|
-
* to the
|
|
72
|
+
* to the event the chain passes in or, when it passes a bare `Request`, to
|
|
73
|
+
* the core request-event scope (the storage `provideRequestEvent`
|
|
73
74
|
* establishes), which is where a middleware chain runs — override it when
|
|
74
75
|
* dispatching outside one, or to hand handlers a richer event.
|
|
75
76
|
*/
|
|
@@ -85,12 +86,18 @@ export interface APIHandlerOptions {
|
|
|
85
86
|
* export default [createAPIHandler(routes)];
|
|
86
87
|
* ```
|
|
87
88
|
*
|
|
89
|
+
* The first argument is the request or a request event carrying it
|
|
90
|
+
* (`event.request`), so the handler fits `(request, next)` and
|
|
91
|
+
* `(event, next)` chains alike; it calls `next()` without arguments.
|
|
92
|
+
*
|
|
88
93
|
* Dispatch preserves SolidStart's semantics: unmatched requests (no route,
|
|
89
94
|
* or no handler for the method) advance the chain; `HEAD` falls back to the
|
|
90
95
|
* `GET` handler; matched params are written to `event.params` before the
|
|
91
96
|
* handler runs. A handler returning `undefined` declines the request —
|
|
92
|
-
* allowed for `GET` only (anything else throws): a page module falls
|
|
97
|
+
* allowed for `GET` and `HEAD` only (anything else throws): a page module falls
|
|
93
98
|
* through to the chain (its component renders instead), a handler-only
|
|
94
99
|
* module answers 404.
|
|
95
100
|
*/
|
|
96
|
-
export declare function createAPIHandler(routes: readonly FileRouteHandlers[], options?: APIHandlerOptions): (
|
|
101
|
+
export declare function createAPIHandler(routes: readonly FileRouteHandlers[], options?: APIHandlerOptions): (input: Request | {
|
|
102
|
+
request: Request;
|
|
103
|
+
}, next: (request?: Request) => Response | Promise<Response>) => Promise<Response>;
|
package/dist/api.js
CHANGED
|
@@ -62,8 +62,8 @@ const RequestContext = Symbol.for("solid.RequestContext");
|
|
|
62
62
|
function getScopedEvent() {
|
|
63
63
|
const event = globalThis[RequestContext]?.getStore();
|
|
64
64
|
if (!event) {
|
|
65
|
-
throw new Error("API route dispatched outside a request-event scope.
|
|
66
|
-
"
|
|
65
|
+
throw new Error("API route dispatched outside a request-event scope. Pass the request event " +
|
|
66
|
+
"instead of the request, compose the handler inside `provideRequestEvent`, " +
|
|
67
67
|
"or pass `getEvent`.");
|
|
68
68
|
}
|
|
69
69
|
return event;
|
|
@@ -85,18 +85,22 @@ function toResponse(result) {
|
|
|
85
85
|
* export default [createAPIHandler(routes)];
|
|
86
86
|
* ```
|
|
87
87
|
*
|
|
88
|
+
* The first argument is the request or a request event carrying it
|
|
89
|
+
* (`event.request`), so the handler fits `(request, next)` and
|
|
90
|
+
* `(event, next)` chains alike; it calls `next()` without arguments.
|
|
91
|
+
*
|
|
88
92
|
* Dispatch preserves SolidStart's semantics: unmatched requests (no route,
|
|
89
93
|
* or no handler for the method) advance the chain; `HEAD` falls back to the
|
|
90
94
|
* `GET` handler; matched params are written to `event.params` before the
|
|
91
95
|
* handler runs. A handler returning `undefined` declines the request —
|
|
92
|
-
* allowed for `GET` only (anything else throws): a page module falls
|
|
96
|
+
* allowed for `GET` and `HEAD` only (anything else throws): a page module falls
|
|
93
97
|
* through to the chain (its component renders instead), a handler-only
|
|
94
98
|
* module answers 404.
|
|
95
99
|
*/
|
|
96
100
|
export function createAPIHandler(routes, options = {}) {
|
|
97
101
|
const match = createAPIMatcher(routes);
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
return async (input, next) => {
|
|
103
|
+
const request = "request" in input ? input.request : input;
|
|
100
104
|
const url = new URL(request.url);
|
|
101
105
|
const matched = match(stripPathBase(url.pathname, options.base ?? "/"), request.method);
|
|
102
106
|
if (!matched)
|
|
@@ -105,12 +109,12 @@ export function createAPIHandler(routes, options = {}) {
|
|
|
105
109
|
const fn = request.method === "HEAD" ? mod.HEAD || mod.GET : mod[request.method];
|
|
106
110
|
if (typeof fn !== "function")
|
|
107
111
|
return next();
|
|
108
|
-
const event = getEvent();
|
|
112
|
+
const event = options.getEvent?.() ?? ("request" in input ? input : getScopedEvent());
|
|
109
113
|
event.params = matched.params || {};
|
|
110
114
|
const result = await fn(event);
|
|
111
115
|
if (result !== undefined)
|
|
112
116
|
return toResponse(result);
|
|
113
|
-
if (request.method !== "GET") {
|
|
117
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
114
118
|
throw new Error(`API handler for ${request.method} "${request.url}" did not return a response.`);
|
|
115
119
|
}
|
|
116
120
|
if (!matched.isPage)
|
package/dist/convention.js
CHANGED
|
@@ -12,8 +12,9 @@ export function routePathFromFile(routeFile) {
|
|
|
12
12
|
const routePath = routeFile
|
|
13
13
|
// remove the initial slash
|
|
14
14
|
.slice(1)
|
|
15
|
-
// only strip a whole `index` segment, not a name that merely ends in "index" (e.g. `reindex`)
|
|
16
|
-
|
|
15
|
+
// only strip a whole `index` segment, not a name that merely ends in "index" (e.g. `reindex`);
|
|
16
|
+
// group prefixes such as SolidBase's `(0)index` ordering stay for the adapters to strip
|
|
17
|
+
.replace(/(^|\/)((?:\([^/()]*\))*)index$/, "$1$2")
|
|
17
18
|
.replace(/\[([^/]+)\]/g, (_, m) => {
|
|
18
19
|
if (m.length > 3 && m.startsWith("...")) {
|
|
19
20
|
return `*${m.slice(3)}`;
|
package/dist/router.d.ts
CHANGED
|
@@ -38,14 +38,21 @@ export declare function cleanPath(src: string, config: FileSystemRouterConfig):
|
|
|
38
38
|
*/
|
|
39
39
|
export declare class BaseFileSystemRouter extends EventTarget {
|
|
40
40
|
routes: RouteManifestEntry[];
|
|
41
|
+
/** Source file that first produced each route path, so incremental changes keep scan order. */
|
|
42
|
+
private sources;
|
|
41
43
|
config: FileSystemRouterConfig;
|
|
44
|
+
/**
|
|
45
|
+
* Route path each source file produced. A removed file can no longer be
|
|
46
|
+
* passed to `toRoute`, and `toPath` may give another file's route.
|
|
47
|
+
*/
|
|
48
|
+
private routePaths;
|
|
42
49
|
constructor(config: FileSystemRouterConfig);
|
|
43
50
|
glob(): string;
|
|
44
51
|
buildRoutes(): Promise<RouteManifestEntry[]>;
|
|
45
52
|
isRoute(src: string): boolean;
|
|
46
53
|
toPath(src: string): string | undefined;
|
|
47
54
|
toRoute(src: string): RouteManifestEntry | undefined;
|
|
48
|
-
_addRoute(route: RouteManifestEntry): boolean;
|
|
55
|
+
_addRoute(route: RouteManifestEntry, src?: string): boolean;
|
|
49
56
|
addRoute(src: string): Promise<void>;
|
|
50
57
|
reload(route: string, type: "update" | "remove" | "add"): void;
|
|
51
58
|
updateRoute(src_: string): Promise<void>;
|
package/dist/router.js
CHANGED
|
@@ -20,7 +20,14 @@ export function cleanPath(src, config) {
|
|
|
20
20
|
*/
|
|
21
21
|
export class BaseFileSystemRouter extends EventTarget {
|
|
22
22
|
routes;
|
|
23
|
+
/** Source file that first produced each route path, so incremental changes keep scan order. */
|
|
24
|
+
sources = new Map();
|
|
23
25
|
config;
|
|
26
|
+
/**
|
|
27
|
+
* Route path each source file produced. A removed file can no longer be
|
|
28
|
+
* passed to `toRoute`, and `toPath` may give another file's route.
|
|
29
|
+
*/
|
|
30
|
+
routePaths = new Map();
|
|
24
31
|
constructor(config) {
|
|
25
32
|
super();
|
|
26
33
|
this.routes = [];
|
|
@@ -54,12 +61,28 @@ export class BaseFileSystemRouter extends EventTarget {
|
|
|
54
61
|
return this.config.toRoute(src, this);
|
|
55
62
|
throw new Error("Not implemented");
|
|
56
63
|
}
|
|
57
|
-
_addRoute(route) {
|
|
64
|
+
_addRoute(route, src) {
|
|
58
65
|
const idx = this.routes.findIndex(r => r.path === route.path);
|
|
59
|
-
if (idx >= 0)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
if (idx >= 0) {
|
|
67
|
+
// Replace in place: an edited file keeps its position. The source that
|
|
68
|
+
// first produced the path still decides it, as in a full scan.
|
|
69
|
+
this.routes[idx] = route;
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if (src !== undefined)
|
|
73
|
+
this.sources.set(route.path, src);
|
|
74
|
+
// A new file goes where a full scan would put it.
|
|
75
|
+
const at = src === undefined
|
|
76
|
+
? -1
|
|
77
|
+
: this.routes.findIndex(r => {
|
|
78
|
+
const other = this.sources.get(r.path);
|
|
79
|
+
return other !== undefined && other > src;
|
|
80
|
+
});
|
|
81
|
+
if (at === -1)
|
|
82
|
+
this.routes.push(route);
|
|
83
|
+
else
|
|
84
|
+
this.routes.splice(at, 0, route);
|
|
85
|
+
return false;
|
|
63
86
|
}
|
|
64
87
|
async addRoute(src) {
|
|
65
88
|
src = normalizePath(src);
|
|
@@ -67,7 +90,10 @@ export class BaseFileSystemRouter extends EventTarget {
|
|
|
67
90
|
try {
|
|
68
91
|
const route = this.toRoute(src);
|
|
69
92
|
if (route) {
|
|
70
|
-
this.
|
|
93
|
+
if (this.routePaths.get(src) !== route.path)
|
|
94
|
+
this.removeRoute(src);
|
|
95
|
+
this._addRoute(route, src);
|
|
96
|
+
this.routePaths.set(src, route.path);
|
|
71
97
|
this.reload(route.path, "add");
|
|
72
98
|
}
|
|
73
99
|
}
|
|
@@ -90,7 +116,10 @@ export class BaseFileSystemRouter extends EventTarget {
|
|
|
90
116
|
try {
|
|
91
117
|
const route = this.toRoute(src);
|
|
92
118
|
if (route) {
|
|
93
|
-
|
|
119
|
+
if (this.routePaths.get(src) !== route.path)
|
|
120
|
+
this.removeRoute(src_);
|
|
121
|
+
const updated = this._addRoute(route, src);
|
|
122
|
+
this.routePaths.set(src, route.path);
|
|
94
123
|
this.reload(route.path, updated ? "update" : "add");
|
|
95
124
|
}
|
|
96
125
|
else {
|
|
@@ -104,17 +133,16 @@ export class BaseFileSystemRouter extends EventTarget {
|
|
|
104
133
|
}
|
|
105
134
|
removeRoute(src) {
|
|
106
135
|
src = normalizePath(src);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
136
|
+
const path = this.routePaths.get(src);
|
|
137
|
+
if (path === undefined)
|
|
138
|
+
return;
|
|
139
|
+
this.routePaths.delete(src);
|
|
140
|
+
const idx = this.routes.findIndex(r => r.path === path);
|
|
141
|
+
if (idx === -1)
|
|
142
|
+
return;
|
|
143
|
+
this.routes.splice(idx, 1);
|
|
144
|
+
this.sources.delete(path);
|
|
145
|
+
this.reload(path, "remove");
|
|
118
146
|
}
|
|
119
147
|
on(type, cb) {
|
|
120
148
|
this.addEventListener(type, cb);
|
package/dist/tree.js
CHANGED
|
@@ -20,8 +20,10 @@ export function buildRouteTree(entries) {
|
|
|
20
20
|
}
|
|
21
21
|
processRoute(parentRoute.children || (parentRoute.children = []), route, id.slice(parentRoute.id.length));
|
|
22
22
|
}
|
|
23
|
+
// Ties break by UTF-16 code unit, as the scan sorts files, not by
|
|
24
|
+
// `localeCompare`: the generated output must not depend on the machine's locale.
|
|
23
25
|
return [...entries]
|
|
24
|
-
.sort((a, b) =>
|
|
26
|
+
.sort((a, b) => a.path.length - b.path.length || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
|
|
25
27
|
.reduce((routes, route) => {
|
|
26
28
|
processRoute(routes, route, route.path);
|
|
27
29
|
return routes;
|
package/dist/vite/index.js
CHANGED
|
@@ -21,6 +21,15 @@ const HANDLER_REF = /^\$[A-Z]+$/;
|
|
|
21
21
|
* own default: the `client` environment is the one client consumer.
|
|
22
22
|
*/
|
|
23
23
|
const isServerConsumer = (name, consumer) => (consumer ?? (name === "client" ? "client" : "server")) === "server";
|
|
24
|
+
/**
|
|
25
|
+
* The id a route module ref is loaded from: its source plus its picks. On the
|
|
26
|
+
* server a server page's module is loaded whole, because the server-function
|
|
27
|
+
* handler imports it whole too, and a second (picked) instance would register
|
|
28
|
+
* the page's server function twice.
|
|
29
|
+
*/
|
|
30
|
+
const toModuleId = (route, ref, serverConsumer) => serverConsumer && route.server === true && ref.src === route.$component?.src
|
|
31
|
+
? ref.src
|
|
32
|
+
: toPickId(ref.src, ref.pick);
|
|
24
33
|
/**
|
|
25
34
|
* The client's view of a shared server manifest: handler refs removed. A
|
|
26
35
|
* client bundle can never invoke a request handler, but serializing its ref
|
|
@@ -106,8 +115,6 @@ export function fileRoutes(options = {}) {
|
|
|
106
115
|
mkdirSync(dirname(file), { recursive: true });
|
|
107
116
|
writeFileSync(file, contents);
|
|
108
117
|
}
|
|
109
|
-
/** The id a route module ref is loaded from: its source plus its picks. */
|
|
110
|
-
const toModuleId = (ref) => toPickId(ref.src, ref.pick);
|
|
111
118
|
return [
|
|
112
119
|
{
|
|
113
120
|
name: "filesystem-routing",
|
|
@@ -161,7 +168,8 @@ export function fileRoutes(options = {}) {
|
|
|
161
168
|
// A client environment on the shared router never serializes
|
|
162
169
|
// handler refs (see load), so their modules must not become its
|
|
163
170
|
// build entries either.
|
|
164
|
-
const
|
|
171
|
+
const serverConsumer = isServerConsumer(name, _config.consumer);
|
|
172
|
+
const routes = options.routers?.[name] || serverConsumer
|
|
165
173
|
? await router.getRoutes()
|
|
166
174
|
: stripHandlerRefs(await router.getRoutes());
|
|
167
175
|
// Every code-split route module is an entry of its own, so the
|
|
@@ -171,7 +179,7 @@ export function fileRoutes(options = {}) {
|
|
|
171
179
|
for (const route of routes) {
|
|
172
180
|
for (const [key, ref] of Object.entries(route)) {
|
|
173
181
|
if (ref && key.startsWith("$") && !key.startsWith("$$") && !ref.eager) {
|
|
174
|
-
input.push(toModuleId(ref));
|
|
182
|
+
input.push(toModuleId(route, ref, serverConsumer));
|
|
175
183
|
}
|
|
176
184
|
}
|
|
177
185
|
}
|
|
@@ -204,19 +212,19 @@ export function fileRoutes(options = {}) {
|
|
|
204
212
|
const environmentName = this.environment.name;
|
|
205
213
|
const router = getRouter(environmentName);
|
|
206
214
|
let routes = (router ? await router.getRoutes() : []) ?? [];
|
|
215
|
+
const serverConsumer = isServerConsumer(environmentName, this.environment.config.consumer);
|
|
207
216
|
// The shared router serves every environment, so the split is this
|
|
208
217
|
// adapter's job: client consumers get the manifest without handler
|
|
209
218
|
// refs. An explicit per-environment router already IS the split —
|
|
210
219
|
// serve whatever it emits.
|
|
211
|
-
if (!options.routers?.[environmentName] &&
|
|
212
|
-
!isServerConsumer(environmentName, this.environment.config.consumer)) {
|
|
220
|
+
if (!options.routers?.[environmentName] && !serverConsumer) {
|
|
213
221
|
routes = stripHandlerRefs(routes);
|
|
214
222
|
}
|
|
215
223
|
const serializeEntry = (entry) => JSON.stringify(entry, (key, value) => {
|
|
216
224
|
if (value === undefined)
|
|
217
225
|
return undefined;
|
|
218
226
|
if (key.startsWith("$$")) {
|
|
219
|
-
const buildId = toModuleId(value);
|
|
227
|
+
const buildId = toModuleId(entry, value, serverConsumer);
|
|
220
228
|
const refs = {};
|
|
221
229
|
for (const pick of value.pick) {
|
|
222
230
|
refs[pick] = js.addNamedImport(pick, buildId);
|
|
@@ -228,7 +236,7 @@ export function fileRoutes(options = {}) {
|
|
|
228
236
|
};
|
|
229
237
|
}
|
|
230
238
|
else if (key.startsWith("$")) {
|
|
231
|
-
const buildId = toModuleId(value);
|
|
239
|
+
const buildId = toModuleId(entry, value, serverConsumer);
|
|
232
240
|
// With code splitting off — or a ref the convention marked
|
|
233
241
|
// `eager` — the ref is delivered eagerly: a namespace import
|
|
234
242
|
// (named imports would fail on the synthetic `$css` pick)
|
package/dist/vite/types.js
CHANGED
|
@@ -47,19 +47,22 @@ const REF_TYPES = ` /** A code-split ref: the delivery adapter emits it as a dy
|
|
|
47
47
|
/** Extensions TypeScript can resolve through a `typeof import(...)`. */
|
|
48
48
|
const TYPED_MODULE = /\.([mc]?[jt]sx?)$/;
|
|
49
49
|
/**
|
|
50
|
-
* Types a ref by
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* TypeScript cannot resolve (`.md`, `.mdx`) fall back to the generic ref.
|
|
50
|
+
* Types a ref by its picked exports, keeping the module's `default` and
|
|
51
|
+
* `route` types so an emission adapter can read either without a cast.
|
|
52
|
+
* Modules TypeScript cannot resolve (`.md`, `.mdx`) fall back to the generic ref.
|
|
54
53
|
*/
|
|
55
54
|
function refType(ref, kind, from) {
|
|
56
55
|
const name = kind === "eager" ? "FileRouteEagerRef" : "FileRouteLazyRef";
|
|
57
56
|
if (!TYPED_MODULE.test(ref.src))
|
|
58
57
|
return name;
|
|
58
|
+
const picks = ref.pick.filter(pick => pick !== "$css").map(pick => JSON.stringify(pick));
|
|
59
|
+
if (!picks.length)
|
|
60
|
+
return name;
|
|
59
61
|
let specifier = normalizePath(relative(from, ref.src)).replace(TYPED_MODULE, "");
|
|
60
62
|
if (!specifier.startsWith("."))
|
|
61
63
|
specifier = `./${specifier}`;
|
|
62
|
-
|
|
64
|
+
const moduleType = `typeof import(${JSON.stringify(specifier)})`;
|
|
65
|
+
return `${name}<Pick<${moduleType}, Extract<${picks.join(" | ")}, keyof ${moduleType}>>>`;
|
|
63
66
|
}
|
|
64
67
|
const isLazyRefKey = (key) => key.startsWith("$") && !key.startsWith("$$");
|
|
65
68
|
const isEagerRefKey = (key) => key.startsWith("$$");
|
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.
|
|
5
|
+
"version": "0.4.2",
|
|
6
6
|
"author": "Ryan Carniato",
|
|
7
7
|
"homepage": "https://github.com/solidjs/filesystem-routing#readme",
|
|
8
8
|
"repository": {
|