nukejs 0.0.6 → 0.0.8
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 +89 -6
- package/dist/{as-is/Link.js → Link.js} +3 -1
- package/dist/Link.js.map +7 -0
- package/dist/app.d.ts +3 -2
- package/dist/app.js +3 -13
- package/dist/app.js.map +2 -2
- package/dist/build-common.d.ts +6 -0
- package/dist/build-common.js +20 -6
- package/dist/build-common.js.map +2 -2
- package/dist/build-node.d.ts +1 -1
- package/dist/build-node.js +6 -17
- package/dist/build-node.js.map +2 -2
- package/dist/build-vercel.js +1 -1
- package/dist/build-vercel.js.map +2 -2
- package/dist/builder.d.ts +4 -10
- package/dist/builder.js +7 -38
- package/dist/builder.js.map +2 -2
- package/dist/bundle.js +60 -4
- package/dist/bundle.js.map +2 -2
- package/dist/component-analyzer.d.ts +6 -0
- package/dist/component-analyzer.js +12 -1
- package/dist/component-analyzer.js.map +2 -2
- package/dist/hmr-bundle.js +17 -4
- package/dist/hmr-bundle.js.map +2 -2
- package/dist/html-store.d.ts +7 -0
- package/dist/html-store.js.map +2 -2
- package/dist/http-server.d.ts +2 -9
- package/dist/http-server.js +16 -2
- package/dist/http-server.js.map +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/renderer.js +2 -7
- package/dist/renderer.js.map +2 -2
- package/dist/router.d.ts +20 -19
- package/dist/router.js +14 -6
- package/dist/router.js.map +2 -2
- package/dist/ssr.js +21 -4
- package/dist/ssr.js.map +2 -2
- package/dist/use-html.js +5 -1
- package/dist/use-html.js.map +2 -2
- package/dist/{as-is/useRouter.js → use-router.js} +1 -1
- package/dist/{as-is/useRouter.js.map → use-router.js.map} +2 -2
- package/package.json +1 -1
- package/dist/as-is/Link.js.map +0 -7
- package/dist/as-is/Link.tsx +0 -20
- package/dist/as-is/useRouter.ts +0 -33
- /package/dist/{as-is/Link.d.ts → Link.d.ts} +0 -0
- /package/dist/{as-is/useRouter.d.ts → use-router.d.ts} +0 -0
package/dist/html-store.d.ts
CHANGED
|
@@ -68,6 +68,13 @@ export interface ScriptTag {
|
|
|
68
68
|
crossOrigin?: string;
|
|
69
69
|
integrity?: string;
|
|
70
70
|
noModule?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Where to inject the script in the document.
|
|
73
|
+
* 'head' (default) — placed inside <head>, inside the <!--n-head--> block.
|
|
74
|
+
* 'body' — placed at the very end of <body>, inside the
|
|
75
|
+
* <!--n-body-scripts--> block, just before </body>.
|
|
76
|
+
*/
|
|
77
|
+
position?: 'head' | 'body';
|
|
71
78
|
}
|
|
72
79
|
export interface StyleTag {
|
|
73
80
|
content?: string;
|
package/dist/html-store.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/html-store.ts"],
|
|
4
|
-
"sourcesContent": ["/**\r\n * html-store.ts \u2014 Per-Request HTML Head Store\r\n *\r\n * Provides a request-scoped store that server components can write to via\r\n * `useHtml()` during SSR. The accumulated values are flushed into the\r\n * rendered HTML document after the component tree is fully rendered.\r\n *\r\n * Why globalThis?\r\n * Node's module system may import this file multiple times if the page\r\n * module and the nukejs package resolve to different copies (e.g. when\r\n * running from source in dev with tsx). Using a well-known Symbol on\r\n * globalThis guarantees all copies share the same store instance.\r\n *\r\n * Request isolation:\r\n * runWithHtmlStore() creates a fresh store before rendering and clears it\r\n * in the `finally` block, so concurrent requests cannot bleed into each other.\r\n *\r\n * Title resolution:\r\n * Layouts and pages can both call useHtml({ title: \u2026 }). Layouts typically\r\n * pass a template function:\r\n *\r\n * useHtml({ title: (prev) => `${prev} | Acme` })\r\n *\r\n * Operations are collected in render order (outermost layout first, page\r\n * last) then resolved *in reverse* so the page's string value is the base\r\n * and layout template functions wrap outward.\r\n */\r\n\r\n// \u2500\u2500\u2500 Public types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** A page sets a literal string; a layout wraps with a template function. */\r\nexport type TitleValue = string | ((prev: string) => string);\r\n\r\nexport interface HtmlAttrs {\r\n lang?: string;\r\n class?: string;\r\n style?: string;\r\n dir?: string;\r\n [attr: string]: string | undefined;\r\n}\r\n\r\nexport interface BodyAttrs {\r\n class?: string;\r\n style?: string;\r\n [attr: string]: string | undefined;\r\n}\r\n\r\nexport interface MetaTag {\r\n name?: string;\r\n property?: string;\r\n httpEquiv?: string;\r\n charset?: string;\r\n content?: string;\r\n [attr: string]: string | undefined;\r\n}\r\n\r\nexport interface LinkTag {\r\n rel?: string;\r\n href?: string;\r\n type?: string;\r\n media?: string;\r\n as?: string;\r\n crossOrigin?: string;\r\n integrity?: string;\r\n hrefLang?: string;\r\n sizes?: string;\r\n [attr: string]: string | undefined;\r\n}\r\n\r\nexport interface ScriptTag {\r\n src?: string;\r\n content?: string;\r\n type?: string;\r\n defer?: boolean;\r\n async?: boolean;\r\n crossOrigin?: string;\r\n integrity?: string;\r\n noModule?: boolean;\r\n}\r\n\r\nexport interface StyleTag {\r\n content?: string;\r\n media?: string;\r\n}\r\n\r\nexport interface HtmlStore {\r\n /** Collected in render order; resolved in reverse so the page title wins. */\r\n titleOps: TitleValue[];\r\n /** Attributes merged onto <html>; last write wins per attribute. */\r\n htmlAttrs: HtmlAttrs;\r\n /** Attributes merged onto <body>; last write wins per attribute. */\r\n bodyAttrs: BodyAttrs;\r\n /** Accumulated in render order: layouts first, page last. */\r\n meta: MetaTag[];\r\n link: LinkTag[];\r\n script: ScriptTag[];\r\n style: StyleTag[];\r\n}\r\n\r\n// \u2500\u2500\u2500 GlobalThis storage \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Well-known Symbol used to share the store across duplicate module copies. */\r\nconst KEY = Symbol.for('__nukejs_html_store__');\r\n\r\nconst getGlobal = (): HtmlStore | null => (globalThis as any)[KEY] ?? null;\r\nconst setGlobal = (store: HtmlStore | null): void => { (globalThis as any)[KEY] = store; };\r\n\r\nfunction emptyStore(): HtmlStore {\r\n return {\r\n titleOps: [],\r\n htmlAttrs: {},\r\n bodyAttrs: {},\r\n meta: [],\r\n link: [],\r\n script: [],\r\n style: [],\r\n };\r\n}\r\n\r\n// \u2500\u2500\u2500 Public API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Runs `fn` inside a fresh HTML store and returns the collected values.\r\n *\r\n * Usage in SSR:\r\n * ```ts\r\n * const store = await runWithHtmlStore(async () => {\r\n * appHtml = await renderElementToHtml(element, ctx);\r\n * });\r\n * // store.titleOps, store.meta, etc. are now populated\r\n * ```\r\n */\r\nexport async function runWithHtmlStore(fn: () => Promise<void>): Promise<HtmlStore> {\r\n setGlobal(emptyStore());\r\n try {\r\n await fn();\r\n return { ...(getGlobal() ?? emptyStore()) } as HtmlStore;\r\n } finally {\r\n // Always clear the store, even if rendering throws, to prevent leakage\r\n // into the next request on the same event-loop tick.\r\n setGlobal(null);\r\n }\r\n}\r\n\r\n/**\r\n * Returns the current request's store, or `undefined` if called outside of\r\n * a `runWithHtmlStore` context (e.g. in the browser or in a test).\r\n */\r\nexport function getHtmlStore(): HtmlStore | undefined {\r\n return getGlobal() ?? undefined;\r\n}\r\n\r\n/**\r\n * Resolves the final page title from a list of title operations.\r\n *\r\n * Operations are walked in *reverse* so the page's value is the starting\r\n * point and layout template functions wrap it outward:\r\n *\r\n * ```\r\n * ops = [ (p) => `${p} | Acme`, 'About' ] \u2190 layout pushed first, page last\r\n * Walk in reverse:\r\n * i=1: op = 'About' \u2192 title = 'About'\r\n * i=0: op = (p) => \u2026 \u2192 title = 'About | Acme'\r\n * ```\r\n *\r\n * @param fallback Used when ops is empty (e.g. a page that didn't call useHtml).\r\n */\r\nexport function resolveTitle(ops: TitleValue[], fallback = ''): string {\r\n let title = fallback;\r\n for (let i = ops.length - 1; i >= 0; i--) {\r\n const op = ops[i];\r\n title = typeof op === 'string' ? op : op(title);\r\n }\r\n return title;\r\n}
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/**\r\n * html-store.ts \u2014 Per-Request HTML Head Store\r\n *\r\n * Provides a request-scoped store that server components can write to via\r\n * `useHtml()` during SSR. The accumulated values are flushed into the\r\n * rendered HTML document after the component tree is fully rendered.\r\n *\r\n * Why globalThis?\r\n * Node's module system may import this file multiple times if the page\r\n * module and the nukejs package resolve to different copies (e.g. when\r\n * running from source in dev with tsx). Using a well-known Symbol on\r\n * globalThis guarantees all copies share the same store instance.\r\n *\r\n * Request isolation:\r\n * runWithHtmlStore() creates a fresh store before rendering and clears it\r\n * in the `finally` block, so concurrent requests cannot bleed into each other.\r\n *\r\n * Title resolution:\r\n * Layouts and pages can both call useHtml({ title: \u2026 }). Layouts typically\r\n * pass a template function:\r\n *\r\n * useHtml({ title: (prev) => `${prev} | Acme` })\r\n *\r\n * Operations are collected in render order (outermost layout first, page\r\n * last) then resolved *in reverse* so the page's string value is the base\r\n * and layout template functions wrap outward.\r\n */\r\n\r\n// \u2500\u2500\u2500 Public types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** A page sets a literal string; a layout wraps with a template function. */\r\nexport type TitleValue = string | ((prev: string) => string);\r\n\r\nexport interface HtmlAttrs {\r\n lang?: string;\r\n class?: string;\r\n style?: string;\r\n dir?: string;\r\n [attr: string]: string | undefined;\r\n}\r\n\r\nexport interface BodyAttrs {\r\n class?: string;\r\n style?: string;\r\n [attr: string]: string | undefined;\r\n}\r\n\r\nexport interface MetaTag {\r\n name?: string;\r\n property?: string;\r\n httpEquiv?: string;\r\n charset?: string;\r\n content?: string;\r\n [attr: string]: string | undefined;\r\n}\r\n\r\nexport interface LinkTag {\r\n rel?: string;\r\n href?: string;\r\n type?: string;\r\n media?: string;\r\n as?: string;\r\n crossOrigin?: string;\r\n integrity?: string;\r\n hrefLang?: string;\r\n sizes?: string;\r\n [attr: string]: string | undefined;\r\n}\r\n\r\nexport interface ScriptTag {\r\n src?: string;\r\n content?: string;\r\n type?: string;\r\n defer?: boolean;\r\n async?: boolean;\r\n crossOrigin?: string;\r\n integrity?: string;\r\n noModule?: boolean;\r\n /**\r\n * Where to inject the script in the document.\r\n * 'head' (default) \u2014 placed inside <head>, inside the <!--n-head--> block.\r\n * 'body' \u2014 placed at the very end of <body>, inside the\r\n * <!--n-body-scripts--> block, just before </body>.\r\n */\r\n position?: 'head' | 'body';\r\n}\r\n\r\nexport interface StyleTag {\r\n content?: string;\r\n media?: string;\r\n}\r\n\r\nexport interface HtmlStore {\r\n /** Collected in render order; resolved in reverse so the page title wins. */\r\n titleOps: TitleValue[];\r\n /** Attributes merged onto <html>; last write wins per attribute. */\r\n htmlAttrs: HtmlAttrs;\r\n /** Attributes merged onto <body>; last write wins per attribute. */\r\n bodyAttrs: BodyAttrs;\r\n /** Accumulated in render order: layouts first, page last. */\r\n meta: MetaTag[];\r\n link: LinkTag[];\r\n script: ScriptTag[];\r\n style: StyleTag[];\r\n}\r\n\r\n// \u2500\u2500\u2500 GlobalThis storage \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Well-known Symbol used to share the store across duplicate module copies. */\r\nconst KEY = Symbol.for('__nukejs_html_store__');\r\n\r\nconst getGlobal = (): HtmlStore | null => (globalThis as any)[KEY] ?? null;\r\nconst setGlobal = (store: HtmlStore | null): void => { (globalThis as any)[KEY] = store; };\r\n\r\nfunction emptyStore(): HtmlStore {\r\n return {\r\n titleOps: [],\r\n htmlAttrs: {},\r\n bodyAttrs: {},\r\n meta: [],\r\n link: [],\r\n script: [],\r\n style: [],\r\n };\r\n}\r\n\r\n// \u2500\u2500\u2500 Public API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Runs `fn` inside a fresh HTML store and returns the collected values.\r\n *\r\n * Usage in SSR:\r\n * ```ts\r\n * const store = await runWithHtmlStore(async () => {\r\n * appHtml = await renderElementToHtml(element, ctx);\r\n * });\r\n * // store.titleOps, store.meta, etc. are now populated\r\n * ```\r\n */\r\nexport async function runWithHtmlStore(fn: () => Promise<void>): Promise<HtmlStore> {\r\n setGlobal(emptyStore());\r\n try {\r\n await fn();\r\n return { ...(getGlobal() ?? emptyStore()) } as HtmlStore;\r\n } finally {\r\n // Always clear the store, even if rendering throws, to prevent leakage\r\n // into the next request on the same event-loop tick.\r\n setGlobal(null);\r\n }\r\n}\r\n\r\n/**\r\n * Returns the current request's store, or `undefined` if called outside of\r\n * a `runWithHtmlStore` context (e.g. in the browser or in a test).\r\n */\r\nexport function getHtmlStore(): HtmlStore | undefined {\r\n return getGlobal() ?? undefined;\r\n}\r\n\r\n/**\r\n * Resolves the final page title from a list of title operations.\r\n *\r\n * Operations are walked in *reverse* so the page's value is the starting\r\n * point and layout template functions wrap it outward:\r\n *\r\n * ```\r\n * ops = [ (p) => `${p} | Acme`, 'About' ] \u2190 layout pushed first, page last\r\n * Walk in reverse:\r\n * i=1: op = 'About' \u2192 title = 'About'\r\n * i=0: op = (p) => \u2026 \u2192 title = 'About | Acme'\r\n * ```\r\n *\r\n * @param fallback Used when ops is empty (e.g. a page that didn't call useHtml).\r\n */\r\nexport function resolveTitle(ops: TitleValue[], fallback = ''): string {\r\n let title = fallback;\r\n for (let i = ops.length - 1; i >= 0; i--) {\r\n const op = ops[i];\r\n title = typeof op === 'string' ? op : op(title);\r\n }\r\n return title;\r\n}"],
|
|
5
|
+
"mappings": "AA6GA,MAAM,MAAM,uBAAO,IAAI,uBAAuB;AAE9C,MAAM,YAAY,MAAyB,WAAmB,GAAG,KAAK;AACtE,MAAM,YAAY,CAAC,UAAkC;AAAE,EAAC,WAAmB,GAAG,IAAI;AAAO;AAEzF,SAAS,aAAwB;AAC/B,SAAO;AAAA,IACL,UAAW,CAAC;AAAA,IACZ,WAAW,CAAC;AAAA,IACZ,WAAW,CAAC;AAAA,IACZ,MAAW,CAAC;AAAA,IACZ,MAAW,CAAC;AAAA,IACZ,QAAW,CAAC;AAAA,IACZ,OAAW,CAAC;AAAA,EACd;AACF;AAeA,eAAsB,iBAAiB,IAA6C;AAClF,YAAU,WAAW,CAAC;AACtB,MAAI;AACF,UAAM,GAAG;AACT,WAAO,EAAE,GAAI,UAAU,KAAK,WAAW,EAAG;AAAA,EAC5C,UAAE;AAGA,cAAU,IAAI;AAAA,EAChB;AACF;AAMO,SAAS,eAAsC;AACpD,SAAO,UAAU,KAAK;AACxB;AAiBO,SAAS,aAAa,KAAmB,WAAW,IAAY;AACrE,MAAI,QAAQ;AACZ,WAAS,IAAI,IAAI,SAAS,GAAG,KAAK,GAAG,KAAK;AACxC,UAAM,KAAK,IAAI,CAAC;AAChB,YAAQ,OAAO,OAAO,WAAW,KAAK,GAAG,KAAK;AAAA,EAChD;AACA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/http-server.d.ts
CHANGED
|
@@ -86,14 +86,7 @@ export declare function matchApiPrefix(url: string, apiPrefixes: ApiPrefixInfo[]
|
|
|
86
86
|
interface ApiHandlerOptions {
|
|
87
87
|
apiPrefixes: ApiPrefixInfo[];
|
|
88
88
|
port: number;
|
|
89
|
+
isDev: boolean;
|
|
89
90
|
}
|
|
90
|
-
|
|
91
|
-
* Creates the main API request dispatcher. The returned function:
|
|
92
|
-
*
|
|
93
|
-
* 1. Finds the matching prefix for the URL.
|
|
94
|
-
* 2. Resolves the exact handler file (direct file path, index, or dynamic route).
|
|
95
|
-
* 3. Dynamically imports the module (always fresh in dev thanks to file: URLs).
|
|
96
|
-
* 4. Calls the method-specific export (GET, POST, …) or `default`.
|
|
97
|
-
*/
|
|
98
|
-
export declare function createApiHandler({ apiPrefixes, port }: ApiHandlerOptions): (url: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
91
|
+
export declare function createApiHandler({ apiPrefixes, port, isDev }: ApiHandlerOptions): (url: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
99
92
|
export {};
|
package/dist/http-server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import { pathToFileURL } from "url";
|
|
4
|
+
import { build } from "esbuild";
|
|
4
5
|
import { log } from "./logger.js";
|
|
5
6
|
import { matchRoute } from "./router.js";
|
|
6
7
|
function discoverApiPrefixes(serverDir) {
|
|
@@ -102,7 +103,20 @@ function matchApiPrefix(url, apiPrefixes) {
|
|
|
102
103
|
}
|
|
103
104
|
return null;
|
|
104
105
|
}
|
|
105
|
-
function
|
|
106
|
+
async function importFreshInDev(filePath) {
|
|
107
|
+
const result = await build({
|
|
108
|
+
entryPoints: [filePath],
|
|
109
|
+
bundle: true,
|
|
110
|
+
format: "esm",
|
|
111
|
+
platform: "node",
|
|
112
|
+
target: "node20",
|
|
113
|
+
packages: "external",
|
|
114
|
+
write: false
|
|
115
|
+
});
|
|
116
|
+
const dataUrl = `data:text/javascript,${encodeURIComponent(result.outputFiles[0].text)}`;
|
|
117
|
+
return await import(dataUrl);
|
|
118
|
+
}
|
|
119
|
+
function createApiHandler({ apiPrefixes, port, isDev }) {
|
|
106
120
|
return async function handleApiRoute(url, req, res) {
|
|
107
121
|
const apiRes = enhanceResponse(res);
|
|
108
122
|
const apiMatch = matchApiPrefix(url, apiPrefixes);
|
|
@@ -142,7 +156,7 @@ function createApiHandler({ apiPrefixes, port }) {
|
|
|
142
156
|
apiReq.body = await parseBody(req);
|
|
143
157
|
apiReq.params = params;
|
|
144
158
|
apiReq.query = parseQuery(url, port);
|
|
145
|
-
const apiModule = await import(pathToFileURL(filePath).href);
|
|
159
|
+
const apiModule = isDev ? await importFreshInDev(filePath) : await import(pathToFileURL(filePath).href);
|
|
146
160
|
const handler = apiModule[method] ?? apiModule.default;
|
|
147
161
|
if (!handler) {
|
|
148
162
|
apiRes.json({ error: `Method ${method} not allowed` }, 405);
|
package/dist/http-server.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/http-server.ts"],
|
|
4
|
-
"sourcesContent": ["/**\r\n * http-server.ts \u2014 API Route Dispatcher\r\n *\r\n * Handles discovery and dispatch of API routes inside `serverDir`.\r\n *\r\n * Directory conventions (mirrors Next.js):\r\n * server/\r\n * users/ \u2192 prefix /users (directory)\r\n * index.ts \u2192 GET /users (method exports: GET, POST, \u2026)\r\n * [id].ts \u2192 GET /users/:id\r\n * auth.ts \u2192 prefix /auth (top-level file)\r\n * index.ts \u2192 prefix / (root handler)\r\n *\r\n * Route handler exports:\r\n * export function GET(req, res) { \u2026 }\r\n * export function POST(req, res) { \u2026 }\r\n * export default function(req, res) { \u2026 } // matches any method\r\n *\r\n * Request augmentation:\r\n * req.body \u2014 parsed JSON or raw string (10 MB limit)\r\n * req.params \u2014 dynamic route segments (e.g. { id: '42' })\r\n * req.query \u2014 URL search params\r\n *\r\n * Response augmentation:\r\n * res.json(data, status?) \u2014 JSON response shorthand\r\n * res.status(code) \u2014 sets statusCode, returns res for chaining\r\n */\r\n\r\nimport path from 'path';\r\nimport fs from 'fs';\r\nimport { pathToFileURL } from 'url';\r\nimport type { IncomingMessage, ServerResponse } from 'http';\r\nimport { log } from './logger';\r\nimport { matchRoute } from './router';\r\n\r\n// \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Describes a single API prefix discovered in serverDir. */\r\nexport interface ApiPrefixInfo {\r\n /** URL prefix this entry handles (e.g. '/users', ''). */\r\n prefix: string;\r\n /** Directory to scan for route files. */\r\n directory: string;\r\n /** Set when the prefix comes from a top-level file (not a directory). */\r\n filePath?: string;\r\n}\r\n\r\n/** Node's IncomingMessage with parsed body, params, and query. */\r\nexport interface ApiRequest extends IncomingMessage {\r\n params?: Record<string, string | string[]>;\r\n query?: Record<string, string>;\r\n body?: any;\r\n}\r\n\r\n/** Node's ServerResponse with json() and status() convenience methods. */\r\nexport interface ApiResponse extends ServerResponse {\r\n json: (data: any, status?: number) => void;\r\n status: (code: number) => ApiResponse;\r\n}\r\n\r\ntype HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS';\r\ntype ApiHandler = (req: ApiRequest, res: ApiResponse) => void | Promise<void>;\r\n\r\ninterface ApiModule {\r\n default?: ApiHandler;\r\n GET?: ApiHandler;\r\n POST?: ApiHandler;\r\n PUT?: ApiHandler;\r\n DELETE?: ApiHandler;\r\n PATCH?: ApiHandler;\r\n OPTIONS?: ApiHandler;\r\n}\r\n\r\n// \u2500\u2500\u2500 Route discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Scans `serverDir` and returns one ApiPrefixInfo per directory, top-level\r\n * file, and root index.ts. Directories are returned before same-stem files\r\n * so `/a/b` routes resolve to the directory tree before any flat file.\r\n *\r\n * Called at startup and again whenever the server directory changes (in dev).\r\n */\r\nexport function discoverApiPrefixes(serverDir: string): ApiPrefixInfo[] {\r\n if (!fs.existsSync(serverDir)) {\r\n log.warn('Server directory not found:', serverDir);\r\n return [];\r\n }\r\n\r\n const entries = fs.readdirSync(serverDir, { withFileTypes: true });\r\n const prefixes: ApiPrefixInfo[] = [];\r\n\r\n // Directories first (higher specificity than same-stem files).\r\n for (const e of entries) {\r\n if (e.isDirectory()) {\r\n prefixes.push({ prefix: `/${e.name}`, directory: path.join(serverDir, e.name) });\r\n }\r\n }\r\n\r\n // Top-level .ts/.tsx files (excluding index which is handled separately below).\r\n for (const e of entries) {\r\n if (\r\n e.isFile() &&\r\n (e.name.endsWith('.ts') || e.name.endsWith('.tsx')) &&\r\n e.name !== 'index.ts' &&\r\n e.name !== 'index.tsx'\r\n ) {\r\n const stem = e.name.replace(/\\.tsx?$/, '');\r\n prefixes.push({\r\n prefix: `/${stem}`,\r\n directory: serverDir,\r\n filePath: path.join(serverDir, e.name),\r\n });\r\n }\r\n }\r\n\r\n // index.ts/tsx at the root of serverDir handles unmatched paths (prefix '').\r\n if (fs.existsSync(path.join(serverDir, 'index.ts'))) {\r\n prefixes.push({ prefix: '', directory: serverDir });\r\n }\r\n\r\n return prefixes;\r\n}\r\n\r\n// \u2500\u2500\u2500 Body parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB\r\n\r\n/**\r\n * Buffers the request body and returns:\r\n * - Parsed JSON object if Content-Type is application/json.\r\n * - Raw string otherwise.\r\n *\r\n * Rejects with an error if the body exceeds MAX_BODY_BYTES to prevent\r\n * memory exhaustion attacks. Deletes __proto__ and constructor from parsed\r\n * JSON objects to guard against prototype pollution.\r\n */\r\nexport async function parseBody(req: IncomingMessage): Promise<any> {\r\n return new Promise((resolve, reject) => {\r\n let body = '';\r\n let bytes = 0;\r\n\r\n req.on('data', chunk => {\r\n bytes += chunk.length;\r\n if (bytes > MAX_BODY_BYTES) {\r\n req.destroy();\r\n return reject(new Error('Request body too large'));\r\n }\r\n body += chunk.toString();\r\n });\r\n\r\n req.on('end', () => {\r\n try {\r\n if (body && req.headers['content-type']?.includes('application/json')) {\r\n const parsed = JSON.parse(body);\r\n // Guard against prototype pollution via __proto__ / constructor.\r\n if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {\r\n delete parsed.__proto__;\r\n delete parsed.constructor;\r\n }\r\n resolve(parsed);\r\n } else {\r\n resolve(body);\r\n }\r\n } catch (err) {\r\n reject(err);\r\n }\r\n });\r\n\r\n req.on('error', reject);\r\n });\r\n}\r\n\r\n// \u2500\u2500\u2500 Query parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Extracts URL search params into a plain string map. */\r\nexport function parseQuery(url: string, port: number): Record<string, string> {\r\n const query: Record<string, string> = {};\r\n new URL(url, `http://localhost:${port}`)\r\n .searchParams\r\n .forEach((v, k) => { query[k] = v; });\r\n return query;\r\n}\r\n\r\n// \u2500\u2500\u2500 Response enhancement \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Adds `json()` and `status()` convenience methods to a raw ServerResponse,\r\n * mirroring the Express API surface that most API handlers expect.\r\n */\r\nexport function enhanceResponse(res: ServerResponse): ApiResponse {\r\n const apiRes = res as ApiResponse;\r\n apiRes.json = function (data, statusCode = 200) {\r\n this.statusCode = statusCode;\r\n this.setHeader('Content-Type', 'application/json');\r\n this.end(JSON.stringify(data));\r\n };\r\n apiRes.status = function (code) {\r\n this.statusCode = code;\r\n return this;\r\n };\r\n return apiRes;\r\n}\r\n\r\n/** Responds to an OPTIONS preflight with permissive CORS headers. */\r\nfunction respondOptions(res: ApiResponse): void {\r\n res.setHeader('Access-Control-Allow-Origin', '*');\r\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');\r\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type');\r\n res.statusCode = 204;\r\n res.end();\r\n}\r\n\r\n// \u2500\u2500\u2500 Prefix matching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Finds the first ApiPrefixInfo whose prefix is a prefix of `url`.\r\n *\r\n * The empty-string prefix ('') acts as a catch-all and only matches when no\r\n * other prefix claims the URL.\r\n *\r\n * Returns `null` when no prefix matches (request should fall through to SSR).\r\n */\r\nexport function matchApiPrefix(\r\n url: string,\r\n apiPrefixes: ApiPrefixInfo[],\r\n): { prefix: ApiPrefixInfo; apiPath: string } | null {\r\n for (const prefix of apiPrefixes) {\r\n if (prefix.prefix === '') {\r\n // Empty prefix \u2014 only match if no other prefix has claimed this URL.\r\n const claimedByOther = apiPrefixes.some(\r\n p => p.prefix !== '' && url.startsWith(p.prefix),\r\n );\r\n if (!claimedByOther) return { prefix, apiPath: url || '/' };\r\n } else if (url.startsWith(prefix.prefix)) {\r\n return { prefix, apiPath: url.slice(prefix.prefix.length) || '/' };\r\n }\r\n }\r\n return null;\r\n}\r\n\r\n// \u2500\u2500\u2500 Request handler factory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\ninterface ApiHandlerOptions {\r\n apiPrefixes: ApiPrefixInfo[];\r\n port: number;\r\n}\r\n\r\n/**\r\n * Creates the main API request dispatcher. The returned function:\r\n *\r\n * 1. Finds the matching prefix for the URL.\r\n * 2. Resolves the exact handler file (direct file path, index, or dynamic route).\r\n * 3. Dynamically imports the module (always fresh in dev thanks to file: URLs).\r\n * 4. Calls the method-specific export (GET, POST, \u2026) or `default`.\r\n */\r\nexport function createApiHandler({ apiPrefixes, port }: ApiHandlerOptions) {\r\n return async function handleApiRoute(\r\n url: string,\r\n req: IncomingMessage,\r\n res: ServerResponse,\r\n ): Promise<void> {\r\n const apiRes = enhanceResponse(res);\r\n const apiMatch = matchApiPrefix(url, apiPrefixes);\r\n\r\n if (!apiMatch) {\r\n apiRes.json({ error: 'API endpoint not found' }, 404);\r\n return;\r\n }\r\n\r\n const { prefix, apiPath } = apiMatch;\r\n let filePath: string | null = null;\r\n let params: Record<string, string | string[]> = {};\r\n\r\n // 1. Direct file match (top-level file prefix, e.g. server/auth.ts \u2192 /auth).\r\n if (prefix.filePath) {\r\n filePath = prefix.filePath;\r\n }\r\n\r\n // 2. Root index.ts (prefix === '' and path === '/').\r\n if (!filePath && prefix.prefix === '' && apiPath === '/') {\r\n const indexPath = path.join(prefix.directory, 'index.ts');\r\n if (fs.existsSync(indexPath)) filePath = indexPath;\r\n }\r\n\r\n // 3. Dynamic route matching inside the prefix directory.\r\n if (!filePath) {\r\n const routeMatch =\r\n matchRoute(apiPath, prefix.directory, '.ts') ??\r\n matchRoute(apiPath, prefix.directory, '.tsx');\r\n if (routeMatch) { filePath = routeMatch.filePath; params = routeMatch.params; }\r\n }\r\n\r\n if (!filePath) {\r\n apiRes.json({ error: 'API endpoint not found' }, 404);\r\n return;\r\n }\r\n\r\n try {\r\n const method = (req.method || 'GET').toUpperCase() as HttpMethod;\r\n log.verbose(`API ${method} ${url} -> ${path.relative(process.cwd(), filePath)}`);\r\n\r\n // OPTIONS preflight \u2014 respond immediately with CORS headers.\r\n if (method === 'OPTIONS') { respondOptions(apiRes); return; }\r\n\r\n // Augment the request object with parsed body, params, and query.\r\n const apiReq = req as ApiRequest;\r\n apiReq.body = await parseBody(req);\r\n apiReq.params = params;\r\n apiReq.query = parseQuery(url, port);\r\n\r\n // Dynamic import is always fresh because each path includes a unique\r\n // file:// URL \u2014 Node will not serve a stale cached module.\r\n const apiModule: ApiModule = await import(pathToFileURL(filePath).href);\r\n const handler = apiModule[method] ?? apiModule.default;\r\n\r\n if (!handler) {\r\n apiRes.json({ error: `Method ${method} not allowed` }, 405);\r\n return;\r\n }\r\n\r\n await handler(apiReq, apiRes);\r\n } catch (error) {\r\n log.error('API Error:', error);\r\n apiRes.json({ error: 'Internal server error' }, 500);\r\n }\r\n };\r\n}\r\n"],
|
|
5
|
-
"mappings": "AA4BA,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,SAAS,qBAAqB;
|
|
4
|
+
"sourcesContent": ["/**\r\n * http-server.ts \u2014 API Route Dispatcher\r\n *\r\n * Handles discovery and dispatch of API routes inside `serverDir`.\r\n *\r\n * Directory conventions (mirrors Next.js):\r\n * server/\r\n * users/ \u2192 prefix /users (directory)\r\n * index.ts \u2192 GET /users (method exports: GET, POST, \u2026)\r\n * [id].ts \u2192 GET /users/:id\r\n * auth.ts \u2192 prefix /auth (top-level file)\r\n * index.ts \u2192 prefix / (root handler)\r\n *\r\n * Route handler exports:\r\n * export function GET(req, res) { \u2026 }\r\n * export function POST(req, res) { \u2026 }\r\n * export default function(req, res) { \u2026 } // matches any method\r\n *\r\n * Request augmentation:\r\n * req.body \u2014 parsed JSON or raw string (10 MB limit)\r\n * req.params \u2014 dynamic route segments (e.g. { id: '42' })\r\n * req.query \u2014 URL search params\r\n *\r\n * Response augmentation:\r\n * res.json(data, status?) \u2014 JSON response shorthand\r\n * res.status(code) \u2014 sets statusCode, returns res for chaining\r\n */\r\n\r\nimport path from 'path';\r\nimport fs from 'fs';\r\nimport { pathToFileURL } from 'url';\r\nimport { build } from 'esbuild';\r\nimport type { IncomingMessage, ServerResponse } from 'http';\r\nimport { log } from './logger';\r\nimport { matchRoute } from './router';\r\n\r\n// \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Describes a single API prefix discovered in serverDir. */\r\nexport interface ApiPrefixInfo {\r\n /** URL prefix this entry handles (e.g. '/users', ''). */\r\n prefix: string;\r\n /** Directory to scan for route files. */\r\n directory: string;\r\n /** Set when the prefix comes from a top-level file (not a directory). */\r\n filePath?: string;\r\n}\r\n\r\n/** Node's IncomingMessage with parsed body, params, and query. */\r\nexport interface ApiRequest extends IncomingMessage {\r\n params?: Record<string, string | string[]>;\r\n query?: Record<string, string>;\r\n body?: any;\r\n}\r\n\r\n/** Node's ServerResponse with json() and status() convenience methods. */\r\nexport interface ApiResponse extends ServerResponse {\r\n json: (data: any, status?: number) => void;\r\n status: (code: number) => ApiResponse;\r\n}\r\n\r\ntype HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS';\r\ntype ApiHandler = (req: ApiRequest, res: ApiResponse) => void | Promise<void>;\r\n\r\ninterface ApiModule {\r\n default?: ApiHandler;\r\n GET?: ApiHandler;\r\n POST?: ApiHandler;\r\n PUT?: ApiHandler;\r\n DELETE?: ApiHandler;\r\n PATCH?: ApiHandler;\r\n OPTIONS?: ApiHandler;\r\n}\r\n\r\n// \u2500\u2500\u2500 Route discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Scans `serverDir` and returns one ApiPrefixInfo per directory, top-level\r\n * file, and root index.ts. Directories are returned before same-stem files\r\n * so `/a/b` routes resolve to the directory tree before any flat file.\r\n *\r\n * Called at startup and again whenever the server directory changes (in dev).\r\n */\r\nexport function discoverApiPrefixes(serverDir: string): ApiPrefixInfo[] {\r\n if (!fs.existsSync(serverDir)) {\r\n log.warn('Server directory not found:', serverDir);\r\n return [];\r\n }\r\n\r\n const entries = fs.readdirSync(serverDir, { withFileTypes: true });\r\n const prefixes: ApiPrefixInfo[] = [];\r\n\r\n // Directories first (higher specificity than same-stem files).\r\n for (const e of entries) {\r\n if (e.isDirectory()) {\r\n prefixes.push({ prefix: `/${e.name}`, directory: path.join(serverDir, e.name) });\r\n }\r\n }\r\n\r\n // Top-level .ts/.tsx files (excluding index which is handled separately below).\r\n for (const e of entries) {\r\n if (\r\n e.isFile() &&\r\n (e.name.endsWith('.ts') || e.name.endsWith('.tsx')) &&\r\n e.name !== 'index.ts' &&\r\n e.name !== 'index.tsx'\r\n ) {\r\n const stem = e.name.replace(/\\.tsx?$/, '');\r\n prefixes.push({\r\n prefix: `/${stem}`,\r\n directory: serverDir,\r\n filePath: path.join(serverDir, e.name),\r\n });\r\n }\r\n }\r\n\r\n // index.ts/tsx at the root of serverDir handles unmatched paths (prefix '').\r\n if (fs.existsSync(path.join(serverDir, 'index.ts'))) {\r\n prefixes.push({ prefix: '', directory: serverDir });\r\n }\r\n\r\n return prefixes;\r\n}\r\n\r\n// \u2500\u2500\u2500 Body parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB\r\n\r\n/**\r\n * Buffers the request body and returns:\r\n * - Parsed JSON object if Content-Type is application/json.\r\n * - Raw string otherwise.\r\n *\r\n * Rejects with an error if the body exceeds MAX_BODY_BYTES to prevent\r\n * memory exhaustion attacks. Deletes __proto__ and constructor from parsed\r\n * JSON objects to guard against prototype pollution.\r\n */\r\nexport async function parseBody(req: IncomingMessage): Promise<any> {\r\n return new Promise((resolve, reject) => {\r\n let body = '';\r\n let bytes = 0;\r\n\r\n req.on('data', chunk => {\r\n bytes += chunk.length;\r\n if (bytes > MAX_BODY_BYTES) {\r\n req.destroy();\r\n return reject(new Error('Request body too large'));\r\n }\r\n body += chunk.toString();\r\n });\r\n\r\n req.on('end', () => {\r\n try {\r\n if (body && req.headers['content-type']?.includes('application/json')) {\r\n const parsed = JSON.parse(body);\r\n // Guard against prototype pollution via __proto__ / constructor.\r\n if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {\r\n delete parsed.__proto__;\r\n delete parsed.constructor;\r\n }\r\n resolve(parsed);\r\n } else {\r\n resolve(body);\r\n }\r\n } catch (err) {\r\n reject(err);\r\n }\r\n });\r\n\r\n req.on('error', reject);\r\n });\r\n}\r\n\r\n// \u2500\u2500\u2500 Query parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Extracts URL search params into a plain string map. */\r\nexport function parseQuery(url: string, port: number): Record<string, string> {\r\n const query: Record<string, string> = {};\r\n new URL(url, `http://localhost:${port}`)\r\n .searchParams\r\n .forEach((v, k) => { query[k] = v; });\r\n return query;\r\n}\r\n\r\n// \u2500\u2500\u2500 Response enhancement \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Adds `json()` and `status()` convenience methods to a raw ServerResponse,\r\n * mirroring the Express API surface that most API handlers expect.\r\n */\r\nexport function enhanceResponse(res: ServerResponse): ApiResponse {\r\n const apiRes = res as ApiResponse;\r\n apiRes.json = function (data, statusCode = 200) {\r\n this.statusCode = statusCode;\r\n this.setHeader('Content-Type', 'application/json');\r\n this.end(JSON.stringify(data));\r\n };\r\n apiRes.status = function (code) {\r\n this.statusCode = code;\r\n return this;\r\n };\r\n return apiRes;\r\n}\r\n\r\n/** Responds to an OPTIONS preflight with permissive CORS headers. */\r\nfunction respondOptions(res: ApiResponse): void {\r\n res.setHeader('Access-Control-Allow-Origin', '*');\r\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');\r\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type');\r\n res.statusCode = 204;\r\n res.end();\r\n}\r\n\r\n// \u2500\u2500\u2500 Prefix matching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Finds the first ApiPrefixInfo whose prefix is a prefix of `url`.\r\n *\r\n * The empty-string prefix ('') acts as a catch-all and only matches when no\r\n * other prefix claims the URL.\r\n *\r\n * Returns `null` when no prefix matches (request should fall through to SSR).\r\n */\r\nexport function matchApiPrefix(\r\n url: string,\r\n apiPrefixes: ApiPrefixInfo[],\r\n): { prefix: ApiPrefixInfo; apiPath: string } | null {\r\n for (const prefix of apiPrefixes) {\r\n if (prefix.prefix === '') {\r\n // Empty prefix \u2014 only match if no other prefix has claimed this URL.\r\n const claimedByOther = apiPrefixes.some(\r\n p => p.prefix !== '' && url.startsWith(p.prefix),\r\n );\r\n if (!claimedByOther) return { prefix, apiPath: url || '/' };\r\n } else if (url.startsWith(prefix.prefix)) {\r\n return { prefix, apiPath: url.slice(prefix.prefix.length) || '/' };\r\n }\r\n }\r\n return null;\r\n}\r\n\r\n// \u2500\u2500\u2500 Dev-mode handler importer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n// \u2500\u2500\u2500 Dev-mode fresh importer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Bundles `filePath` + all its local transitive imports into a single ESM\r\n * string via esbuild on every call, writes it to a unique temp file, imports\r\n * it, then deletes the temp file.\r\n *\r\n * Why esbuild bundling and not ?t=timestamp:\r\n * Node's ESM cache is keyed on the full URL string. ?t=timestamp busts only\r\n * the entry file \u2014 every `import './other'` inside it resolves from cache as\r\n * normal. Bundling inlines all local deps so there are no transitive cache\r\n * hits at all. npm packages are kept external (packages: 'external') because\r\n * they live in node_modules and never change between requests.\r\n */\r\nasync function importFreshInDev(filePath: string): Promise<ApiModule> {\r\n const result = await build({\r\n entryPoints: [filePath],\r\n bundle: true,\r\n format: 'esm',\r\n platform: 'node',\r\n target: 'node20',\r\n packages: 'external',\r\n write: false,\r\n });\r\n\r\n const dataUrl = `data:text/javascript,${encodeURIComponent(result.outputFiles[0].text)}`;\r\n return await import(dataUrl) as ApiModule;\r\n}\r\n\r\n// \u2500\u2500\u2500 Request handler factory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\ninterface ApiHandlerOptions {\r\n apiPrefixes: ApiPrefixInfo[];\r\n port: number;\r\n isDev: boolean;\r\n}\r\n\r\nexport function createApiHandler({ apiPrefixes, port, isDev }: ApiHandlerOptions) {\r\n return async function handleApiRoute(\r\n url: string,\r\n req: IncomingMessage,\r\n res: ServerResponse,\r\n ): Promise<void> {\r\n const apiRes = enhanceResponse(res);\r\n const apiMatch = matchApiPrefix(url, apiPrefixes);\r\n\r\n if (!apiMatch) {\r\n apiRes.json({ error: 'API endpoint not found' }, 404);\r\n return;\r\n }\r\n\r\n const { prefix, apiPath } = apiMatch;\r\n let filePath: string | null = null;\r\n let params: Record<string, string | string[]> = {};\r\n\r\n // 1. Direct file match (top-level file prefix, e.g. server/auth.ts \u2192 /auth).\r\n if (prefix.filePath) {\r\n filePath = prefix.filePath;\r\n }\r\n\r\n // 2. Root index.ts (prefix === '' and path === '/').\r\n if (!filePath && prefix.prefix === '' && apiPath === '/') {\r\n const indexPath = path.join(prefix.directory, 'index.ts');\r\n if (fs.existsSync(indexPath)) filePath = indexPath;\r\n }\r\n\r\n // 3. Dynamic route matching inside the prefix directory.\r\n if (!filePath) {\r\n const routeMatch =\r\n matchRoute(apiPath, prefix.directory, '.ts') ??\r\n matchRoute(apiPath, prefix.directory, '.tsx');\r\n if (routeMatch) { filePath = routeMatch.filePath; params = routeMatch.params; }\r\n }\r\n\r\n if (!filePath) {\r\n apiRes.json({ error: 'API endpoint not found' }, 404);\r\n return;\r\n }\r\n\r\n try {\r\n const method = (req.method || 'GET').toUpperCase() as HttpMethod;\r\n log.verbose(`API ${method} ${url} -> ${path.relative(process.cwd(), filePath)}`);\r\n\r\n // OPTIONS preflight \u2014 respond immediately with CORS headers.\r\n if (method === 'OPTIONS') { respondOptions(apiRes); return; }\r\n\r\n // Augment the request object with parsed body, params, and query.\r\n const apiReq = req as ApiRequest;\r\n apiReq.body = await parseBody(req);\r\n apiReq.params = params;\r\n apiReq.query = parseQuery(url, port);\r\n\r\n const apiModule: ApiModule = isDev\r\n ? await importFreshInDev(filePath)\r\n : await import(pathToFileURL(filePath).href);\r\n const handler = apiModule[method] ?? apiModule.default;\r\n\r\n if (!handler) {\r\n apiRes.json({ error: `Method ${method} not allowed` }, 405);\r\n return;\r\n }\r\n\r\n await handler(apiReq, apiRes);\r\n } catch (error) {\r\n log.error('API Error:', error);\r\n apiRes.json({ error: 'Internal server error' }, 500);\r\n }\r\n };\r\n}"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { useHtml } from './use-html';
|
|
2
2
|
export type { HtmlOptions, TitleValue, HtmlAttrs, BodyAttrs, MetaTag, LinkTag, ScriptTag, StyleTag, } from './use-html';
|
|
3
|
-
export { default as useRouter } from './
|
|
4
|
-
export { default as Link } from './
|
|
3
|
+
export { default as useRouter } from './use-router';
|
|
4
|
+
export { default as Link } from './Link';
|
|
5
5
|
export { setupLocationChangeMonitor, initRuntime } from './bundle';
|
|
6
6
|
export type { RuntimeData } from './bundle';
|
|
7
7
|
export { escapeHtml } from './utils';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useHtml } from "./use-html.js";
|
|
2
|
-
import { default as default2 } from "./
|
|
3
|
-
import { default as default3 } from "./
|
|
2
|
+
import { default as default2 } from "./use-router.js";
|
|
3
|
+
import { default as default3 } from "./Link.js";
|
|
4
4
|
import { setupLocationChangeMonitor, initRuntime } from "./bundle.js";
|
|
5
5
|
import { escapeHtml } from "./utils.js";
|
|
6
6
|
import { ansi, c, log, setDebugLevel, getDebugLevel } from "./logger.js";
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.ts"],
|
|
4
|
-
"sourcesContent": ["// \u2500\u2500\u2500 Client-side hooks & components \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\nexport { useHtml } from './use-html';\r\nexport type {\r\n HtmlOptions,\r\n TitleValue,\r\n HtmlAttrs,\r\n BodyAttrs,\r\n MetaTag,\r\n LinkTag,\r\n ScriptTag,\r\n StyleTag,\r\n} from './use-html';\r\n\r\nexport { default as useRouter } from './
|
|
4
|
+
"sourcesContent": ["// \u2500\u2500\u2500 Client-side hooks & components \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\nexport { useHtml } from './use-html';\r\nexport type {\r\n HtmlOptions,\r\n TitleValue,\r\n HtmlAttrs,\r\n BodyAttrs,\r\n MetaTag,\r\n LinkTag,\r\n ScriptTag,\r\n StyleTag,\r\n} from './use-html';\r\n\r\nexport { default as useRouter } from './use-router';\r\n\r\nexport { default as Link } from './Link';\r\n\r\n// \u2500\u2500\u2500 Client runtime (browser bootstrap) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\nexport { setupLocationChangeMonitor, initRuntime } from './bundle';\r\nexport type { RuntimeData } from './bundle';\r\n\r\n// \u2500\u2500\u2500 Shared utilities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\nexport { escapeHtml } from './utils';\r\n\r\nexport { ansi, c, log, setDebugLevel, getDebugLevel } from './logger';\r\nexport type { DebugLevel } from './logger';"],
|
|
5
5
|
"mappings": "AACA,SAAS,eAAe;AAYxB,SAAoB,WAAXA,gBAA4B;AAErC,SAAoB,WAAXA,gBAAuB;AAGhC,SAAS,4BAA4B,mBAAmB;AAIxD,SAAS,kBAAkB;AAE3B,SAAS,MAAM,GAAG,KAAK,eAAe,qBAAqB;",
|
|
6
6
|
"names": ["default"]
|
|
7
7
|
}
|
package/dist/renderer.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
-
import fs from "fs";
|
|
3
2
|
import { createElement, Fragment } from "react";
|
|
4
3
|
import { renderToString } from "react-dom/server";
|
|
5
4
|
import { log } from "./logger.js";
|
|
@@ -52,9 +51,7 @@ async function renderFunctionComponent(type, props, ctx) {
|
|
|
52
51
|
for (const [id, filePath] of ctx.registry.entries()) {
|
|
53
52
|
const info = componentCache.get(filePath);
|
|
54
53
|
if (!info?.isClientComponent) continue;
|
|
55
|
-
|
|
56
|
-
const match = content.match(/export\s+default\s+(?:function\s+)?(\w+)/);
|
|
57
|
-
if (!match?.[1] || type.name !== match[1]) continue;
|
|
54
|
+
if (!info.exportedName || type.name !== info.exportedName) continue;
|
|
58
55
|
try {
|
|
59
56
|
ctx.hydrated.add(id);
|
|
60
57
|
const serializedProps = serializePropsForHydration(props, ctx.registry);
|
|
@@ -111,9 +108,7 @@ function serializeReactElement(element, registry) {
|
|
|
111
108
|
for (const [id, filePath] of registry.entries()) {
|
|
112
109
|
const info = componentCache.get(filePath);
|
|
113
110
|
if (!info?.isClientComponent) continue;
|
|
114
|
-
|
|
115
|
-
const match = content.match(/export\s+default\s+(?:function\s+)?(\w+)/);
|
|
116
|
-
if (match?.[1] && type.name === match[1]) {
|
|
111
|
+
if (info.exportedName && type.name === info.exportedName) {
|
|
117
112
|
return {
|
|
118
113
|
__re: "client",
|
|
119
114
|
componentId: id,
|
package/dist/renderer.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/renderer.ts"],
|
|
4
|
-
"sourcesContent": ["/**\r\n * renderer.ts \u2014 Dev-Mode Async SSR Renderer\r\n *\r\n * Implements a recursive async renderer used in `nuke dev` to convert a React\r\n * element tree into an HTML string. It is a lighter alternative to\r\n * react-dom/server.renderToString that:\r\n *\r\n * - Supports async server components (components that return Promises).\r\n * - Emits <span data-hydrate-id=\"\u2026\"> markers for \"use client\" boundaries\r\n * instead of trying to render them server-side without their browser APIs.\r\n * - Serializes props passed to client components into the marker's\r\n * data-hydrate-props attribute so the browser can reconstruct them.\r\n *\r\n * In production (nuke build), the equivalent renderer is inlined into each\r\n * page's standalone bundle by build-common.ts (makePageAdapterSource).\r\n *\r\n * RenderContext:\r\n * registry \u2014 Map<id, filePath> of all client components for this page.\r\n * Populated by component-analyzer.ts before rendering.\r\n * hydrated \u2014 Set<id> populated during render; used to tell the browser\r\n * which components to hydrate on this specific request.\r\n * skipClientSSR \u2014 When true (HMR request), client components emit an empty\r\n * marker instead of running renderToString (faster dev reload).\r\n */\r\n\r\nimport path from 'path';\r\nimport fs from 'fs';\r\nimport { createElement, Fragment } from 'react';\r\nimport { renderToString } from 'react-dom/server';\r\nimport { log } from './logger';\r\nimport { getComponentCache } from './component-analyzer';\r\nimport { escapeHtml } from './utils';\r\n\r\n// \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nexport interface RenderContext {\r\n /** id \u2192 absolute file path for every client component reachable from this page. */\r\n registry: Map<string, string>;\r\n /** Populated during render: IDs of client components actually encountered. */\r\n hydrated: Set<string>;\r\n /** When true, skip renderToString for client components (faster HMR). */\r\n skipClientSSR?: boolean;\r\n}\r\n\r\n// \u2500\u2500\u2500 Top-level renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Recursively renders a React element (or primitive) to an HTML string.\r\n *\r\n * Handles:\r\n * null / undefined / boolean \u2192 ''\r\n * string / number \u2192 HTML-escaped text\r\n * array \u2192 rendered in parallel, joined\r\n * Fragment \u2192 renders children directly\r\n * HTML element string \u2192 renderHtmlElement()\r\n * function component \u2192 renderFunctionComponent()\r\n */\r\nexport async function renderElementToHtml(\r\n element: any,\r\n ctx: RenderContext,\r\n): Promise<string> {\r\n if (element === null || element === undefined || typeof element === 'boolean') return '';\r\n if (typeof element === 'string' || typeof element === 'number')\r\n return escapeHtml(String(element));\r\n\r\n if (Array.isArray(element)) {\r\n const parts = await Promise.all(element.map(e => renderElementToHtml(e, ctx)));\r\n return parts.join('');\r\n }\r\n\r\n if (!element.type) return '';\r\n\r\n const { type, props } = element;\r\n\r\n if (type === Fragment) return renderElementToHtml(props.children, ctx);\r\n if (typeof type === 'string') return renderHtmlElement(type, props, ctx);\r\n if (typeof type === 'function') return renderFunctionComponent(type, props, ctx);\r\n\r\n return '';\r\n}\r\n\r\n// \u2500\u2500\u2500 HTML element renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Renders a native HTML element (e.g. `<div className=\"foo\">`).\r\n *\r\n * Attribute conversion:\r\n * className \u2192 class\r\n * htmlFor \u2192 for\r\n * style \u2192 converted from camelCase object to CSS string\r\n * boolean \u2192 omitted when false, rendered as name-only attribute when true\r\n * dangerouslySetInnerHTML \u2192 inner HTML set verbatim (no escaping)\r\n *\r\n * Void elements (img, br, input, etc.) are self-closed.\r\n */\r\nasync function renderHtmlElement(\r\n type: string,\r\n props: any,\r\n ctx: RenderContext,\r\n): Promise<string> {\r\n const { children, ...attributes } = (props || {}) as Record<string, any>;\r\n\r\n const attrs = Object.entries(attributes as Record<string, any>)\r\n .map(([key, value]) => {\r\n // React prop name \u2192 HTML attribute name.\r\n if (key === 'className') key = 'class';\r\n if (key === 'htmlFor') key = 'for';\r\n if (key === 'dangerouslySetInnerHTML') return ''; // handled separately below\r\n\r\n if (typeof value === 'boolean') return value ? key : '';\r\n\r\n // camelCase style object \u2192 \"prop:value;\u2026\" CSS string.\r\n if (key === 'style' && typeof value === 'object') {\r\n const styleStr = Object.entries(value)\r\n .map(([k, v]) => {\r\n const prop = k.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`);\r\n // Strip characters that could break out of the attribute value.\r\n const safeVal = String(v).replace(/[<>\"'`\\\\]/g, '');\r\n return `${prop}:${safeVal}`;\r\n })\r\n .join(';');\r\n return `style=\"${styleStr}\"`;\r\n }\r\n\r\n return `${key}=\"${escapeHtml(String(value))}\"`;\r\n })\r\n .filter(Boolean)\r\n .join(' ');\r\n\r\n const attrStr = attrs ? ` ${attrs}` : '';\r\n\r\n if (props?.dangerouslySetInnerHTML) {\r\n return `<${type}${attrStr}>${props.dangerouslySetInnerHTML.__html}</${type}>`;\r\n }\r\n\r\n // Void elements cannot have children.\r\n if (['img', 'br', 'hr', 'input', 'meta', 'link'].includes(type)) {\r\n return `<${type}${attrStr} />`;\r\n }\r\n\r\n const childrenHtml = children ? await renderElementToHtml(children, ctx) : '';\r\n return `<${type}${attrStr}>${childrenHtml}</${type}>`;\r\n}\r\n\r\n// \u2500\u2500\u2500 Function component renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Renders a function (or class) component.\r\n *\r\n * Client boundary detection:\r\n * The component cache maps file paths to ComponentInfo. We match the\r\n * component's function name against the default export of each registered\r\n * client file to determine whether this component is a client boundary.\r\n *\r\n * If it is, we emit a hydration marker and optionally run renderToString\r\n * to produce the initial HTML inside the marker (skipped when skipClientSSR\r\n * is set, e.g. during HMR navigation).\r\n *\r\n * Class components:\r\n * Instantiated via `new type(props)` and their render() method called.\r\n *\r\n * Async components:\r\n * Awaited if the return value is a Promise (standard server component pattern).\r\n */\r\nasync function renderFunctionComponent(\r\n type: Function,\r\n props: any,\r\n ctx: RenderContext,\r\n): Promise<string> {\r\n const componentCache = getComponentCache();\r\n\r\n // Check whether this component function is a registered client component.\r\n for (const [id, filePath] of ctx.registry.entries()) {\r\n const info = componentCache.get(filePath);\r\n if (!info?.isClientComponent) continue;\r\n\r\n // Match by default export function name.\r\n const content = fs.readFileSync(filePath, 'utf-8');\r\n const match = content.match(/export\\s+default\\s+(?:function\\s+)?(\\w+)/);\r\n if (!match?.[1] || type.name !== match[1]) continue;\r\n\r\n // This is a client boundary.\r\n try {\r\n ctx.hydrated.add(id);\r\n const serializedProps = serializePropsForHydration(props, ctx.registry);\r\n log.verbose(`Client component rendered for hydration: ${id} (${path.basename(filePath)})`);\r\n\r\n // Optionally SSR the component so the initial HTML is meaningful\r\n // (improves perceived performance and avoids layout shift).\r\n const html = ctx.skipClientSSR\r\n ? ''\r\n : renderToString(createElement(type as React.ComponentType<any>, props));\r\n\r\n return `<span data-hydrate-id=\"${id}\" data-hydrate-props=\"${escapeHtml(\r\n JSON.stringify(serializedProps),\r\n )}\">${html}</span>`;\r\n } catch (err) {\r\n log.error('Error rendering client component:', err);\r\n return `<div style=\"color:red\">Error rendering client component: ${escapeHtml(String(err))}</div>`;\r\n }\r\n }\r\n\r\n // Server component \u2014 call it and recurse into the result.\r\n try {\r\n const result = type(props);\r\n const resolved = result?.then ? await result : result;\r\n return renderElementToHtml(resolved, ctx);\r\n } catch (err) {\r\n log.error('Error rendering component:', err);\r\n return `<div style=\"color:red\">Error rendering component: ${escapeHtml(String(err))}</div>`;\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Prop serialization \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Converts props into a JSON-serializable form for the data-hydrate-props\r\n * attribute. React elements inside props are serialized to a tagged object\r\n * format ({ __re: 'html'|'client', \u2026 }) that the browser's reconstructElement\r\n * function (in bundle.ts) can turn back into real React elements.\r\n *\r\n * Functions are dropped (cannot be serialized).\r\n */\r\nfunction serializePropsForHydration(\r\n props: any,\r\n registry: Map<string, string>,\r\n): any {\r\n if (!props || typeof props !== 'object') return props;\r\n const out: any = {};\r\n for (const [key, value] of Object.entries(props as Record<string, any>)) {\r\n const s = serializeValue(value, registry);\r\n if (s !== undefined) out[key] = s;\r\n }\r\n return out;\r\n}\r\n\r\nfunction serializeValue(value: any, registry: Map<string, string>): any {\r\n if (value === null || value === undefined) return value;\r\n if (typeof value === 'function') return undefined; // not serializable\r\n if (typeof value !== 'object') return value;\r\n if (Array.isArray(value))\r\n return value.map(v => serializeValue(v, registry)).filter(v => v !== undefined);\r\n if ((value as any).$$typeof)\r\n return serializeReactElement(value, registry);\r\n\r\n const out: any = {};\r\n for (const [k, v] of Object.entries(value as Record<string, any>)) {\r\n const s = serializeValue(v, registry);\r\n if (s !== undefined) out[k] = s;\r\n }\r\n return out;\r\n}\r\n\r\n/**\r\n * Serializes a React element to its wire format:\r\n * Native element \u2192 { __re: 'html', tag, props }\r\n * Client component \u2192 { __re: 'client', componentId, props }\r\n * Server component \u2192 undefined (cannot be serialized)\r\n */\r\nfunction serializeReactElement(element: any, registry: Map<string, string>): any {\r\n const { type, props } = element;\r\n\r\n if (typeof type === 'string') {\r\n return { __re: 'html', tag: type, props: serializePropsForHydration(props, registry) };\r\n }\r\n\r\n if (typeof type === 'function') {\r\n const componentCache = getComponentCache();\r\n for (const [id, filePath] of registry.entries()) {\r\n const info = componentCache.get(filePath);\r\n if (!info?.isClientComponent) continue;\r\n const content = fs.readFileSync(filePath, 'utf-8');\r\n const match = content.match(/export\\s+default\\s+(?:function\\s+)?(\\w+)/);\r\n if (match?.[1] && type.name === match[1]) {\r\n return {\r\n __re: 'client',\r\n componentId: id,\r\n props: serializePropsForHydration(props, registry),\r\n };\r\n }\r\n }\r\n }\r\n\r\n return undefined; // Server component \u2014 not serializable\r\n}\r\n"],
|
|
5
|
-
"mappings": "AAyBA,OAAO,UAAU;AACjB,
|
|
4
|
+
"sourcesContent": ["/**\r\n * renderer.ts \u2014 Dev-Mode Async SSR Renderer\r\n *\r\n * Implements a recursive async renderer used in `nuke dev` to convert a React\r\n * element tree into an HTML string. It is a lighter alternative to\r\n * react-dom/server.renderToString that:\r\n *\r\n * - Supports async server components (components that return Promises).\r\n * - Emits <span data-hydrate-id=\"\u2026\"> markers for \"use client\" boundaries\r\n * instead of trying to render them server-side without their browser APIs.\r\n * - Serializes props passed to client components into the marker's\r\n * data-hydrate-props attribute so the browser can reconstruct them.\r\n *\r\n * In production (nuke build), the equivalent renderer is inlined into each\r\n * page's standalone bundle by build-common.ts (makePageAdapterSource).\r\n *\r\n * RenderContext:\r\n * registry \u2014 Map<id, filePath> of all client components for this page.\r\n * Populated by component-analyzer.ts before rendering.\r\n * hydrated \u2014 Set<id> populated during render; used to tell the browser\r\n * which components to hydrate on this specific request.\r\n * skipClientSSR \u2014 When true (HMR request), client components emit an empty\r\n * marker instead of running renderToString (faster dev reload).\r\n */\r\n\r\nimport path from 'path';\r\nimport { createElement, Fragment } from 'react';\r\nimport { renderToString } from 'react-dom/server';\r\nimport { log } from './logger';\r\nimport { getComponentCache } from './component-analyzer';\r\nimport { escapeHtml } from './utils';\r\n\r\n// \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nexport interface RenderContext {\r\n /** id \u2192 absolute file path for every client component reachable from this page. */\r\n registry: Map<string, string>;\r\n /** Populated during render: IDs of client components actually encountered. */\r\n hydrated: Set<string>;\r\n /** When true, skip renderToString for client components (faster HMR). */\r\n skipClientSSR?: boolean;\r\n}\r\n\r\n// \u2500\u2500\u2500 Top-level renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Recursively renders a React element (or primitive) to an HTML string.\r\n *\r\n * Handles:\r\n * null / undefined / boolean \u2192 ''\r\n * string / number \u2192 HTML-escaped text\r\n * array \u2192 rendered in parallel, joined\r\n * Fragment \u2192 renders children directly\r\n * HTML element string \u2192 renderHtmlElement()\r\n * function component \u2192 renderFunctionComponent()\r\n */\r\nexport async function renderElementToHtml(\r\n element: any,\r\n ctx: RenderContext,\r\n): Promise<string> {\r\n if (element === null || element === undefined || typeof element === 'boolean') return '';\r\n if (typeof element === 'string' || typeof element === 'number')\r\n return escapeHtml(String(element));\r\n\r\n if (Array.isArray(element)) {\r\n const parts = await Promise.all(element.map(e => renderElementToHtml(e, ctx)));\r\n return parts.join('');\r\n }\r\n\r\n if (!element.type) return '';\r\n\r\n const { type, props } = element;\r\n\r\n if (type === Fragment) return renderElementToHtml(props.children, ctx);\r\n if (typeof type === 'string') return renderHtmlElement(type, props, ctx);\r\n if (typeof type === 'function') return renderFunctionComponent(type, props, ctx);\r\n\r\n return '';\r\n}\r\n\r\n// \u2500\u2500\u2500 HTML element renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Renders a native HTML element (e.g. `<div className=\"foo\">`).\r\n *\r\n * Attribute conversion:\r\n * className \u2192 class\r\n * htmlFor \u2192 for\r\n * style \u2192 converted from camelCase object to CSS string\r\n * boolean \u2192 omitted when false, rendered as name-only attribute when true\r\n * dangerouslySetInnerHTML \u2192 inner HTML set verbatim (no escaping)\r\n *\r\n * Void elements (img, br, input, etc.) are self-closed.\r\n */\r\nasync function renderHtmlElement(\r\n type: string,\r\n props: any,\r\n ctx: RenderContext,\r\n): Promise<string> {\r\n const { children, ...attributes } = (props || {}) as Record<string, any>;\r\n\r\n const attrs = Object.entries(attributes as Record<string, any>)\r\n .map(([key, value]) => {\r\n // React prop name \u2192 HTML attribute name.\r\n if (key === 'className') key = 'class';\r\n if (key === 'htmlFor') key = 'for';\r\n if (key === 'dangerouslySetInnerHTML') return ''; // handled separately below\r\n\r\n if (typeof value === 'boolean') return value ? key : '';\r\n\r\n // camelCase style object \u2192 \"prop:value;\u2026\" CSS string.\r\n if (key === 'style' && typeof value === 'object') {\r\n const styleStr = Object.entries(value)\r\n .map(([k, v]) => {\r\n const prop = k.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`);\r\n // Strip characters that could break out of the attribute value.\r\n const safeVal = String(v).replace(/[<>\"'`\\\\]/g, '');\r\n return `${prop}:${safeVal}`;\r\n })\r\n .join(';');\r\n return `style=\"${styleStr}\"`;\r\n }\r\n\r\n return `${key}=\"${escapeHtml(String(value))}\"`;\r\n })\r\n .filter(Boolean)\r\n .join(' ');\r\n\r\n const attrStr = attrs ? ` ${attrs}` : '';\r\n\r\n if (props?.dangerouslySetInnerHTML) {\r\n return `<${type}${attrStr}>${props.dangerouslySetInnerHTML.__html}</${type}>`;\r\n }\r\n\r\n // Void elements cannot have children.\r\n if (['img', 'br', 'hr', 'input', 'meta', 'link'].includes(type)) {\r\n return `<${type}${attrStr} />`;\r\n }\r\n\r\n const childrenHtml = children ? await renderElementToHtml(children, ctx) : '';\r\n return `<${type}${attrStr}>${childrenHtml}</${type}>`;\r\n}\r\n\r\n// \u2500\u2500\u2500 Function component renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Renders a function (or class) component.\r\n *\r\n * Client boundary detection:\r\n * The component cache maps file paths to ComponentInfo. We match the\r\n * component's function name against the default export of each registered\r\n * client file to determine whether this component is a client boundary.\r\n *\r\n * If it is, we emit a hydration marker and optionally run renderToString\r\n * to produce the initial HTML inside the marker (skipped when skipClientSSR\r\n * is set, e.g. during HMR navigation).\r\n *\r\n * Class components:\r\n * Instantiated via `new type(props)` and their render() method called.\r\n *\r\n * Async components:\r\n * Awaited if the return value is a Promise (standard server component pattern).\r\n */\r\nasync function renderFunctionComponent(\r\n type: Function,\r\n props: any,\r\n ctx: RenderContext,\r\n): Promise<string> {\r\n const componentCache = getComponentCache();\r\n\r\n // Check whether this component function is a registered client component.\r\n for (const [id, filePath] of ctx.registry.entries()) {\r\n const info = componentCache.get(filePath);\r\n if (!info?.isClientComponent) continue;\r\n\r\n // Match by default export function name (cached \u2014 handles both source and\r\n // esbuild-compiled formats; see component-analyzer.getExportedDefaultName).\r\n if (!info.exportedName || type.name !== info.exportedName) continue;\r\n\r\n // This is a client boundary.\r\n try {\r\n ctx.hydrated.add(id);\r\n const serializedProps = serializePropsForHydration(props, ctx.registry);\r\n log.verbose(`Client component rendered for hydration: ${id} (${path.basename(filePath)})`);\r\n\r\n // Optionally SSR the component so the initial HTML is meaningful\r\n // (improves perceived performance and avoids layout shift).\r\n const html = ctx.skipClientSSR\r\n ? ''\r\n : renderToString(createElement(type as React.ComponentType<any>, props));\r\n\r\n return `<span data-hydrate-id=\"${id}\" data-hydrate-props=\"${escapeHtml(\r\n JSON.stringify(serializedProps),\r\n )}\">${html}</span>`;\r\n } catch (err) {\r\n log.error('Error rendering client component:', err);\r\n return `<div style=\"color:red\">Error rendering client component: ${escapeHtml(String(err))}</div>`;\r\n }\r\n }\r\n\r\n // Server component \u2014 call it and recurse into the result.\r\n try {\r\n const result = type(props);\r\n const resolved = result?.then ? await result : result;\r\n return renderElementToHtml(resolved, ctx);\r\n } catch (err) {\r\n log.error('Error rendering component:', err);\r\n return `<div style=\"color:red\">Error rendering component: ${escapeHtml(String(err))}</div>`;\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Prop serialization \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Converts props into a JSON-serializable form for the data-hydrate-props\r\n * attribute. React elements inside props are serialized to a tagged object\r\n * format ({ __re: 'html'|'client', \u2026 }) that the browser's reconstructElement\r\n * function (in bundle.ts) can turn back into real React elements.\r\n *\r\n * Functions are dropped (cannot be serialized).\r\n */\r\nfunction serializePropsForHydration(\r\n props: any,\r\n registry: Map<string, string>,\r\n): any {\r\n if (!props || typeof props !== 'object') return props;\r\n const out: any = {};\r\n for (const [key, value] of Object.entries(props as Record<string, any>)) {\r\n const s = serializeValue(value, registry);\r\n if (s !== undefined) out[key] = s;\r\n }\r\n return out;\r\n}\r\n\r\nfunction serializeValue(value: any, registry: Map<string, string>): any {\r\n if (value === null || value === undefined) return value;\r\n if (typeof value === 'function') return undefined; // not serializable\r\n if (typeof value !== 'object') return value;\r\n if (Array.isArray(value))\r\n return value.map(v => serializeValue(v, registry)).filter(v => v !== undefined);\r\n if ((value as any).$$typeof)\r\n return serializeReactElement(value, registry);\r\n\r\n const out: any = {};\r\n for (const [k, v] of Object.entries(value as Record<string, any>)) {\r\n const s = serializeValue(v, registry);\r\n if (s !== undefined) out[k] = s;\r\n }\r\n return out;\r\n}\r\n\r\n/**\r\n * Serializes a React element to its wire format:\r\n * Native element \u2192 { __re: 'html', tag, props }\r\n * Client component \u2192 { __re: 'client', componentId, props }\r\n * Server component \u2192 undefined (cannot be serialized)\r\n */\r\nfunction serializeReactElement(element: any, registry: Map<string, string>): any {\r\n const { type, props } = element;\r\n\r\n if (typeof type === 'string') {\r\n return { __re: 'html', tag: type, props: serializePropsForHydration(props, registry) };\r\n }\r\n\r\n if (typeof type === 'function') {\r\n const componentCache = getComponentCache();\r\n for (const [id, filePath] of registry.entries()) {\r\n const info = componentCache.get(filePath);\r\n if (!info?.isClientComponent) continue;\r\n if (info.exportedName && type.name === info.exportedName) {\r\n return {\r\n __re: 'client',\r\n componentId: id,\r\n props: serializePropsForHydration(props, registry),\r\n };\r\n }\r\n }\r\n }\r\n\r\n return undefined; // Server component \u2014 not serializable\r\n}"],
|
|
5
|
+
"mappings": "AAyBA,OAAO,UAAU;AACjB,SAAS,eAAe,gBAAgB;AACxC,SAAS,sBAAsB;AAC/B,SAAS,WAAW;AACpB,SAAS,yBAAyB;AAClC,SAAS,kBAAkB;AA0B3B,eAAsB,oBACpB,SACA,KACiB;AACjB,MAAI,YAAY,QAAQ,YAAY,UAAa,OAAO,YAAY,UAAW,QAAO;AACtF,MAAI,OAAO,YAAY,YAAY,OAAO,YAAY;AACpD,WAAO,WAAW,OAAO,OAAO,CAAC;AAEnC,MAAI,MAAM,QAAQ,OAAO,GAAG;AAC1B,UAAM,QAAQ,MAAM,QAAQ,IAAI,QAAQ,IAAI,OAAK,oBAAoB,GAAG,GAAG,CAAC,CAAC;AAC7E,WAAO,MAAM,KAAK,EAAE;AAAA,EACtB;AAEA,MAAI,CAAC,QAAQ,KAAM,QAAO;AAE1B,QAAM,EAAE,MAAM,MAAM,IAAI;AAExB,MAAI,SAAS,SAAuB,QAAO,oBAAoB,MAAM,UAAU,GAAG;AAClF,MAAI,OAAO,SAAS,SAAgB,QAAO,kBAAkB,MAAM,OAAO,GAAG;AAC7E,MAAI,OAAO,SAAS,WAAgB,QAAO,wBAAwB,MAAM,OAAO,GAAG;AAEnF,SAAO;AACT;AAgBA,eAAe,kBACb,MACA,OACA,KACiB;AACjB,QAAM,EAAE,UAAU,GAAG,WAAW,IAAK,SAAS,CAAC;AAE/C,QAAM,QAAQ,OAAO,QAAQ,UAAiC,EAC3D,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM;AAErB,QAAI,QAAQ,YAA0B,OAAM;AAC5C,QAAI,QAAQ,UAA0B,OAAM;AAC5C,QAAI,QAAQ,0BAA2B,QAAO;AAE9C,QAAI,OAAO,UAAU,UAAW,QAAO,QAAQ,MAAM;AAGrD,QAAI,QAAQ,WAAW,OAAO,UAAU,UAAU;AAChD,YAAM,WAAW,OAAO,QAAQ,KAAK,EAClC,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM;AACf,cAAM,OAAU,EAAE,QAAQ,UAAU,OAAK,IAAI,EAAE,YAAY,CAAC,EAAE;AAE9D,cAAM,UAAU,OAAO,CAAC,EAAE,QAAQ,cAAc,EAAE;AAClD,eAAO,GAAG,IAAI,IAAI,OAAO;AAAA,MAC3B,CAAC,EACA,KAAK,GAAG;AACX,aAAO,UAAU,QAAQ;AAAA,IAC3B;AAEA,WAAO,GAAG,GAAG,KAAK,WAAW,OAAO,KAAK,CAAC,CAAC;AAAA,EAC7C,CAAC,EACA,OAAO,OAAO,EACd,KAAK,GAAG;AAEX,QAAM,UAAU,QAAQ,IAAI,KAAK,KAAK;AAEtC,MAAI,OAAO,yBAAyB;AAClC,WAAO,IAAI,IAAI,GAAG,OAAO,IAAI,MAAM,wBAAwB,MAAM,KAAK,IAAI;AAAA,EAC5E;AAGA,MAAI,CAAC,OAAO,MAAM,MAAM,SAAS,QAAQ,MAAM,EAAE,SAAS,IAAI,GAAG;AAC/D,WAAO,IAAI,IAAI,GAAG,OAAO;AAAA,EAC3B;AAEA,QAAM,eAAe,WAAW,MAAM,oBAAoB,UAAU,GAAG,IAAI;AAC3E,SAAO,IAAI,IAAI,GAAG,OAAO,IAAI,YAAY,KAAK,IAAI;AACpD;AAsBA,eAAe,wBACb,MACA,OACA,KACiB;AACjB,QAAM,iBAAiB,kBAAkB;AAGzC,aAAW,CAAC,IAAI,QAAQ,KAAK,IAAI,SAAS,QAAQ,GAAG;AACnD,UAAM,OAAO,eAAe,IAAI,QAAQ;AACxC,QAAI,CAAC,MAAM,kBAAmB;AAI9B,QAAI,CAAC,KAAK,gBAAgB,KAAK,SAAS,KAAK,aAAc;AAG3D,QAAI;AACF,UAAI,SAAS,IAAI,EAAE;AACnB,YAAM,kBAAkB,2BAA2B,OAAO,IAAI,QAAQ;AACtE,UAAI,QAAQ,4CAA4C,EAAE,KAAK,KAAK,SAAS,QAAQ,CAAC,GAAG;AAIzF,YAAM,OAAO,IAAI,gBACb,KACA,eAAe,cAAc,MAAkC,KAAK,CAAC;AAEzE,aAAO,0BAA0B,EAAE,yBAAyB;AAAA,QAC1D,KAAK,UAAU,eAAe;AAAA,MAChC,CAAC,KAAK,IAAI;AAAA,IACZ,SAAS,KAAK;AACZ,UAAI,MAAM,qCAAqC,GAAG;AAClD,aAAO,4DAA4D,WAAW,OAAO,GAAG,CAAC,CAAC;AAAA,IAC5F;AAAA,EACF;AAGA,MAAI;AACF,UAAM,SAAW,KAAK,KAAK;AAC3B,UAAM,WAAW,QAAQ,OAAO,MAAM,SAAS;AAC/C,WAAO,oBAAoB,UAAU,GAAG;AAAA,EAC1C,SAAS,KAAK;AACZ,QAAI,MAAM,8BAA8B,GAAG;AAC3C,WAAO,qDAAqD,WAAW,OAAO,GAAG,CAAC,CAAC;AAAA,EACrF;AACF;AAYA,SAAS,2BACP,OACA,UACK;AACL,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,QAAM,MAAW,CAAC;AAClB,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAA4B,GAAG;AACvE,UAAM,IAAI,eAAe,OAAO,QAAQ;AACxC,QAAI,MAAM,OAAW,KAAI,GAAG,IAAI;AAAA,EAClC;AACA,SAAO;AACT;AAEA,SAAS,eAAe,OAAY,UAAoC;AACtE,MAAI,UAAU,QAAQ,UAAU,OAAY,QAAO;AACnD,MAAI,OAAO,UAAU,WAAuB,QAAO;AACnD,MAAI,OAAO,UAAU,SAAuB,QAAO;AACnD,MAAI,MAAM,QAAQ,KAAK;AACrB,WAAO,MAAM,IAAI,OAAK,eAAe,GAAG,QAAQ,CAAC,EAAE,OAAO,OAAK,MAAM,MAAS;AAChF,MAAK,MAAc;AACjB,WAAO,sBAAsB,OAAO,QAAQ;AAE9C,QAAM,MAAW,CAAC;AAClB,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,KAA4B,GAAG;AACjE,UAAM,IAAI,eAAe,GAAG,QAAQ;AACpC,QAAI,MAAM,OAAW,KAAI,CAAC,IAAI;AAAA,EAChC;AACA,SAAO;AACT;AAQA,SAAS,sBAAsB,SAAc,UAAoC;AAC/E,QAAM,EAAE,MAAM,MAAM,IAAI;AAExB,MAAI,OAAO,SAAS,UAAU;AAC5B,WAAO,EAAE,MAAM,QAAQ,KAAK,MAAM,OAAO,2BAA2B,OAAO,QAAQ,EAAE;AAAA,EACvF;AAEA,MAAI,OAAO,SAAS,YAAY;AAC9B,UAAM,iBAAiB,kBAAkB;AACzC,eAAW,CAAC,IAAI,QAAQ,KAAK,SAAS,QAAQ,GAAG;AAC/C,YAAM,OAAO,eAAe,IAAI,QAAQ;AACxC,UAAI,CAAC,MAAM,kBAAmB;AAC9B,UAAI,KAAK,gBAAgB,KAAK,SAAS,KAAK,cAAc;AACxD,eAAO;AAAA,UACL,MAAa;AAAA,UACb,aAAa;AAAA,UACb,OAAa,2BAA2B,OAAO,QAAQ;AAAA,QACzD;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/router.d.ts
CHANGED
|
@@ -4,17 +4,15 @@
|
|
|
4
4
|
* Maps incoming URL paths to handler files using Next.js-compatible conventions:
|
|
5
5
|
*
|
|
6
6
|
* server/users/index.ts → /users
|
|
7
|
-
* server/users/[id].ts → /users/:id
|
|
8
|
-
* server/
|
|
9
|
-
* server/
|
|
10
|
-
* server/files/[[...path]].ts → /files or /files/* (optional catch-all)
|
|
7
|
+
* server/users/[id].ts → /users/:id (dynamic segment)
|
|
8
|
+
* server/blog/[...slug].ts → /blog/* (catch-all)
|
|
9
|
+
* server/files/[[...path]].ts → /files or /files/* (optional catch-all)
|
|
11
10
|
*
|
|
12
|
-
* Route specificity (higher
|
|
13
|
-
* static segment
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* [[...optCatchAll]] +1 (e.g. '[[...path]]')
|
|
11
|
+
* Route specificity (higher = wins over lower):
|
|
12
|
+
* static segment +4 (e.g. 'about')
|
|
13
|
+
* dynamic segment +3 (e.g. '[id]')
|
|
14
|
+
* catch-all +2 (e.g. '[...slug]')
|
|
15
|
+
* optional catch-all +1 (e.g. '[[...path]]')
|
|
18
16
|
*
|
|
19
17
|
* Path traversal protection:
|
|
20
18
|
* matchRoute() rejects URL segments that contain '..' or '.' and verifies
|
|
@@ -34,24 +32,27 @@
|
|
|
34
32
|
export declare function findAllRoutes(dir: string, baseDir?: string): string[];
|
|
35
33
|
/**
|
|
36
34
|
* Attempts to match `urlSegments` against a route that may contain dynamic
|
|
37
|
-
* segments ([param]),
|
|
38
|
-
* ([...slug]), and optional catch-alls ([[...path]]).
|
|
35
|
+
* segments ([param]), catch-alls ([...slug]), and optional catch-alls ([[...path]]).
|
|
39
36
|
*
|
|
40
37
|
* Returns the captured params on success, or null if the route does not match.
|
|
41
38
|
*
|
|
42
39
|
* Param value types:
|
|
43
|
-
* [param]
|
|
44
|
-
* [
|
|
45
|
-
* [...
|
|
46
|
-
* [[...path]] → string[] (optional, may be empty)
|
|
40
|
+
* [param] → string
|
|
41
|
+
* [...slug] → string[] (at least one segment required)
|
|
42
|
+
* [[...path]] → string[] (zero or more segments)
|
|
47
43
|
*/
|
|
48
44
|
export declare function matchDynamicRoute(urlSegments: string[], routePath: string): {
|
|
49
45
|
params: Record<string, string | string[]>;
|
|
50
46
|
} | null;
|
|
51
47
|
/**
|
|
52
48
|
* Computes a specificity score for a route path.
|
|
53
|
-
* Used to sort candidate routes so more specific routes shadow
|
|
54
|
-
*
|
|
49
|
+
* Used to sort candidate routes so more specific routes shadow catch-alls.
|
|
50
|
+
*
|
|
51
|
+
* Higher score = more specific:
|
|
52
|
+
* static segment 4
|
|
53
|
+
* [dynamic] 3
|
|
54
|
+
* [...catchAll] 2
|
|
55
|
+
* [[...optCatchAll]] 1
|
|
55
56
|
*/
|
|
56
57
|
export declare function getRouteSpecificity(routePath: string): number;
|
|
57
58
|
export interface RouteMatch {
|
|
@@ -82,7 +83,7 @@ export declare function matchRoute(urlPath: string, baseDir: string, extension?:
|
|
|
82
83
|
* app/pages/layout.tsx ← root layout
|
|
83
84
|
* app/pages/blog/layout.tsx ← blog section layout
|
|
84
85
|
*
|
|
85
|
-
*
|
|
86
|
+
* The outermost-first order matches how wrapWithLayouts() nests them:
|
|
86
87
|
* the last layout in the array is the innermost wrapper.
|
|
87
88
|
*/
|
|
88
89
|
export declare function findLayoutsForRoute(routeFilePath: string, pagesDir: string): string[];
|
package/dist/router.js
CHANGED
|
@@ -30,7 +30,11 @@ function matchDynamicRoute(urlSegments, routePath) {
|
|
|
30
30
|
}
|
|
31
31
|
const optDynamic = seg.match(/^\[\[([^.][^\]]*)\]\]$/);
|
|
32
32
|
if (optDynamic) {
|
|
33
|
-
|
|
33
|
+
if (ui < urlSegments.length) {
|
|
34
|
+
params[optDynamic[1]] = urlSegments[ui++];
|
|
35
|
+
} else {
|
|
36
|
+
params[optDynamic[1]] = "";
|
|
37
|
+
}
|
|
34
38
|
ri++;
|
|
35
39
|
continue;
|
|
36
40
|
}
|
|
@@ -68,21 +72,25 @@ function isWithinBase(baseDir, filePath) {
|
|
|
68
72
|
return Boolean(rel) && !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
69
73
|
}
|
|
70
74
|
function matchRoute(urlPath, baseDir, extension = ".tsx") {
|
|
71
|
-
const
|
|
75
|
+
const normPath = urlPath.length > 1 ? urlPath.replace(/\/+$/, "") : urlPath;
|
|
76
|
+
const rawSegments = normPath === "/" ? [] : normPath.slice(1).split("/");
|
|
72
77
|
if (rawSegments.some((s) => s === ".." || s === ".")) return null;
|
|
73
78
|
const segments = rawSegments.length === 0 ? ["index"] : rawSegments;
|
|
74
79
|
const exactPath = path.join(baseDir, ...segments) + extension;
|
|
75
|
-
|
|
80
|
+
const exactStem = path.basename(exactPath, extension);
|
|
81
|
+
if (!isWithinBase(baseDir, exactPath)) return null;
|
|
82
|
+
if (exactStem !== "layout" && fs.existsSync(exactPath)) {
|
|
76
83
|
return { filePath: exactPath, params: {}, routePattern: segments.join("/") };
|
|
77
84
|
}
|
|
78
85
|
const sortedRoutes = findAllRoutes(baseDir).sort(
|
|
79
86
|
(a, b) => getRouteSpecificity(b) - getRouteSpecificity(a)
|
|
80
87
|
);
|
|
81
88
|
for (const route of sortedRoutes) {
|
|
82
|
-
const match = matchDynamicRoute(
|
|
89
|
+
const match = matchDynamicRoute(rawSegments, route);
|
|
83
90
|
if (!match) continue;
|
|
84
91
|
const filePath = path.join(baseDir, route) + extension;
|
|
85
|
-
if (isWithinBase(baseDir, filePath)
|
|
92
|
+
if (!isWithinBase(baseDir, filePath)) continue;
|
|
93
|
+
if (fs.existsSync(filePath)) {
|
|
86
94
|
return { filePath, params: match.params, routePattern: route };
|
|
87
95
|
}
|
|
88
96
|
}
|
|
@@ -94,7 +102,7 @@ function findLayoutsForRoute(routeFilePath, pagesDir) {
|
|
|
94
102
|
if (fs.existsSync(rootLayout)) layouts.push(rootLayout);
|
|
95
103
|
const relativePath = path.relative(pagesDir, path.dirname(routeFilePath));
|
|
96
104
|
if (!relativePath || relativePath === ".") return layouts;
|
|
97
|
-
const segments = relativePath.split(path.sep).filter(
|
|
105
|
+
const segments = relativePath.split(path.sep).filter((s) => s !== ".");
|
|
98
106
|
for (let i = 1; i <= segments.length; i++) {
|
|
99
107
|
const layoutPath = path.join(pagesDir, ...segments.slice(0, i), "layout.tsx");
|
|
100
108
|
if (fs.existsSync(layoutPath)) layouts.push(layoutPath);
|
package/dist/router.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/router.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * router.ts \u2014 File-System Based URL Router\n *\n * Maps incoming URL paths to handler files using Next.js-compatible conventions:\n *\n * server/users/index.ts \u2192 /users\n * server/users/[id].ts \u2192 /users/:id (dynamic segment)\n * server/users/[[id]].ts \u2192 /users or /users/42 (optional single)\n * server/blog/[...slug].ts \u2192 /blog/* (required catch-all)\n * server/files/[[...path]].ts \u2192 /files or /files/* (optional catch-all)\n *\n * Route specificity (higher score wins):\n * static segment +5 (e.g. 'about')\n * [dynamic] +4 (e.g. '[id]')\n * [[optional]] +3 (e.g. '[[id]]')\n * [...catchAll] +2 (e.g. '[...slug]')\n * [[...optCatchAll]] +1 (e.g. '[[...path]]')\n *\n * Path traversal protection:\n * matchRoute() rejects URL segments that contain '..' or '.' and verifies\n * that the resolved file path stays inside the base directory before\n * checking whether the file exists.\n */\n\nimport path from 'path';\nimport fs from 'fs';\n\n// \u2500\u2500\u2500 Route file discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Recursively collects all routable .ts/.tsx files in `dir`, returning paths\n * relative to `baseDir` without the file extension.\n *\n * layout.tsx files are excluded \u2014 they wrap pages but are never routes\n * themselves. This mirrors the filter in collectServerPages() so dev-mode\n * route matching behaves identically to the production build.\n *\n * Example output: ['index', 'users/index', 'users/[id]', 'blog/[...slug]']\n */\nexport function findAllRoutes(dir: string, baseDir: string = dir): string[] {\n if (!fs.existsSync(dir)) return [];\n\n const routes: string[] = [];\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n routes.push(...findAllRoutes(fullPath, baseDir));\n } else if (entry.name.endsWith('.tsx') || entry.name.endsWith('.ts')) {\n const stem = entry.name.replace(/\\.(tsx|ts)$/, '');\n if (stem === 'layout') continue;\n routes.push(path.relative(baseDir, fullPath).replace(/\\.(tsx|ts)$/, ''));\n }\n }\n return routes;\n}\n\n// \u2500\u2500\u2500 Dynamic segment matching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Attempts to match `urlSegments` against a route that may contain dynamic\n * segments ([param]), optional single segments ([[param]]), catch-alls\n * ([...slug]), and optional catch-alls ([[...path]]).\n *\n * Returns the captured params on success, or null if the route does not match.\n *\n * Param value types:\n * [param] \u2192 string (required)\n * [[param]] \u2192 string (optional, '' when absent)\n * [...slug] \u2192 string[] (required, \u22651 segment)\n * [[...path]] \u2192 string[] (optional, may be empty)\n */\nexport function matchDynamicRoute(\n urlSegments: string[],\n routePath: string,\n): { params: Record<string, string | string[]> } | null {\n const routeSegments = routePath.split(path.sep);\n\n // 'index' at the end means the route handles the parent directory URL.\n if (routeSegments.at(-1) === 'index') routeSegments.pop();\n\n const params: Record<string, string | string[]> = {};\n let ri = 0; // route segment index\n let ui = 0; // URL segment index\n\n while (ri < routeSegments.length) {\n const seg = routeSegments[ri];\n\n // [[...name]] \u2014 optional catch-all: consumes zero or more remaining segments.\n const optCatchAll = seg.match(/^\\[\\[\\.\\.\\.(.+)\\]\\]$/);\n if (optCatchAll) {\n params[optCatchAll[1]] = urlSegments.slice(ui);\n return { params };\n }\n\n // [[name]] \u2014 optional single: consumes zero or one segment.\n const optDynamic = seg.match(/^\\[\\[([^.][^\\]]*)\\]\\]$/);\n if (optDynamic) {\n params[optDynamic[1]] = ui < urlSegments.length ? urlSegments[ui++] : '';\n ri++;\n continue;\n }\n\n // [...name] \u2014 required catch-all: must consume at least one segment.\n const catchAll = seg.match(/^\\[\\.\\.\\.(.+)\\]$/);\n if (catchAll) {\n const remaining = urlSegments.slice(ui);\n if (!remaining.length) return null;\n params[catchAll[1]] = remaining;\n return { params };\n }\n\n // [name] \u2014 required single: consumes exactly one segment.\n const dynamic = seg.match(/^\\[(.+)\\]$/);\n if (dynamic) {\n if (ui >= urlSegments.length) return null;\n params[dynamic[1]] = urlSegments[ui++];\n ri++;\n continue;\n }\n\n // Static \u2014 must match exactly.\n if (ui >= urlSegments.length || seg !== urlSegments[ui]) return null;\n ui++; ri++;\n }\n\n // All route segments consumed \u2014 URL must also be fully consumed.\n return ui < urlSegments.length ? null : { params };\n}\n\n// \u2500\u2500\u2500 Specificity scoring \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Computes a specificity score for a route path.\n * Used to sort candidate routes so more specific routes shadow less specific ones.\n * Higher score = more specific.\n */\nexport function getRouteSpecificity(routePath: string): number {\n return routePath.split(path.sep).reduce((score, seg) => {\n if (seg.match(/^\\[\\[\\.\\.\\.(.+)\\]\\]$/)) return score + 1; // [[...a]] optional catch-all\n if (seg.match(/^\\[\\.\\.\\.(.+)\\]$/)) return score + 2; // [...a] required catch-all\n if (seg.match(/^\\[\\[([^.][^\\]]*)\\]\\]$/)) return score + 3; // [[a]] optional single\n if (seg.match(/^\\[(.+)\\]$/)) return score + 4; // [a] required single\n return score + 5; // static segment\n }, 0);\n}\n\n// \u2500\u2500\u2500 Route match result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface RouteMatch {\n filePath: string;\n params: Record<string, string | string[]>;\n routePattern: string;\n}\n\n// \u2500\u2500\u2500 Path traversal guard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Returns true only when `filePath` is a descendant of `baseDir`.\n * Used to prevent URL path traversal attacks (e.g. /../../etc/passwd).\n */\nfunction isWithinBase(baseDir: string, filePath: string): boolean {\n const rel = path.relative(baseDir, filePath);\n return Boolean(rel) && !rel.startsWith('..') && !path.isAbsolute(rel);\n}\n\n// \u2500\u2500\u2500 Route matching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Resolves a URL path to a route file inside `baseDir`.\n *\n * Steps:\n * 1. Reject '..' or '.' path segments (path traversal guard).\n * 2. Try an exact file match (e.g. /about \u2192 baseDir/about.tsx).\n * layout.tsx is explicitly excluded from exact matching.\n * 3. Sort all discovered routes by specificity (most specific first).\n * 4. Return the first dynamic route that matches.\n *\n * @param urlPath The URL path to match (e.g. '/users/42').\n * @param baseDir Absolute path to the directory containing route files.\n * @param extension File extension to look for ('.tsx' or '.ts').\n */\nexport function matchRoute(\n urlPath: string,\n baseDir: string,\n extension = '.tsx',\n): RouteMatch | null {\n const rawSegments = urlPath === '/' ? [] : urlPath.slice(1).split('/');\n if (rawSegments.some(s => s === '..' || s === '.')) return null;\n\n const segments = rawSegments.length === 0 ? ['index'] : rawSegments;\n\n // 1. Exact match: /about \u2192 about.tsx \u2014 never resolve to a layout file.\n const exactPath = path.join(baseDir, ...segments) + extension;\n if (\n isWithinBase(baseDir, exactPath) &&\n path.basename(exactPath, extension) !== 'layout' &&\n fs.existsSync(exactPath)\n ) {\n return { filePath: exactPath, params: {}, routePattern: segments.join('/') };\n }\n\n // 2. Dynamic match \u2014 try routes sorted by specificity.\n const sortedRoutes = findAllRoutes(baseDir).sort(\n (a, b) => getRouteSpecificity(b) - getRouteSpecificity(a),\n );\n\n for (const route of sortedRoutes) {\n const match = matchDynamicRoute(segments, route);\n if (!match) continue;\n const filePath = path.join(baseDir, route) + extension;\n if (isWithinBase(baseDir, filePath) && fs.existsSync(filePath)) {\n return { filePath, params: match.params, routePattern: route };\n }\n }\n\n return null;\n}\n\n// \u2500\u2500\u2500 Layout discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Returns every layout.tsx file that wraps a given route file, in\n * outermost-first order (root layout first, nearest layout last).\n *\n * Layout chain example for app/pages/blog/[slug]/page.tsx:\n * app/pages/layout.tsx \u2190 root layout\n * app/pages/blog/layout.tsx \u2190 blog section layout\n *\n * Outermost-first order matches how wrapWithLayouts() nests them:\n * the last layout in the array is the innermost wrapper.\n */\nexport function findLayoutsForRoute(routeFilePath: string, pagesDir: string): string[] {\n const layouts: string[] = [];\n\n const rootLayout = path.join(pagesDir, 'layout.tsx');\n if (fs.existsSync(rootLayout)) layouts.push(rootLayout);\n\n const relativePath = path.relative(pagesDir, path.dirname(routeFilePath));\n if (!relativePath || relativePath === '.') return layouts;\n\n const segments = relativePath.split(path.sep).filter(Boolean);\n for (let i = 1; i <= segments.length; i++) {\n const layoutPath = path.join(pagesDir, ...segments.slice(0, i), 'layout.tsx');\n if (fs.existsSync(layoutPath)) layouts.push(layoutPath);\n }\n\n return layouts;\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/**\r\n * router.ts \u2014 File-System Based URL Router\r\n *\r\n * Maps incoming URL paths to handler files using Next.js-compatible conventions:\r\n *\r\n * server/users/index.ts \u2192 /users\r\n * server/users/[id].ts \u2192 /users/:id (dynamic segment)\r\n * server/blog/[...slug].ts \u2192 /blog/* (catch-all)\r\n * server/files/[[...path]].ts \u2192 /files or /files/* (optional catch-all)\r\n *\r\n * Route specificity (higher = wins over lower):\r\n * static segment +4 (e.g. 'about')\r\n * dynamic segment +3 (e.g. '[id]')\r\n * catch-all +2 (e.g. '[...slug]')\r\n * optional catch-all +1 (e.g. '[[...path]]')\r\n *\r\n * Path traversal protection:\r\n * matchRoute() rejects URL segments that contain '..' or '.' and verifies\r\n * that the resolved file path stays inside the base directory before\r\n * checking whether the file exists.\r\n */\r\n\r\nimport path from 'path';\r\nimport fs from 'fs';\r\n\r\n// \u2500\u2500\u2500 Route file discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Recursively collects all routable .ts/.tsx files in `dir`, returning paths\r\n * relative to `baseDir` without the file extension.\r\n *\r\n * layout.tsx files are excluded \u2014 they wrap pages but are never routes\r\n * themselves. This mirrors the filter in collectServerPages() so dev-mode\r\n * route matching behaves identically to the production build.\r\n *\r\n * Example output: ['index', 'users/index', 'users/[id]', 'blog/[...slug]']\r\n */\r\nexport function findAllRoutes(dir: string, baseDir: string = dir): string[] {\r\n if (!fs.existsSync(dir)) return [];\r\n\r\n const routes: string[] = [];\r\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\r\n const fullPath = path.join(dir, entry.name);\r\n if (entry.isDirectory()) {\r\n routes.push(...findAllRoutes(fullPath, baseDir));\r\n } else if (entry.name.endsWith('.tsx') || entry.name.endsWith('.ts')) {\r\n const stem = entry.name.replace(/\\.(tsx|ts)$/, '');\r\n if (stem === 'layout') continue;\r\n routes.push(path.relative(baseDir, fullPath).replace(/\\.(tsx|ts)$/, ''));\r\n }\r\n }\r\n return routes;\r\n}\r\n\r\n// \u2500\u2500\u2500 Dynamic segment matching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Attempts to match `urlSegments` against a route that may contain dynamic\r\n * segments ([param]), catch-alls ([...slug]), and optional catch-alls ([[...path]]).\r\n *\r\n * Returns the captured params on success, or null if the route does not match.\r\n *\r\n * Param value types:\r\n * [param] \u2192 string\r\n * [...slug] \u2192 string[] (at least one segment required)\r\n * [[...path]] \u2192 string[] (zero or more segments)\r\n */\r\nexport function matchDynamicRoute(\r\n urlSegments: string[],\r\n routePath: string,\r\n): { params: Record<string, string | string[]> } | null {\r\n const routeSegments = routePath.split(path.sep);\r\n\r\n // 'index' at the end of a route path means the route handles the parent directory URL.\r\n if (routeSegments.at(-1) === 'index') routeSegments.pop();\r\n\r\n const params: Record<string, string | string[]> = {};\r\n let ri = 0; // route segment index\r\n let ui = 0; // URL segment index\r\n\r\n while (ri < routeSegments.length) {\r\n const seg = routeSegments[ri];\r\n\r\n // [[...name]] \u2014 optional catch-all: consumes zero or more remaining URL segments.\r\n const optCatchAll = seg.match(/^\\[\\[\\.\\.\\.(.+)\\]\\]$/);\r\n if (optCatchAll) {\r\n params[optCatchAll[1]] = urlSegments.slice(ui);\r\n return { params };\r\n }\r\n\r\n // [[name]] \u2014 optional single segment: consumes zero or one URL segment.\r\n const optDynamic = seg.match(/^\\[\\[([^.][^\\]]*)\\]\\]$/);\r\n if (optDynamic) {\r\n if (ui < urlSegments.length) {\r\n params[optDynamic[1]] = urlSegments[ui++];\r\n } else {\r\n params[optDynamic[1]] = '';\r\n }\r\n ri++;\r\n continue;\r\n }\r\n\r\n // [...name] \u2014 required catch-all: must consume at least one URL segment.\r\n const catchAll = seg.match(/^\\[\\.\\.\\.(.+)\\]$/);\r\n if (catchAll) {\r\n const remaining = urlSegments.slice(ui);\r\n if (!remaining.length) return null;\r\n params[catchAll[1]] = remaining;\r\n return { params };\r\n }\r\n\r\n // [name] \u2014 single dynamic segment: consumes exactly one URL segment.\r\n const dynamic = seg.match(/^\\[(.+)\\]$/);\r\n if (dynamic) {\r\n if (ui >= urlSegments.length) return null;\r\n params[dynamic[1]] = urlSegments[ui++];\r\n ri++;\r\n continue;\r\n }\r\n\r\n // Static segment \u2014 must match exactly.\r\n if (ui >= urlSegments.length || seg !== urlSegments[ui]) return null;\r\n ui++; ri++;\r\n }\r\n\r\n // All route segments consumed \u2014 URL must be fully consumed too.\r\n return ui < urlSegments.length ? null : { params };\r\n}\r\n\r\n// \u2500\u2500\u2500 Specificity scoring \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Computes a specificity score for a route path.\r\n * Used to sort candidate routes so more specific routes shadow catch-alls.\r\n *\r\n * Higher score = more specific:\r\n * static segment 4\r\n * [dynamic] 3\r\n * [...catchAll] 2\r\n * [[...optCatchAll]] 1\r\n */\r\nexport function getRouteSpecificity(routePath: string): number {\r\n return routePath.split(path.sep).reduce((score, seg) => {\r\n if (seg.match(/^\\[\\[\\.\\.\\.(.+)\\]\\]$/)) return score + 1; // [[...a]] optional catch-all\r\n if (seg.match(/^\\[\\.\\.\\.(.+)\\]$/)) return score + 2; // [...a] required catch-all\r\n if (seg.match(/^\\[\\[([^.][^\\]]*)\\]\\]$/)) return score + 3; // [[a]] optional single\r\n if (seg.match(/^\\[(.+)\\]$/)) return score + 4; // [a] required single\r\n return score + 5; // static segment\r\n }, 0);\r\n}\r\n\r\n// \u2500\u2500\u2500 Route match result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nexport interface RouteMatch {\r\n filePath: string;\r\n params: Record<string, string | string[]>;\r\n routePattern: string;\r\n}\r\n\r\n// \u2500\u2500\u2500 Path traversal guard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Returns true only when `filePath` is a descendant of `baseDir`.\r\n * Used to prevent URL path traversal attacks (e.g. /../../etc/passwd).\r\n */\r\nfunction isWithinBase(baseDir: string, filePath: string): boolean {\r\n const rel = path.relative(baseDir, filePath);\r\n return Boolean(rel) && !rel.startsWith('..') && !path.isAbsolute(rel);\r\n}\r\n\r\n// \u2500\u2500\u2500 Route matching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Resolves a URL path to a route file inside `baseDir`.\r\n *\r\n * Steps:\r\n * 1. Reject '..' or '.' path segments (path traversal guard).\r\n * 2. Try an exact file match (e.g. /about \u2192 baseDir/about.tsx).\r\n * layout.tsx is explicitly excluded from exact matching.\r\n * 3. Sort all discovered routes by specificity (most specific first).\r\n * 4. Return the first dynamic route that matches.\r\n *\r\n * @param urlPath The URL path to match (e.g. '/users/42').\r\n * @param baseDir Absolute path to the directory containing route files.\r\n * @param extension File extension to look for ('.tsx' or '.ts').\r\n */\r\nexport function matchRoute(\r\n urlPath: string,\r\n baseDir: string,\r\n extension = '.tsx',\r\n): RouteMatch | null {\r\n // Normalise trailing slashes: /test/ \u2192 /test, but keep root / as-is.\r\n const normPath = urlPath.length > 1 ? urlPath.replace(/\\/+$/, '') : urlPath;\r\n // Split into segments, rejecting any that attempt path traversal.\r\n const rawSegments = normPath === '/' ? [] : normPath.slice(1).split('/');\r\n if (rawSegments.some(s => s === '..' || s === '.')) return null;\r\n\r\n // For the root URL, look for an index file.\r\n const segments = rawSegments.length === 0 ? ['index'] : rawSegments;\r\n\r\n // 1. Exact match: /about \u2192 about.tsx \u2014 never resolve to a layout file.\r\n const exactPath = path.join(baseDir, ...segments) + extension;\r\n const exactStem = path.basename(exactPath, extension);\r\n if (!isWithinBase(baseDir, exactPath)) return null;\r\n if (exactStem !== 'layout' && fs.existsSync(exactPath)) {\r\n return { filePath: exactPath, params: {}, routePattern: segments.join('/') };\r\n }\r\n\r\n // 2. Dynamic match \u2014 use rawSegments (not ['index']) so that [param] routes\r\n // do not accidentally match '/' by consuming the synthetic 'index' segment.\r\n const sortedRoutes = findAllRoutes(baseDir).sort(\r\n (a, b) => getRouteSpecificity(b) - getRouteSpecificity(a),\r\n );\r\n\r\n for (const route of sortedRoutes) {\r\n const match = matchDynamicRoute(rawSegments, route);\r\n if (!match) continue;\r\n const filePath = path.join(baseDir, route) + extension;\r\n if (!isWithinBase(baseDir, filePath)) continue;\r\n if (fs.existsSync(filePath)) {\r\n return { filePath, params: match.params, routePattern: route };\r\n }\r\n }\r\n\r\n return null;\r\n}\r\n\r\n// \u2500\u2500\u2500 Layout discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Returns every layout.tsx file that wraps a given route file, in\r\n * outermost-first order (root layout first, nearest layout last).\r\n *\r\n * Layout chain example for app/pages/blog/[slug]/page.tsx:\r\n * app/pages/layout.tsx \u2190 root layout\r\n * app/pages/blog/layout.tsx \u2190 blog section layout\r\n *\r\n * The outermost-first order matches how wrapWithLayouts() nests them:\r\n * the last layout in the array is the innermost wrapper.\r\n */\r\nexport function findLayoutsForRoute(routeFilePath: string, pagesDir: string): string[] {\r\n const layouts: string[] = [];\r\n\r\n // Root layout wraps everything.\r\n const rootLayout = path.join(pagesDir, 'layout.tsx');\r\n if (fs.existsSync(rootLayout)) layouts.push(rootLayout);\r\n\r\n // Walk the directory hierarchy from pagesDir to the file's parent.\r\n const relativePath = path.relative(pagesDir, path.dirname(routeFilePath));\r\n if (!relativePath || relativePath === '.') return layouts;\r\n\r\n const segments = relativePath.split(path.sep).filter(s => s !== '.');\r\n for (let i = 1; i <= segments.length; i++) {\r\n const layoutPath = path.join(pagesDir, ...segments.slice(0, i), 'layout.tsx');\r\n if (fs.existsSync(layoutPath)) layouts.push(layoutPath);\r\n }\r\n\r\n return layouts;\r\n}"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|