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,123 @@
1
+ import { relative } from "node:path";
2
+ import { normalizePath } from "../router.js";
3
+ /**
4
+ * Type generation for the virtual manifest module.
5
+ *
6
+ * The delivered manifest is a runtime value, so a router that derives types
7
+ * from its route table — `@solidjs/router`'s typed `paths` proxy, say — gets
8
+ * nothing from it: `RoutePaths` degrades to `any` as soon as the route array
9
+ * is a plain `RouteDefinition[]` rather than a tuple. Emitting a declaration
10
+ * that types the module as a *literal tuple* is what keeps those types alive
11
+ * across the file-routing seam.
12
+ *
13
+ * Emission adapters have to preserve the tuple in turn, by mapping with a
14
+ * `<const T extends readonly ...>` signature rather than a plain `.map`.
15
+ */
16
+ const HEADER = `// Generated by filesystem-routing. Do not edit.
17
+ // Regenerated whenever a route file is added, changed or removed.`;
18
+ const REF_TYPES = ` /** A code-split ref: the delivery adapter emits it as a dynamic import. */
19
+ export interface FileRouteLazyRef<M = Record<string, unknown>> {
20
+ src: string;
21
+ import(): Promise<M>;
22
+ }
23
+
24
+ /** An eager ref: its picked exports are imported statically. */
25
+ export interface FileRouteEagerRef<M = Record<string, unknown>> {
26
+ require(): M;
27
+ }
28
+
29
+ /** A flat entry of the route manifest. */
30
+ export interface FileRouteEntry {
31
+ path: string;
32
+ page?: boolean;
33
+ $component?: FileRouteLazyRef<any>;
34
+ $$route?: FileRouteEagerRef<any>;
35
+ [key: string]: unknown;
36
+ }
37
+
38
+ /** An entry of the nested \`pageRoutes\` view. */
39
+ export interface FileRouteTreeEntry extends FileRouteEntry {
40
+ id: string;
41
+ children?: readonly FileRouteTreeEntry[];
42
+ }`;
43
+ /** Extensions TypeScript can resolve through a `typeof import(...)`. */
44
+ const TYPED_MODULE = /\.([mc]?[jt]sx?)$/;
45
+ /**
46
+ * Types a ref by the module it points at, so a page's `default` really is the
47
+ * component and a `route` export really is the config the module declares —
48
+ * which is what saves an emission adapter from casting either one. Modules
49
+ * TypeScript cannot resolve (`.md`, `.mdx`) fall back to the generic ref.
50
+ */
51
+ function refType(ref, kind, from) {
52
+ const name = kind === "eager" ? "FileRouteEagerRef" : "FileRouteLazyRef";
53
+ if (!TYPED_MODULE.test(ref.src))
54
+ return name;
55
+ let specifier = normalizePath(relative(from, ref.src)).replace(TYPED_MODULE, "");
56
+ if (!specifier.startsWith("."))
57
+ specifier = `./${specifier}`;
58
+ return `${name}<typeof import(${JSON.stringify(specifier)})>`;
59
+ }
60
+ const isLazyRefKey = (key) => key.startsWith("$") && !key.startsWith("$$");
61
+ const isEagerRefKey = (key) => key.startsWith("$$");
62
+ /**
63
+ * Types one manifest entry, keeping `path` literal so it reaches path-derived
64
+ * types intact. Absent refs are typed `?: undefined` rather than omitted, so
65
+ * an adapter can read `entry.$$route` uniformly across the tuple.
66
+ */
67
+ function entryType(entry, indent, nested, from) {
68
+ const inner = indent + " ";
69
+ const fields = [`path: ${JSON.stringify(entry.path)}`];
70
+ if (nested)
71
+ fields.push(`id: ${JSON.stringify(entry.id)}`);
72
+ fields.push(`page: ${entry.page ? "true" : "false"}`);
73
+ // A convention records refs it did not emit as present-but-undefined keys,
74
+ // so go by the value, not the key.
75
+ const refKeys = Object.keys(entry).filter(key => key.startsWith("$") && entry[key] !== undefined);
76
+ for (const key of refKeys) {
77
+ const kind = isEagerRefKey(key) ? "eager" : "lazy";
78
+ fields.push(`${key}: ${refType(entry[key], kind, from)}`);
79
+ }
80
+ // Reading an absent ref is how adapters branch, so keep the key present.
81
+ if (!refKeys.some(isLazyRefKey))
82
+ fields.push(`$component?: undefined`);
83
+ if (!refKeys.some(isEagerRefKey))
84
+ fields.push(`$$route?: undefined`);
85
+ if (nested) {
86
+ const children = entry.children;
87
+ fields.push(children?.length
88
+ ? `children: ${tupleType(children, inner, true, from)}`
89
+ : `children?: undefined`);
90
+ }
91
+ return `{\n${fields.map(field => `${inner}${field};`).join("\n")}\n${indent}}`;
92
+ }
93
+ function tupleType(entries, indent, nested, from) {
94
+ if (!entries.length)
95
+ return "readonly []";
96
+ const inner = indent + " ";
97
+ const members = entries
98
+ .map(entry => `${inner}${entryType(entry, inner, nested, from)}`)
99
+ .join(",\n");
100
+ return `readonly [\n${members}\n${indent}]`;
101
+ }
102
+ /**
103
+ * Builds the ambient declaration for `moduleId`. It is self-contained and
104
+ * replaces `filesystem-routing/types` — reference one or the other, never
105
+ * both, or TypeScript sees two declarations of the same module.
106
+ */
107
+ export function serializeTypes(moduleId, routes, pageRoutes,
108
+ /** Directory the declaration is written to; module specifiers are relative to it. */
109
+ from) {
110
+ return `${HEADER}
111
+
112
+ declare module ${JSON.stringify(moduleId)} {
113
+ ${REF_TYPES}
114
+
115
+ /** The flat route manifest, in scan order. */
116
+ const routes: ${tupleType(routes, " ", false, from)};
117
+ export default routes;
118
+
119
+ /** The page entries, nested by path with grouping segments stripped. */
120
+ export const pageRoutes: ${tupleType(pageRoutes, " ", true, from)};
121
+ }
122
+ `;
123
+ }
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "filesystem-routing",
3
+ "description": "Router-neutral file-system routing: scans a route directory into a neutral route manifest with pluggable conventions and delivery adapters",
4
+ "license": "MIT",
5
+ "version": "0.1.0",
6
+ "author": "Ryan Carniato",
7
+ "homepage": "https://github.com/solidjs/filesystem-routing#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/solidjs/filesystem-routing"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/solidjs/filesystem-routing/issues"
14
+ },
15
+ "keywords": [
16
+ "file-system routing",
17
+ "file routes",
18
+ "router",
19
+ "vite",
20
+ "vite-plugin",
21
+ "solid"
22
+ ],
23
+ "sideEffects": false,
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "type": "module",
28
+ "main": "dist/index.js",
29
+ "types": "dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ },
35
+ "./tree": {
36
+ "types": "./dist/tree.d.ts",
37
+ "default": "./dist/tree.js"
38
+ },
39
+ "./vite": {
40
+ "types": "./dist/vite/index.d.ts",
41
+ "default": "./dist/vite/index.js"
42
+ },
43
+ "./types": {
44
+ "types": "./types.d.ts"
45
+ }
46
+ },
47
+ "files": [
48
+ "dist",
49
+ "types.d.ts"
50
+ ],
51
+ "dependencies": {
52
+ "@babel/core": "^7.29.0",
53
+ "@types/babel__core": "^7.20.5",
54
+ "@types/babel__traverse": "^7.28.0",
55
+ "@types/micromatch": "^4.0.10",
56
+ "fast-glob": "^3.3.3",
57
+ "micromatch": "^4.0.8",
58
+ "oxc-parser": "^0.139.0"
59
+ },
60
+ "peerDependencies": {
61
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
62
+ },
63
+ "peerDependenciesMeta": {
64
+ "vite": {
65
+ "optional": true
66
+ }
67
+ },
68
+ "devDependencies": {
69
+ "@changesets/cli": "^2.31.1",
70
+ "@types/node": "^22.10.0",
71
+ "prettier": "^3.4.1",
72
+ "typescript": "^5.7.2",
73
+ "vite": "^6.0.0",
74
+ "vitest": "^2.1.6"
75
+ },
76
+ "scripts": {
77
+ "build": "rm -rf dist && tsc",
78
+ "test": "vitest run",
79
+ "test:watch": "vitest",
80
+ "pretty": "prettier --write \"{src,test}/**/*.ts\"",
81
+ "typecheck:dist": "pnpx @arethetypeswrong/cli --pack . --profile esm-only",
82
+ "release": "pnpm build && changeset publish"
83
+ }
84
+ }
package/types.d.ts ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Types for the virtual route-manifest module served by the Vite delivery
3
+ * adapter. Frameworks and apps that import the manifest directly reference
4
+ * this file once:
5
+ *
6
+ * ```ts
7
+ * /// <reference types="filesystem-routing/types" />
8
+ * ```
9
+ *
10
+ * These describe the manifest *as delivered*: the module refs the scanner
11
+ * records as `{ src, pick }` reach the app as callable imports. A project
12
+ * serving the manifest from a custom `moduleId` declares that id itself,
13
+ * with the same two exports.
14
+ */
15
+
16
+ declare module "virtual:file-routes" {
17
+ /** A code-split ref: the delivery adapter emits it as a dynamic import. */
18
+ export interface FileRouteLazyRef {
19
+ src: string;
20
+ import(): Promise<Record<string, unknown>>;
21
+ }
22
+
23
+ /** An eager ref: its picked exports are imported statically. */
24
+ export interface FileRouteEagerRef {
25
+ require(): Record<string, unknown>;
26
+ }
27
+
28
+ /** A flat entry of the route manifest. */
29
+ export interface FileRouteEntry {
30
+ /**
31
+ * The route path in the neutral pattern language (`:param`, `:param?`,
32
+ * `*rest`), with any grouping segments still present.
33
+ */
34
+ path: string;
35
+ /** `true` when the module renders a page. */
36
+ page?: boolean;
37
+ /** The page component module. */
38
+ $component?: FileRouteLazyRef;
39
+ /** The route config (`route` export), when the module has one. */
40
+ $$route?: FileRouteEagerRef;
41
+ /** Refs added by convention extensions, e.g. `$GET` handlers. */
42
+ [key: string]: unknown;
43
+ }
44
+
45
+ /** An entry of the nested `pageRoutes` view. */
46
+ export interface FileRouteTreeEntry extends FileRouteEntry {
47
+ /** The manifest path this entry was nested under, groups included. */
48
+ id: string;
49
+ children?: FileRouteTreeEntry[];
50
+ }
51
+
52
+ /** The flat route manifest, in scan order. */
53
+ const routes: FileRouteEntry[];
54
+ export default routes;
55
+
56
+ /** The page entries, nested by path with grouping segments stripped. */
57
+ export const pageRoutes: FileRouteTreeEntry[];
58
+ }