@tschk/moonshine-compiler 0.3.2 → 0.3.4
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/package.json +3 -3
- package/src/discover.ts +136 -12
- package/src/index.ts +1 -0
- package/src/inherit.ts +40 -7
- package/src/manifest.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tschk/moonshine-compiler",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"test": "bun test"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@tschk/moonshine-framework": "^0.3.
|
|
19
|
-
"@tschk/moonshine-router": "^0.3.
|
|
18
|
+
"@tschk/moonshine-framework": "^0.3.4",
|
|
19
|
+
"@tschk/moonshine-router": "^0.3.4"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/bun": "1.3.14",
|
package/src/discover.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { readdir } from "node:fs/promises";
|
|
2
2
|
import { dirname, basename, join, relative, resolve } from "node:path";
|
|
3
3
|
import type { RouteDefinition, RenderMode } from "@tschk/moonshine-framework";
|
|
4
|
-
import { mergeRoutes } from "./inherit.js";
|
|
4
|
+
import { mergeRoutes, type RouteConvention } from "./inherit.js";
|
|
5
5
|
import { toPosix } from "./path.js";
|
|
6
6
|
|
|
7
|
+
export type { RouteConvention } from "./inherit.js";
|
|
8
|
+
|
|
7
9
|
export type DiscoverOptions = {
|
|
8
10
|
routesDir: string;
|
|
9
11
|
programmatic?: RouteDefinition[];
|
|
12
|
+
/**
|
|
13
|
+
* File-system routing dialect. `moonshine` is the native `src/routes` layout;
|
|
14
|
+
* the two `next-*` values read an adopted Next app in place.
|
|
15
|
+
*/
|
|
16
|
+
convention?: RouteConvention;
|
|
10
17
|
};
|
|
11
18
|
|
|
12
19
|
export function segmentToPattern(segment: string): string {
|
|
@@ -43,15 +50,109 @@ async function* walkFiles(dir: string): AsyncGenerator<string> {
|
|
|
43
50
|
}
|
|
44
51
|
}
|
|
45
52
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
const ROUTE_EXT = /\.(tsx?|jsx?|mjs)$/;
|
|
54
|
+
|
|
55
|
+
/** Segments the App Router strips from the URL, or refuses to route at all. */
|
|
56
|
+
function isRouteGroup(segment: string): boolean {
|
|
57
|
+
return /^\([^)]*\)$/.test(segment);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isPrivateSegment(segment: string): boolean {
|
|
61
|
+
return segment.startsWith("_") || segment.startsWith("@");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Directory chain of an App Router file, as URL pattern plus a stable id. */
|
|
65
|
+
function appRouterPath(dir: string): { path: string; id: string } | null {
|
|
66
|
+
const parts = dir === "." ? [] : dir.split("/").filter(Boolean);
|
|
67
|
+
const pattern: string[] = [];
|
|
68
|
+
for (const part of parts) {
|
|
69
|
+
if (isPrivateSegment(part)) return null;
|
|
70
|
+
if (isRouteGroup(part)) continue;
|
|
71
|
+
pattern.push(segmentToPattern(part));
|
|
53
72
|
}
|
|
73
|
+
return {
|
|
74
|
+
path: pattern.length ? `/${pattern.join("/")}` : "/",
|
|
75
|
+
id: parts.length ? parts.join("/") : "index",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* `app/**\/page.tsx` and `app/**\/route.ts`. Every other App Router special
|
|
81
|
+
* (`layout`, `loading`, `template`, `not-found`, `error`, `default`) is either
|
|
82
|
+
* picked up by layout inheritance or has no moonshine equivalent.
|
|
83
|
+
*/
|
|
84
|
+
function discoverNextApp(
|
|
85
|
+
files: string[],
|
|
86
|
+
routesDir: string,
|
|
87
|
+
): RouteDefinition[] {
|
|
88
|
+
const raw: RouteDefinition[] = [];
|
|
89
|
+
for (const file of files) {
|
|
90
|
+
const rel = toPosix(relative(routesDir, file));
|
|
91
|
+
const name = basename(rel);
|
|
92
|
+
if (!ROUTE_EXT.test(name)) continue;
|
|
93
|
+
const base = name.replace(ROUTE_EXT, "");
|
|
94
|
+
if (base !== "page" && base !== "route") continue;
|
|
95
|
+
|
|
96
|
+
const located = appRouterPath(dirname(rel));
|
|
97
|
+
if (!located) continue;
|
|
98
|
+
|
|
99
|
+
const mode: RenderMode | undefined = base === "route" ? "api" : undefined;
|
|
100
|
+
raw.push({
|
|
101
|
+
id: base === "route" ? `${located.id}#route` : located.id,
|
|
102
|
+
path: located.path,
|
|
103
|
+
file,
|
|
104
|
+
...(mode && { mode }),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return raw;
|
|
108
|
+
}
|
|
54
109
|
|
|
110
|
+
/** Files the Pages Router treats as framework hooks rather than routes. */
|
|
111
|
+
const PAGES_RESERVED = new Set([
|
|
112
|
+
"_app",
|
|
113
|
+
"_document",
|
|
114
|
+
"_error",
|
|
115
|
+
"middleware",
|
|
116
|
+
"_middleware",
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
/** `pages/**\/*.tsx`, with `pages/api/**` mapped onto moonshine's api mode. */
|
|
120
|
+
function discoverNextPages(
|
|
121
|
+
files: string[],
|
|
122
|
+
routesDir: string,
|
|
123
|
+
): RouteDefinition[] {
|
|
124
|
+
const raw: RouteDefinition[] = [];
|
|
125
|
+
for (const file of files) {
|
|
126
|
+
const rel = toPosix(relative(routesDir, file));
|
|
127
|
+
const name = basename(rel);
|
|
128
|
+
if (!ROUTE_EXT.test(name)) continue;
|
|
129
|
+
if (/\.(test|spec|d)\.[jt]sx?$/.test(name)) continue;
|
|
130
|
+
const base = name.replace(ROUTE_EXT, "");
|
|
131
|
+
if (PAGES_RESERVED.has(base)) continue;
|
|
132
|
+
|
|
133
|
+
const dir = dirname(rel);
|
|
134
|
+
const parts = dir === "." ? [] : dir.split("/").filter(Boolean);
|
|
135
|
+
if (parts.some(isPrivateSegment)) continue;
|
|
136
|
+
|
|
137
|
+
const segments = parts.map(segmentToPattern);
|
|
138
|
+
if (base !== "index") segments.push(segmentToPattern(base));
|
|
139
|
+
const path = segments.length ? `/${segments.join("/")}` : "/";
|
|
140
|
+
const mode: RenderMode | undefined = parts[0] === "api" ? "api" : undefined;
|
|
141
|
+
|
|
142
|
+
raw.push({
|
|
143
|
+
id: dir === "." ? base : `${dir}/${base}`,
|
|
144
|
+
path,
|
|
145
|
+
file,
|
|
146
|
+
...(mode && { mode }),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return raw;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function discoverMoonshine(
|
|
153
|
+
files: string[],
|
|
154
|
+
routesDir: string,
|
|
155
|
+
): RouteDefinition[] {
|
|
55
156
|
const fileSet = new Set(files);
|
|
56
157
|
const raw: RouteDefinition[] = [];
|
|
57
158
|
|
|
@@ -88,18 +189,41 @@ export async function discoverRoutes(
|
|
|
88
189
|
const mode: RenderMode | undefined =
|
|
89
190
|
suffix === "server" ? "api" : undefined;
|
|
90
191
|
|
|
91
|
-
|
|
192
|
+
raw.push({
|
|
92
193
|
id,
|
|
93
194
|
path: routePath,
|
|
94
195
|
file,
|
|
95
196
|
...(dataFile && { dataFile }),
|
|
96
197
|
...(mode && { mode }),
|
|
97
|
-
};
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return raw;
|
|
202
|
+
}
|
|
98
203
|
|
|
99
|
-
|
|
204
|
+
export async function discoverRoutes(
|
|
205
|
+
options: DiscoverOptions,
|
|
206
|
+
): Promise<RouteDefinition[]> {
|
|
207
|
+
const routesDir = resolve(options.routesDir);
|
|
208
|
+
const convention = options.convention ?? "moonshine";
|
|
209
|
+
const files: string[] = [];
|
|
210
|
+
for await (const file of walkFiles(routesDir)) {
|
|
211
|
+
files.push(file);
|
|
100
212
|
}
|
|
101
213
|
|
|
102
|
-
const
|
|
214
|
+
const raw =
|
|
215
|
+
convention === "next-app"
|
|
216
|
+
? discoverNextApp(files, routesDir)
|
|
217
|
+
: convention === "next-pages"
|
|
218
|
+
? discoverNextPages(files, routesDir)
|
|
219
|
+
: discoverMoonshine(files, routesDir);
|
|
220
|
+
|
|
221
|
+
const merged = mergeRoutes(
|
|
222
|
+
raw,
|
|
223
|
+
options.programmatic ?? [],
|
|
224
|
+
routesDir,
|
|
225
|
+
convention,
|
|
226
|
+
);
|
|
103
227
|
merged.sort(
|
|
104
228
|
(a, b) => a.path.localeCompare(b.path) || a.id.localeCompare(b.id),
|
|
105
229
|
);
|
package/src/index.ts
CHANGED
package/src/inherit.ts
CHANGED
|
@@ -6,20 +6,52 @@ import { toPosix } from "./path.js";
|
|
|
6
6
|
|
|
7
7
|
type SpecialKind = "layout" | "error" | "middleware";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Moonshine names co-located specials `+layout.tsx`; the Next App Router names
|
|
11
|
+
* them `layout.tsx`. Only the prefix differs, so one lookup serves both.
|
|
12
|
+
*/
|
|
13
|
+
export type RouteConvention = "moonshine" | "next-app" | "next-pages";
|
|
14
|
+
|
|
15
|
+
const specialPrefix: Readonly<Record<RouteConvention, string | null>> = {
|
|
16
|
+
moonshine: "+",
|
|
17
|
+
"next-app": "",
|
|
18
|
+
// The Pages Router has no per-directory specials; `_app` wraps everything.
|
|
19
|
+
"next-pages": null,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The App Router's `error.tsx` takes `{ error, reset }` and its root
|
|
24
|
+
* `middleware.ts` runs on the edge, and neither matches the moonshine
|
|
25
|
+
* contract, so only layouts are inherited under that convention.
|
|
26
|
+
*/
|
|
27
|
+
const specialKinds: Readonly<Record<RouteConvention, SpecialKind[]>> = {
|
|
28
|
+
moonshine: ["layout", "error", "middleware"],
|
|
29
|
+
"next-app": ["layout"],
|
|
30
|
+
"next-pages": [],
|
|
31
|
+
};
|
|
32
|
+
|
|
9
33
|
function findSpecial(
|
|
10
34
|
routesDir: string,
|
|
11
35
|
dir: string,
|
|
12
36
|
kind: SpecialKind,
|
|
37
|
+
convention: RouteConvention,
|
|
13
38
|
): string | undefined {
|
|
14
|
-
const
|
|
15
|
-
|
|
39
|
+
const prefix = specialPrefix[convention];
|
|
40
|
+
if (prefix === null) return undefined;
|
|
41
|
+
if (!specialKinds[convention].includes(kind)) return undefined;
|
|
42
|
+
const base = join(routesDir, dir, `${prefix}${kind}`);
|
|
43
|
+
for (const ext of [".tsx", ".ts", ".jsx", ".js"]) {
|
|
16
44
|
const candidate = `${base}${ext}`;
|
|
17
45
|
if (existsSync(candidate)) return candidate;
|
|
18
46
|
}
|
|
19
47
|
return undefined;
|
|
20
48
|
}
|
|
21
49
|
|
|
22
|
-
function applyInheritance(
|
|
50
|
+
function applyInheritance(
|
|
51
|
+
routes: RouteDefinition[],
|
|
52
|
+
routesDir: string,
|
|
53
|
+
convention: RouteConvention,
|
|
54
|
+
): void {
|
|
23
55
|
const resolvedDir = resolve(routesDir);
|
|
24
56
|
for (const route of routes) {
|
|
25
57
|
if (!route.file) continue;
|
|
@@ -38,11 +70,11 @@ function applyInheritance(routes: RouteDefinition[], routesDir: string): void {
|
|
|
38
70
|
let errorBoundary: string | undefined;
|
|
39
71
|
|
|
40
72
|
for (const ancestor of ancestors) {
|
|
41
|
-
const layout = findSpecial(resolvedDir, ancestor, "layout");
|
|
73
|
+
const layout = findSpecial(resolvedDir, ancestor, "layout", convention);
|
|
42
74
|
if (layout) layouts.push(layout);
|
|
43
|
-
const mw = findSpecial(resolvedDir, ancestor, "middleware");
|
|
75
|
+
const mw = findSpecial(resolvedDir, ancestor, "middleware", convention);
|
|
44
76
|
if (mw) middleware.push(mw);
|
|
45
|
-
const err = findSpecial(resolvedDir, ancestor, "error");
|
|
77
|
+
const err = findSpecial(resolvedDir, ancestor, "error", convention);
|
|
46
78
|
if (err) errorBoundary = err;
|
|
47
79
|
}
|
|
48
80
|
|
|
@@ -56,6 +88,7 @@ export function mergeRoutes(
|
|
|
56
88
|
filesystem: RouteDefinition[],
|
|
57
89
|
programmatic: RouteDefinition[] = [],
|
|
58
90
|
routesDir?: string,
|
|
91
|
+
convention: RouteConvention = "moonshine",
|
|
59
92
|
): RouteDefinition[] {
|
|
60
93
|
const map = new Map<string, RouteDefinition>();
|
|
61
94
|
for (const route of filesystem) {
|
|
@@ -70,7 +103,7 @@ export function mergeRoutes(
|
|
|
70
103
|
}
|
|
71
104
|
}
|
|
72
105
|
const merged = [...map.values()];
|
|
73
|
-
if (routesDir) applyInheritance(merged, routesDir);
|
|
106
|
+
if (routesDir) applyInheritance(merged, routesDir, convention);
|
|
74
107
|
createRouteGraph(merged);
|
|
75
108
|
return merged;
|
|
76
109
|
}
|
package/src/manifest.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { analyzeModule } from "./analyze.js";
|
|
|
11
11
|
import { buildBundles } from "./build.js";
|
|
12
12
|
import { classifyRoute } from "./classify.js";
|
|
13
13
|
import { discoverRoutes } from "./discover.js";
|
|
14
|
+
import type { RouteConvention } from "./inherit.js";
|
|
14
15
|
import { toPosix } from "./path.js";
|
|
15
16
|
|
|
16
17
|
export type BuildOptions = {
|
|
@@ -19,6 +20,7 @@ export type BuildOptions = {
|
|
|
19
20
|
outDir?: string;
|
|
20
21
|
programmatic?: RouteDefinition[];
|
|
21
22
|
runtime?: RuntimeTarget;
|
|
23
|
+
convention?: RouteConvention;
|
|
22
24
|
};
|
|
23
25
|
|
|
24
26
|
function toProjectRelative(projectDir: string, p: string): string {
|
|
@@ -94,6 +96,7 @@ export async function buildProject(
|
|
|
94
96
|
const rawRoutes = await discoverRoutes({
|
|
95
97
|
routesDir,
|
|
96
98
|
programmatic: options.programmatic,
|
|
99
|
+
...(options.convention && { convention: options.convention }),
|
|
97
100
|
});
|
|
98
101
|
|
|
99
102
|
const buildRoutes: RouteArtifact[] = [];
|