filesystem-routing 0.1.0 → 0.2.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.
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
- | `RouteDefinition` emission | `@solidjs/router/fs` |
17
- | Request handling for `GET`/`POST` routes, middleware | `@solidjs/start` |
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 never imports the virtual module itself — the app does,
51
- so the adapter resolves without the plugin, custom `moduleId`s work, and
52
- nothing needs excluding from dependency prebundling.
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
- This package stops at the manifest: it discovers handlers, it does not serve
165
- them. Dispatch is a few lines in whatever server consumes the manifest —
166
- match the URL, import the ref, call the export:
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
- // any SSR handler or middleware
171
+ // src/middleware.ts
170
172
  import routes from "virtual:file-routes";
173
+ import { createAPIHandler } from "filesystem-routing/api";
171
174
 
172
- async function handleApi(request: Request, entry, params) {
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
- The handler's argument is whatever your server passes — the convention only
182
- dictates the export names. Frameworks typically enable `httpMethods` on the
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.
@@ -247,10 +253,10 @@ with `(group)` segments stripped, so emission adapters don't each reimplement
247
253
  the tree (`buildRouteTree` from `filesystem-routing/tree` is the same
248
254
  function, for consumers holding only a flat manifest). An emission adapter is
249
255
  a function taking manifest entries and returning the router's shape — see
250
- `fileRoutes` in `@solidjs/router/fs` for Solid Router's, a ~30 line adapter
251
- other routers can mirror. Adapters take the manifest as an argument rather
252
- than importing the virtual module, so they work standalone and with custom
253
- module ids.
256
+ `fileRoutes` in `@solidjs/router/fs` for Solid Router's, an
257
+ adapter other routers can mirror. Adapters take the manifest as an argument
258
+ rather than importing the virtual module, so they work standalone and with
259
+ custom module ids.
254
260
 
255
261
  Add `/// <reference types="filesystem-routing/types" />` to type the import.
256
262
 
package/dist/api.d.ts ADDED
@@ -0,0 +1,90 @@
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
+ export interface APIHandlerRef {
43
+ import(): Promise<Record<string, APIHandler>>;
44
+ }
45
+ export interface APIMatch {
46
+ handler: APIHandlerRef;
47
+ params?: Record<string, string>;
48
+ /** `true` when the module also renders a page at this path. */
49
+ isPage: boolean;
50
+ }
51
+ /**
52
+ * Builds a matcher over the flat manifest's handler-carrying entries. Route
53
+ * paths translate from the neutral pattern language to the radix tree —
54
+ * `(group)` segments stripped, `*rest` catch-alls, static segments
55
+ * URL-encoded. Optional parameters are rejected (a radix tree cannot
56
+ * represent them) and duplicate paths are a configuration error.
57
+ */
58
+ export declare function createAPIMatcher(routes: readonly FileRouteHandlers[]): (path: string, method: string) => APIMatch | undefined;
59
+ /** Strips a leading path base: `stripPathBase("/app/x", "/app")` → `/x`. */
60
+ export declare function stripPathBase(path: string, base: string): string;
61
+ export interface APIHandlerOptions {
62
+ /** A path base stripped from the URL before matching. Defaults to `/`. */
63
+ base?: string;
64
+ /**
65
+ * How the adapter reaches the request event it hands to handlers. Defaults
66
+ * to the core request-event scope (the storage `provideRequestEvent`
67
+ * establishes), which is where a middleware chain runs — override it when
68
+ * dispatching outside one, or to hand handlers a richer event.
69
+ */
70
+ getEvent?: () => APIEvent;
71
+ }
72
+ /**
73
+ * Fetch-style middleware serving the manifest's API routes:
74
+ *
75
+ * ```ts
76
+ * import routes from "virtual:file-routes";
77
+ * import { createAPIHandler } from "filesystem-routing/api";
78
+ *
79
+ * export default [createAPIHandler(routes)];
80
+ * ```
81
+ *
82
+ * Dispatch preserves SolidStart's semantics: unmatched requests (no route,
83
+ * or no handler for the method) advance the chain; `HEAD` falls back to the
84
+ * `GET` handler; matched params are written to `event.params` before the
85
+ * handler runs. A handler returning `undefined` declines the request —
86
+ * allowed for `GET` only (anything else throws): a page module falls
87
+ * through to the chain (its component renders instead), a handler-only
88
+ * module answers 404.
89
+ */
90
+ 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 = 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/router.d.ts CHANGED
@@ -3,7 +3,10 @@ export declare const glob: (path: string) => string[];
3
3
  /** Normalize a file path to posix separators (bundler-agnostic). */
4
4
  export declare function normalizePath(path: string): string;
5
5
  export interface FileSystemRouterConfig {
6
- /** Absolute path of the route directory to scan. */
6
+ /**
7
+ * Route directory to scan. Relative paths are resolved against the
8
+ * current working directory.
9
+ */
7
10
  dir: string;
8
11
  /** File extensions (without the dot) that participate in routing. */
9
12
  extensions: string[];
package/dist/router.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import fg from "fast-glob";
2
2
  import micromatch from "micromatch";
3
- import { posix, sep } from "node:path";
3
+ import { posix, resolve, sep } from "node:path";
4
4
  export const glob = (path) => fg.sync(path, { absolute: true });
5
5
  /** Normalize a file path to posix separators (bundler-agnostic). */
6
6
  export function normalizePath(path) {
@@ -24,7 +24,10 @@ export class BaseFileSystemRouter extends EventTarget {
24
24
  constructor(config) {
25
25
  super();
26
26
  this.routes = [];
27
- this.config = config;
27
+ // A relative dir would glob fine, but the matched files come back
28
+ // absolute and would then fail `isRoute`'s micromatch against the
29
+ // relative pattern, silently yielding zero routes.
30
+ this.config = { ...config, dir: normalizePath(resolve(config.dir)) };
28
31
  }
29
32
  glob() {
30
33
  const extensions = this.config.extensions;
@@ -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`. */
@@ -5,10 +5,10 @@ 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
13
  /**
14
14
  * The Vite delivery adapter for `filesystem-routing`.
@@ -61,20 +61,27 @@ export function fileRoutes(options = {}) {
61
61
  writeFileSync(file, contents);
62
62
  }
63
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("&")}`;
64
+ const toModuleId = (ref) => toPickId(ref.src, ref.pick);
65
65
  return [
66
66
  {
67
67
  name: "filesystem-routing",
68
68
  enforce: "pre",
69
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
70
  return {
75
- optimizeDeps: {
76
- exclude: options.optimizeDepsExclude
77
- }
71
+ build: {
72
+ rollupOptions: {
73
+ output: {
74
+ // Keeps route chunks named after their file rather than after
75
+ // the `?pick=...` id that addresses them. See toPickId.
76
+ sanitizeFileName: sanitizeChunkFileName
77
+ }
78
+ }
79
+ },
80
+ // Packages importing the virtual module (which only this plugin can
81
+ // resolve) must stay out of esbuild prebundling.
82
+ ...(options.optimizeDepsExclude?.length
83
+ ? { optimizeDeps: { exclude: options.optimizeDepsExclude } }
84
+ : {})
78
85
  };
79
86
  },
80
87
  configResolved(config) {
@@ -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;
@@ -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/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.1.0",
5
+ "version": "0.2.0",
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"