nukejs 0.0.11 → 0.0.13
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 +147 -0
- package/dist/Link.js +0 -1
- package/dist/app.js +0 -1
- package/dist/build-common.js +64 -14
- package/dist/build-node.js +63 -5
- package/dist/build-vercel.js +76 -9
- package/dist/builder.js +32 -4
- package/dist/bundle.js +47 -4
- package/dist/bundler.js +0 -1
- package/dist/component-analyzer.js +0 -1
- package/dist/config.js +0 -1
- package/dist/hmr-bundle.js +10 -1
- package/dist/hmr.js +7 -1
- package/dist/html-store.js +0 -1
- package/dist/http-server.js +0 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +0 -1
- package/dist/logger.js +0 -1
- package/dist/metadata.js +0 -1
- package/dist/middleware-loader.js +0 -1
- package/dist/middleware.example.js +0 -1
- package/dist/middleware.js +0 -1
- package/dist/renderer.js +3 -9
- package/dist/request-store.js +0 -1
- package/dist/router.js +0 -1
- package/dist/ssr.js +73 -16
- package/dist/use-html.js +0 -1
- package/dist/use-request.js +0 -1
- package/dist/use-router.js +0 -1
- package/dist/utils.js +0 -1
- package/package.json +1 -1
- package/dist/Link.js.map +0 -7
- package/dist/app.d.ts +0 -19
- package/dist/app.js.map +0 -7
- package/dist/build-common.d.ts +0 -178
- package/dist/build-common.js.map +0 -7
- package/dist/build-node.d.ts +0 -15
- package/dist/build-node.js.map +0 -7
- package/dist/build-vercel.d.ts +0 -19
- package/dist/build-vercel.js.map +0 -7
- package/dist/builder.d.ts +0 -11
- package/dist/builder.js.map +0 -7
- package/dist/bundle.js.map +0 -7
- package/dist/bundler.d.ts +0 -58
- package/dist/bundler.js.map +0 -7
- package/dist/component-analyzer.d.ts +0 -75
- package/dist/component-analyzer.js.map +0 -7
- package/dist/config.d.ts +0 -35
- package/dist/config.js.map +0 -7
- package/dist/hmr-bundle.d.ts +0 -25
- package/dist/hmr-bundle.js.map +0 -7
- package/dist/hmr.d.ts +0 -55
- package/dist/hmr.js.map +0 -7
- package/dist/html-store.js.map +0 -7
- package/dist/http-server.d.ts +0 -92
- package/dist/http-server.js.map +0 -7
- package/dist/index.js.map +0 -7
- package/dist/logger.js.map +0 -7
- package/dist/metadata.d.ts +0 -51
- package/dist/metadata.js.map +0 -7
- package/dist/middleware-loader.d.ts +0 -50
- package/dist/middleware-loader.js.map +0 -7
- package/dist/middleware.d.ts +0 -22
- package/dist/middleware.example.d.ts +0 -8
- package/dist/middleware.example.js.map +0 -7
- package/dist/middleware.js.map +0 -7
- package/dist/renderer.d.ts +0 -44
- package/dist/renderer.js.map +0 -7
- package/dist/request-store.js.map +0 -7
- package/dist/router.d.ts +0 -92
- package/dist/router.js.map +0 -7
- package/dist/ssr.d.ts +0 -46
- package/dist/ssr.js.map +0 -7
- package/dist/use-html.js.map +0 -7
- package/dist/use-request.js.map +0 -7
- package/dist/use-router.js.map +0 -7
- package/dist/utils.js.map +0 -7
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* middleware.ts (place in project root)
|
|
3
|
-
*
|
|
4
|
-
* Runs before every request. Inspect/modify req, add headers, or send early
|
|
5
|
-
* responses to halt further processing.
|
|
6
|
-
*/
|
|
7
|
-
import type { IncomingMessage, ServerResponse } from 'http';
|
|
8
|
-
export default function middleware(req: IncomingMessage, res: ServerResponse): Promise<void>;
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/middleware.example.ts"],
|
|
4
|
-
"sourcesContent": ["/**\r\n * middleware.ts (place in project root)\r\n *\r\n * Runs before every request. Inspect/modify req, add headers, or send early\r\n * responses to halt further processing.\r\n */\r\n\r\nimport type { IncomingMessage, ServerResponse } from 'http';\r\n\r\nexport default async function middleware(req: IncomingMessage, res: ServerResponse): Promise<void> {\r\n console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);\r\n\r\n if (req.url?.startsWith('/admin') && !isAuthenticated(req)) {\r\n res.statusCode = 401;\r\n res.setHeader('Content-Type', 'text/html');\r\n res.end('<h1>401 Unauthorized</h1><p>Please log in to access this page.</p>');\r\n return;\r\n }\r\n\r\n if (req.url === '/old-page') {\r\n req.url = '/new-page';\r\n }\r\n\r\n res.setHeader('X-Powered-By', 'nukejs-framework');\r\n res.setHeader('X-Request-Id', generateRequestId());\r\n\r\n if (req.url?.startsWith('/api/')) {\r\n res.setHeader('Access-Control-Allow-Origin', '*');\r\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');\r\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');\r\n\r\n if (req.method === 'OPTIONS') {\r\n res.statusCode = 204;\r\n res.end();\r\n return;\r\n }\r\n }\r\n\r\n const clientIp = req.socket.remoteAddress || 'unknown';\r\n if (isRateLimited(clientIp)) {\r\n res.statusCode = 429;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ error: 'Too many requests' }));\r\n return;\r\n }\r\n\r\n if (process.env.MAINTENANCE_MODE === 'true' && !req.url?.startsWith('/__')) {\r\n res.statusCode = 503;\r\n res.setHeader('Content-Type', 'text/html');\r\n res.end('<h1>503 Service Unavailable</h1><p>We are currently down for maintenance.</p>');\r\n return;\r\n }\r\n}\r\n\r\nfunction isAuthenticated(req: IncomingMessage): boolean {\r\n return req.headers.authorization === 'Bearer valid-token';\r\n}\r\n\r\nfunction generateRequestId(): string {\r\n return Math.random().toString(36).substring(2, 15);\r\n}\r\n\r\nconst rateLimitMap = new Map<string, { count: number; resetAt: number }>();\r\n\r\nfunction isRateLimited(ip: string): boolean {\r\n const now = Date.now();\r\n const limit = rateLimitMap.get(ip);\r\n\r\n if (!limit || now > limit.resetAt) {\r\n rateLimitMap.set(ip, { count: 1, resetAt: now + 60000 });\r\n return false;\r\n }\r\n\r\n limit.count++;\r\n return limit.count > 100;\r\n}\r\n"],
|
|
5
|
-
"mappings": "AASA,eAAO,WAAkC,KAAsB,KAAoC;AACjG,UAAQ,IAAI,KAAI,oBAAI,KAAK,GAAE,YAAY,CAAC,KAAK,IAAI,MAAM,IAAI,IAAI,GAAG,EAAE;AAEpE,MAAI,IAAI,KAAK,WAAW,QAAQ,KAAK,CAAC,gBAAgB,GAAG,GAAG;AAC1D,QAAI,aAAa;AACjB,QAAI,UAAU,gBAAgB,WAAW;AACzC,QAAI,IAAI,oEAAoE;AAC5E;AAAA,EACF;AAEA,MAAI,IAAI,QAAQ,aAAa;AAC3B,QAAI,MAAM;AAAA,EACZ;AAEA,MAAI,UAAU,gBAAgB,kBAAkB;AAChD,MAAI,UAAU,gBAAgB,kBAAkB,CAAC;AAEjD,MAAI,IAAI,KAAK,WAAW,OAAO,GAAG;AAChC,QAAI,UAAU,+BAA+B,GAAG;AAChD,QAAI,UAAU,gCAAgC,iCAAiC;AAC/E,QAAI,UAAU,gCAAgC,6BAA6B;AAE3E,QAAI,IAAI,WAAW,WAAW;AAC5B,UAAI,aAAa;AACjB,UAAI,IAAI;AACR;AAAA,IACF;AAAA,EACF;AAEA,QAAM,WAAW,IAAI,OAAO,iBAAiB;AAC7C,MAAI,cAAc,QAAQ,GAAG;AAC3B,QAAI,aAAa;AACjB,QAAI,UAAU,gBAAgB,kBAAkB;AAChD,QAAI,IAAI,KAAK,UAAU,EAAE,OAAO,oBAAoB,CAAC,CAAC;AACtD;AAAA,EACF;AAEA,MAAI,QAAQ,IAAI,qBAAqB,UAAU,CAAC,IAAI,KAAK,WAAW,KAAK,GAAG;AAC1E,QAAI,aAAa;AACjB,QAAI,UAAU,gBAAgB,WAAW;AACzC,QAAI,IAAI,+EAA+E;AACvF;AAAA,EACF;AACF;AAEA,SAAS,gBAAgB,KAA+B;AACtD,SAAO,IAAI,QAAQ,kBAAkB;AACvC;AAEA,SAAS,oBAA4B;AACnC,SAAO,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,UAAU,GAAG,EAAE;AACnD;AAEA,MAAM,eAAe,oBAAI,IAAgD;AAEzE,SAAS,cAAc,IAAqB;AAC1C,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,QAAQ,aAAa,IAAI,EAAE;AAEjC,MAAI,CAAC,SAAS,MAAM,MAAM,SAAS;AACjC,iBAAa,IAAI,IAAI,EAAE,OAAO,GAAG,SAAS,MAAM,IAAM,CAAC;AACvD,WAAO;AAAA,EACT;AAEA,QAAM;AACN,SAAO,MAAM,QAAQ;AACvB;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
package/dist/middleware.js.map
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/middleware.ts"],
|
|
4
|
-
"sourcesContent": ["/**\r\n * middleware.ts \u2014 Built-In NukeJS Middleware\r\n *\r\n * This is the internal middleware loaded before any user-defined middleware.\r\n * It handles three responsibilities:\r\n *\r\n * 1. Static public files (app/public/**)\r\n * Any file placed in app/public/ is served at its path relative to\r\n * that directory. E.g. app/public/favicon.ico \u2192 GET /favicon.ico.\r\n * The correct Content-Type is set automatically. Path traversal attempts\r\n * are rejected with 400.\r\n *\r\n * 2. HMR client script (/__hmr.js)\r\n * Builds and serves hmr-bundle.ts on demand. Injected into every dev\r\n * page as <script type=\"module\" src=\"/__hmr.js\">.\r\n *\r\n * 3. HMR SSE stream (/__hmr)\r\n * Long-lived Server-Sent Events connection used by the browser to receive\r\n * reload/replace/restart events when source files change.\r\n */\r\n\r\nimport { build } from 'esbuild';\r\nimport type { IncomingMessage, ServerResponse } from 'http';\r\nimport path from 'path';\r\nimport fs from 'fs';\r\nimport { fileURLToPath } from 'url';\r\nimport { hmrClients } from './hmr';\r\nimport { getMimeType } from './utils';\r\n\r\n// Cache the compiled HMR bundle Promise so esbuild only runs once per server lifetime.\r\n// Caching the Promise (not just the result) prevents a race condition where multiple\r\n// concurrent requests all see null and each kick off their own build.\r\nlet hmrBundlePromise: Promise<string> | null = null;\r\n\r\n// Absolute path to the static public directory.\r\n// Files here are served at their path relative to this directory.\r\nconst PUBLIC_DIR = path.resolve('./app/public');\r\n\r\nexport default async function middleware(\r\n req: IncomingMessage,\r\n res: ServerResponse,\r\n): Promise<void> {\r\n\r\n // \u2500\u2500 Static public files \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 // Checked first so /favicon.ico, /main.css, etc. are never accidentally\r\n // routed to the SSR or API layers.\r\n const rawUrl = req.url ?? '/';\r\n const pathname = rawUrl.split('?')[0]; // strip query string\r\n\r\n if (fs.existsSync(PUBLIC_DIR)) {\r\n // path.join handles the leading '/' in pathname naturally and normalises\r\n // any '..' segments, making it safe to use directly with a startsWith guard.\r\n // Using path.join (not path.resolve) ensures an absolute second argument\r\n // cannot silently escape PUBLIC_DIR the way path.resolve would allow.\r\n const candidate = path.join(PUBLIC_DIR, pathname);\r\n\r\n // Path traversal guard: the resolved path must be inside PUBLIC_DIR.\r\n // We normalise PUBLIC_DIR with a trailing separator so that a directory\r\n // whose name is a prefix of another cannot pass (e.g. /public2 vs /public).\r\n const publicBase = PUBLIC_DIR.endsWith(path.sep) ? PUBLIC_DIR : PUBLIC_DIR + path.sep;\r\n const safe = candidate.startsWith(publicBase) || candidate === PUBLIC_DIR;\r\n\r\n if (!safe) {\r\n res.statusCode = 400;\r\n res.end('Bad request');\r\n return;\r\n }\r\n\r\n // Serve the file if it exists at any depth inside PUBLIC_DIR.\r\n // Directories are intentionally skipped (no directory listings).\r\n if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {\r\n const ext = path.extname(candidate);\r\n res.setHeader('Content-Type', getMimeType(ext));\r\n res.end(fs.readFileSync(candidate));\r\n return;\r\n }\r\n }\r\n\r\n // \u2500\u2500 HMR client script \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 // Builds hmr-bundle.ts on demand so the browser always gets the latest version.\r\n if (rawUrl === '/__hmr.js') {\r\n if (!hmrBundlePromise) {\r\n const dir = path.dirname(fileURLToPath(import.meta.url));\r\n const entry = path.join(dir, `hmr-bundle.${dir.endsWith('dist') ? 'js' : 'ts'}`);\r\n hmrBundlePromise = build({\r\n entryPoints: [entry],\r\n write: false,\r\n format: 'esm',\r\n minify: true,\r\n bundle: true,\r\n external: ['react', 'react-dom/client', 'react/jsx-runtime'],\r\n }).then(r => r.outputFiles[0].text);\r\n }\r\n\r\n res.setHeader('Content-Type', 'application/javascript');\r\n res.end(await hmrBundlePromise);\r\n return;\r\n }\r\n\r\n // \u2500\u2500 HMR SSE stream \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 // Long-lived connection tracked in hmrClients so hmr.ts can broadcast events\r\n // to all connected browsers when a file changes.\r\n if (rawUrl === '/__hmr') {\r\n // Each full-page reload opens a new SSE connection before the old one has\r\n // fully closed. After ~6 reloads the browser's connection limit is exhausted\r\n // and it can't make any new requests \u2014 the server appears to hang.\r\n // Fix: allow up to 5 SSE connections per IP; when a 6th arrives, destroy\r\n // the oldest one from that IP to free a slot before accepting the new one.\r\n const MAX_SSE_PER_IP = 5;\r\n const remoteAddr = req.socket?.remoteAddress;\r\n if (remoteAddr) {\r\n const fromSameIp = [...hmrClients].filter(\r\n c => (c as any).socket?.remoteAddress === remoteAddr\r\n );\r\n if (fromSameIp.length >= MAX_SSE_PER_IP) {\r\n // Drop the oldest (first in insertion order).\r\n const oldest = fromSameIp[0];\r\n oldest.destroy();\r\n hmrClients.delete(oldest);\r\n }\r\n }\r\n\r\n res.setHeader('Content-Type', 'text/event-stream');\r\n res.setHeader('Cache-Control', 'no-cache');\r\n res.setHeader('Connection', 'keep-alive');\r\n res.setHeader('Access-Control-Allow-Origin', '*');\r\n res.flushHeaders();\r\n\r\n res.write('data: {\"type\":\"connected\"}\\n\\n');\r\n\r\n hmrClients.add(res);\r\n req.on('close', () => hmrClients.delete(res));\r\n return;\r\n }\r\n}"],
|
|
5
|
-
"mappings": "AAqBA,SAAS,aAAa;AAEtB,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,SAAS,mBAAmB;AAK5B,IAAI,mBAA2C;AAI/C,MAAM,aAAa,KAAK,QAAQ,cAAc;AAE9C,eAAO,WACL,KACA,KACe;AAKf,QAAM,SAAS,IAAI,OAAO;AAC1B,QAAM,WAAW,OAAO,MAAM,GAAG,EAAE,CAAC;AAEpC,MAAI,GAAG,WAAW,UAAU,GAAG;AAK7B,UAAM,YAAY,KAAK,KAAK,YAAY,QAAQ;AAKhD,UAAM,aAAa,WAAW,SAAS,KAAK,GAAG,IAAI,aAAa,aAAa,KAAK;AAClF,UAAM,OAAO,UAAU,WAAW,UAAU,KAAK,cAAc;AAE/D,QAAI,CAAC,MAAM;AACT,UAAI,aAAa;AACjB,UAAI,IAAI,aAAa;AACrB;AAAA,IACF;AAIA,QAAI,GAAG,WAAW,SAAS,KAAK,GAAG,SAAS,SAAS,EAAE,OAAO,GAAG;AAC/D,YAAM,MAAM,KAAK,QAAQ,SAAS;AAClC,UAAI,UAAU,gBAAgB,YAAY,GAAG,CAAC;AAC9C,UAAI,IAAI,GAAG,aAAa,SAAS,CAAC;AAClC;AAAA,IACF;AAAA,EACF;AAIA,MAAI,WAAW,aAAa;AAC1B,QAAI,CAAC,kBAAkB;AACrB,YAAM,MAAQ,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;AACzD,YAAM,QAAQ,KAAK,KAAK,KAAK,cAAc,IAAI,SAAS,MAAM,IAAI,OAAO,IAAI,EAAE;AAC/E,yBAAmB,MAAM;AAAA,QACvB,aAAa,CAAC,KAAK;AAAA,QACnB,OAAa;AAAA,QACb,QAAa;AAAA,QACb,QAAa;AAAA,QACb,QAAa;AAAA,QACb,UAAa,CAAC,SAAS,oBAAoB,mBAAmB;AAAA,MAChE,CAAC,EAAE,KAAK,OAAK,EAAE,YAAY,CAAC,EAAE,IAAI;AAAA,IACpC;AAEA,QAAI,UAAU,gBAAgB,wBAAwB;AACtD,QAAI,IAAI,MAAM,gBAAgB;AAC9B;AAAA,EACF;AAKA,MAAI,WAAW,UAAU;AAMvB,UAAM,iBAAiB;AACvB,UAAM,aAAa,IAAI,QAAQ;AAC/B,QAAI,YAAY;AACd,YAAM,aAAa,CAAC,GAAG,UAAU,EAAE;AAAA,QACjC,OAAM,EAAU,QAAQ,kBAAkB;AAAA,MAC5C;AACA,UAAI,WAAW,UAAU,gBAAgB;AAEvC,cAAM,SAAS,WAAW,CAAC;AAC3B,eAAO,QAAQ;AACf,mBAAW,OAAO,MAAM;AAAA,MAC1B;AAAA,IACF;AAEA,QAAI,UAAU,gBAAgB,mBAAmB;AACjD,QAAI,UAAU,iBAAiB,UAAU;AACzC,QAAI,UAAU,cAAc,YAAY;AACxC,QAAI,UAAU,+BAA+B,GAAG;AAChD,QAAI,aAAa;AAEjB,QAAI,MAAM,gCAAgC;AAE1C,eAAW,IAAI,GAAG;AAClB,QAAI,GAAG,SAAS,MAAM,WAAW,OAAO,GAAG,CAAC;AAC5C;AAAA,EACF;AACF;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
package/dist/renderer.d.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* renderer.ts — Dev-Mode Async SSR Renderer
|
|
3
|
-
*
|
|
4
|
-
* Implements a recursive async renderer used in `nuke dev` to convert a React
|
|
5
|
-
* element tree into an HTML string. It is a lighter alternative to
|
|
6
|
-
* react-dom/server.renderToString that:
|
|
7
|
-
*
|
|
8
|
-
* - Supports async server components (components that return Promises).
|
|
9
|
-
* - Emits <span data-hydrate-id="…"> markers for "use client" boundaries
|
|
10
|
-
* instead of trying to render them server-side without their browser APIs.
|
|
11
|
-
* - Serializes props passed to client components into the marker's
|
|
12
|
-
* data-hydrate-props attribute so the browser can reconstruct them.
|
|
13
|
-
*
|
|
14
|
-
* In production (nuke build), the equivalent renderer is inlined into each
|
|
15
|
-
* page's standalone bundle by build-common.ts (makePageAdapterSource).
|
|
16
|
-
*
|
|
17
|
-
* RenderContext:
|
|
18
|
-
* registry — Map<id, filePath> of all client components for this page.
|
|
19
|
-
* Populated by component-analyzer.ts before rendering.
|
|
20
|
-
* hydrated — Set<id> populated during render; used to tell the browser
|
|
21
|
-
* which components to hydrate on this specific request.
|
|
22
|
-
* skipClientSSR — When true (HMR request), client components emit an empty
|
|
23
|
-
* marker instead of running renderToString (faster dev reload).
|
|
24
|
-
*/
|
|
25
|
-
export interface RenderContext {
|
|
26
|
-
/** id → absolute file path for every client component reachable from this page. */
|
|
27
|
-
registry: Map<string, string>;
|
|
28
|
-
/** Populated during render: IDs of client components actually encountered. */
|
|
29
|
-
hydrated: Set<string>;
|
|
30
|
-
/** When true, skip renderToString for client components (faster HMR). */
|
|
31
|
-
skipClientSSR?: boolean;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Recursively renders a React element (or primitive) to an HTML string.
|
|
35
|
-
*
|
|
36
|
-
* Handles:
|
|
37
|
-
* null / undefined / boolean → ''
|
|
38
|
-
* string / number → HTML-escaped text
|
|
39
|
-
* array → rendered in parallel, joined
|
|
40
|
-
* Fragment → renders children directly
|
|
41
|
-
* HTML element string → renderHtmlElement()
|
|
42
|
-
* function component → renderFunctionComponent()
|
|
43
|
-
*/
|
|
44
|
-
export declare function renderElementToHtml(element: any, ctx: RenderContext): Promise<string>;
|
package/dist/renderer.js.map
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 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 { 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 Wrapper attribute helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Attributes that belong on the hydration <span> wrapper rather than being\r\n * forwarded to the inner client component. Includes className, style, id,\r\n * and any data-* / aria-* attributes.\r\n */\r\nfunction isWrapperAttr(key: string): boolean {\r\n return (\r\n key === 'className' ||\r\n key === 'style' ||\r\n key === 'id' ||\r\n key.startsWith('data-') ||\r\n key.startsWith('aria-')\r\n );\r\n}\r\n\r\n/**\r\n * Splits props into two bags:\r\n * wrapperAttrs \u2014 keys destined for the <span> (className, style, id, data-*, aria-*)\r\n * componentProps \u2014 everything else, forwarded to the actual component\r\n */\r\nfunction splitWrapperAttrs(props: any): {\r\n wrapperAttrs: Record<string, any>;\r\n componentProps: Record<string, any>;\r\n} {\r\n const wrapperAttrs: Record<string, any> = {};\r\n const componentProps: Record<string, any> = {};\r\n for (const [key, value] of Object.entries((props || {}) as Record<string, any>)) {\r\n if (isWrapperAttr(key)) wrapperAttrs[key] = value;\r\n else componentProps[key] = value;\r\n }\r\n return { wrapperAttrs, componentProps };\r\n}\r\n\r\n/**\r\n * Converts a wrapper-attrs bag into an HTML attribute string (leading space\r\n * included when non-empty) suitable for direct interpolation into a tag.\r\n *\r\n * className \u2192 class\r\n * style obj \u2192 \"prop:value;\u2026\" CSS string\r\n */\r\nfunction buildWrapperAttrString(attrs: Record<string, any>): string {\r\n const parts = Object.entries(attrs)\r\n .map(([key, value]) => {\r\n if (key === 'className') key = 'class';\r\n\r\n if (key === 'style' && typeof value === 'object') {\r\n const css = Object.entries(value as Record<string, any>)\r\n .map(([k, v]) => {\r\n const prop = k.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`);\r\n const safeVal = String(v).replace(/[<>\"'`\\\\]/g, '');\r\n return `${prop}:${safeVal}`;\r\n })\r\n .join(';');\r\n return `style=\"${css}\"`;\r\n }\r\n\r\n if (typeof value === 'boolean') return value ? key : '';\r\n if (value == null) return '';\r\n return `${key}=\"${escapeHtml(String(value))}\"`;\r\n })\r\n .filter(Boolean);\r\n\r\n return parts.length ? ' ' + parts.join(' ') : '';\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\r\n // Split props: wrapper attrs go on the <span>, the rest reach the component.\r\n const { wrapperAttrs, componentProps } = splitWrapperAttrs(props);\r\n const wrapperAttrStr = buildWrapperAttrString(wrapperAttrs);\r\n const serializedProps = serializePropsForHydration(componentProps, 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>, componentProps));\r\n\r\n return `<span data-hydrate-id=\"${id}\"${wrapperAttrStr} 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": "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",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/request-store.ts"],
|
|
4
|
-
"sourcesContent": ["/**\r\n * request-store.ts \u2014 Per-Request Server Context Store\r\n *\r\n * Provides a request-scoped store that server components can read via\r\n * `useRequest()` during SSR. The store is populated by the SSR pipeline\r\n * before rendering and cleared in a `finally` block after \u2014 preventing any\r\n * cross-request contamination.\r\n *\r\n * Why globalThis?\r\n * Node's module system may import this file multiple times when the page\r\n * module and the nukejs package resolve to different copies (common in dev\r\n * with tsx/tsImport). Using a well-known Symbol on globalThis guarantees\r\n * all copies share the same store instance, exactly like html-store.ts.\r\n *\r\n * Request isolation:\r\n * runWithRequestStore() creates a fresh store before rendering and clears\r\n * it in the `finally` block, so concurrent requests cannot bleed into each\r\n * other even if rendering throws.\r\n *\r\n * Headers in __n_data:\r\n * A safe subset of headers is embedded in the HTML `__n_data` blob so\r\n * client components can read them after hydration. Sensitive headers\r\n * (cookie, authorization, proxy-authorization) are intentionally excluded\r\n * from the client payload. The server-side store always has ALL headers.\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\nexport interface RequestContext {\r\n /** Full URL with query string (e.g. '/blog/hello?lang=en'). */\r\n url: string;\r\n /** Pathname only, no query string (e.g. '/blog/hello'). */\r\n pathname: string;\r\n /**\r\n * Dynamic route segments matched by the file-system router.\r\n * e.g. for `/blog/[slug]` \u2192 `{ slug: 'hello' }`\r\n */\r\n params: Record<string, string | string[]>;\r\n /**\r\n * Query string parameters, parsed from the URL.\r\n * Multi-value params (e.g. `?tag=a&tag=b`) become arrays.\r\n * e.g. `{ lang: 'en', tag: ['a', 'b'] }`\r\n */\r\n query: Record<string, string | string[]>;\r\n /**\r\n * Incoming request headers.\r\n *\r\n * Server-side (SSR): all headers from IncomingMessage.headers.\r\n * Client-side: safe subset embedded in __n_data (cookie, authorization,\r\n * proxy-authorization are stripped before serialisation).\r\n *\r\n * Multi-value headers are joined with ', '.\r\n */\r\n headers: Record<string, string>;\r\n}\r\n\r\n// \u2500\u2500\u2500 Headers that must never be serialised into the HTML document \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * These headers contain credentials or session tokens. They must not appear\r\n * in the __n_data JSON blob because the HTML document may be cached by\r\n * intermediate proxies or logged by analytics tools.\r\n */\r\nconst SENSITIVE_HEADERS = new Set([\r\n 'cookie',\r\n 'authorization',\r\n 'proxy-authorization',\r\n 'set-cookie',\r\n 'x-api-key',\r\n]);\r\n\r\n/**\r\n * Normalises raw Node `IncomingMessage.headers` into a flat `Record<string,string>`.\r\n * Array values (multi-value headers) are joined with `', '`.\r\n * Undefined values are dropped.\r\n *\r\n * Used server-side so all headers \u2014 including cookies and auth tokens \u2014 are\r\n * available to server components that need them.\r\n */\r\nexport function normaliseHeaders(\r\n raw: Record<string, string | string[] | undefined>,\r\n): Record<string, string> {\r\n const out: Record<string, string> = {};\r\n for (const [k, v] of Object.entries(raw)) {\r\n if (v === undefined) continue;\r\n out[k] = Array.isArray(v) ? v.join(', ') : v;\r\n }\r\n return out;\r\n}\r\n\r\n/**\r\n * Same as `normaliseHeaders` but additionally strips headers that must never\r\n * appear in a serialised HTML document. Used when embedding headers in the\r\n * `__n_data` blob so credentials cannot leak into cached or logged HTML pages.\r\n */\r\nexport function sanitiseHeaders(\r\n raw: Record<string, string | string[] | undefined>,\r\n): Record<string, string> {\r\n const out: Record<string, string> = {};\r\n for (const [k, v] of Object.entries(raw)) {\r\n if (SENSITIVE_HEADERS.has(k.toLowerCase())) continue;\r\n if (v === undefined) continue;\r\n out[k] = Array.isArray(v) ? v.join(', ') : v;\r\n }\r\n return out;\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 shared across all copies of this module in the process. */\r\nconst KEY = Symbol.for('__nukejs_request_store__');\r\n\r\nconst getGlobal = (): RequestContext | null => (globalThis as any)[KEY] ?? null;\r\nconst setGlobal = (ctx: RequestContext | null): void => {\r\n (globalThis as any)[KEY] = ctx;\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 the context of the given request, then clears the store.\r\n *\r\n * Usage in the SSR pipeline:\r\n * ```ts\r\n * const store = await runWithRequestStore(ctx, async () => {\r\n * appHtml = await renderElementToHtml(element, renderCtx);\r\n * });\r\n * ```\r\n */\r\nexport async function runWithRequestStore<T>(\r\n ctx: RequestContext,\r\n fn: () => Promise<T>,\r\n): Promise<T> {\r\n setGlobal(ctx);\r\n try {\r\n return await fn();\r\n } finally {\r\n // Always clear even on throw \u2014 prevents leakage into the next request.\r\n setGlobal(null);\r\n }\r\n}\r\n\r\n/**\r\n * Returns the current request context, or `null` when called outside of\r\n * an active `runWithRequestStore` scope (e.g. in the browser, in tests,\r\n * or in a client component).\r\n */\r\nexport function getRequestStore(): RequestContext | null {\r\n return getGlobal();\r\n}"],
|
|
5
|
-
"mappings": "AA+DA,MAAM,oBAAoB,oBAAI,IAAI;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAUM,SAAS,iBACd,KACwB;AACxB,QAAM,MAA8B,CAAC;AACrC,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,GAAG,GAAG;AACxC,QAAI,MAAM,OAAW;AACrB,QAAI,CAAC,IAAI,MAAM,QAAQ,CAAC,IAAI,EAAE,KAAK,IAAI,IAAI;AAAA,EAC7C;AACA,SAAO;AACT;AAOO,SAAS,gBACd,KACwB;AACxB,QAAM,MAA8B,CAAC;AACrC,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,GAAG,GAAG;AACxC,QAAI,kBAAkB,IAAI,EAAE,YAAY,CAAC,EAAG;AAC5C,QAAI,MAAM,OAAW;AACrB,QAAI,CAAC,IAAI,MAAM,QAAQ,CAAC,IAAI,EAAE,KAAK,IAAI,IAAI;AAAA,EAC7C;AACA,SAAO;AACT;AAKA,MAAM,MAAM,uBAAO,IAAI,0BAA0B;AAEjD,MAAM,YAAY,MAA8B,WAAmB,GAAG,KAAK;AAC3E,MAAM,YAAY,CAAC,QAAqC;AACtD,EAAC,WAAmB,GAAG,IAAI;AAC7B;AAcA,eAAsB,oBACpB,KACA,IACY;AACZ,YAAU,GAAG;AACb,MAAI;AACF,WAAO,MAAM,GAAG;AAAA,EAClB,UAAE;AAEA,cAAU,IAAI;AAAA,EAChB;AACF;AAOO,SAAS,kBAAyC;AACvD,SAAO,UAAU;AACnB;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
package/dist/router.d.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* router.ts — File-System Based URL Router
|
|
3
|
-
*
|
|
4
|
-
* Maps incoming URL paths to handler files using Next.js-compatible conventions:
|
|
5
|
-
*
|
|
6
|
-
* server/users/index.ts → /users
|
|
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)
|
|
10
|
-
*
|
|
11
|
-
* Route specificity (higher = wins over lower):
|
|
12
|
-
* static segment +5 (e.g. 'about')
|
|
13
|
-
* dynamic single +4 (e.g. '[id]')
|
|
14
|
-
* optional single +3 (e.g. '[[id]]')
|
|
15
|
-
* required catch-all +2 (e.g. '[...slug]')
|
|
16
|
-
* optional catch-all +1 (e.g. '[[...path]]')
|
|
17
|
-
* (scores match getRouteSpecificity in this file)
|
|
18
|
-
*
|
|
19
|
-
* Path traversal protection:
|
|
20
|
-
* matchRoute() rejects URL segments that contain '..' or '.' and verifies
|
|
21
|
-
* that the resolved file path stays inside the base directory before
|
|
22
|
-
* checking whether the file exists.
|
|
23
|
-
*/
|
|
24
|
-
/**
|
|
25
|
-
* Recursively collects all routable .ts/.tsx files in `dir`, returning paths
|
|
26
|
-
* relative to `baseDir` without the file extension.
|
|
27
|
-
*
|
|
28
|
-
* layout.tsx files are excluded — they wrap pages but are never routes
|
|
29
|
-
* themselves. This mirrors the filter in collectServerPages() so dev-mode
|
|
30
|
-
* route matching behaves identically to the production build.
|
|
31
|
-
*
|
|
32
|
-
* Example output: ['index', 'users/index', 'users/[id]', 'blog/[...slug]']
|
|
33
|
-
*/
|
|
34
|
-
export declare function findAllRoutes(dir: string, baseDir?: string): string[];
|
|
35
|
-
/**
|
|
36
|
-
* Attempts to match `urlSegments` against a route that may contain dynamic
|
|
37
|
-
* segments ([param]), catch-alls ([...slug]), and optional catch-alls ([[...path]]).
|
|
38
|
-
*
|
|
39
|
-
* Returns the captured params on success, or null if the route does not match.
|
|
40
|
-
*
|
|
41
|
-
* Param value types:
|
|
42
|
-
* [param] → string
|
|
43
|
-
* [...slug] → string[] (at least one segment required)
|
|
44
|
-
* [[...path]] → string[] (zero or more segments)
|
|
45
|
-
*/
|
|
46
|
-
export declare function matchDynamicRoute(urlSegments: string[], routePath: string): {
|
|
47
|
-
params: Record<string, string | string[]>;
|
|
48
|
-
} | null;
|
|
49
|
-
/**
|
|
50
|
-
* Computes a specificity score for a route path.
|
|
51
|
-
* Used to sort candidate routes so more specific routes shadow catch-alls.
|
|
52
|
-
*
|
|
53
|
-
* Higher score = more specific:
|
|
54
|
-
* static segment 5
|
|
55
|
-
* [dynamic] 4
|
|
56
|
-
* [[optSingle]] 3
|
|
57
|
-
* [...catchAll] 2
|
|
58
|
-
* [[...optCatchAll]] 1
|
|
59
|
-
*/
|
|
60
|
-
export declare function getRouteSpecificity(routePath: string): number;
|
|
61
|
-
export interface RouteMatch {
|
|
62
|
-
filePath: string;
|
|
63
|
-
params: Record<string, string | string[]>;
|
|
64
|
-
routePattern: string;
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Resolves a URL path to a route file inside `baseDir`.
|
|
68
|
-
*
|
|
69
|
-
* Steps:
|
|
70
|
-
* 1. Reject '..' or '.' path segments (path traversal guard).
|
|
71
|
-
* 2. Try an exact file match (e.g. /about → baseDir/about.tsx).
|
|
72
|
-
* layout.tsx is explicitly excluded from exact matching.
|
|
73
|
-
* 3. Sort all discovered routes by specificity (most specific first).
|
|
74
|
-
* 4. Return the first dynamic route that matches.
|
|
75
|
-
*
|
|
76
|
-
* @param urlPath The URL path to match (e.g. '/users/42').
|
|
77
|
-
* @param baseDir Absolute path to the directory containing route files.
|
|
78
|
-
* @param extension File extension to look for ('.tsx' or '.ts').
|
|
79
|
-
*/
|
|
80
|
-
export declare function matchRoute(urlPath: string, baseDir: string, extension?: string): RouteMatch | null;
|
|
81
|
-
/**
|
|
82
|
-
* Returns every layout.tsx file that wraps a given route file, in
|
|
83
|
-
* outermost-first order (root layout first, nearest layout last).
|
|
84
|
-
*
|
|
85
|
-
* Layout chain example for app/pages/blog/[slug]/page.tsx:
|
|
86
|
-
* app/pages/layout.tsx ← root layout
|
|
87
|
-
* app/pages/blog/layout.tsx ← blog section layout
|
|
88
|
-
*
|
|
89
|
-
* The outermost-first order matches how wrapWithLayouts() nests them:
|
|
90
|
-
* the last layout in the array is the innermost wrapper.
|
|
91
|
-
*/
|
|
92
|
-
export declare function findLayoutsForRoute(routeFilePath: string, pagesDir: string): string[];
|
package/dist/router.js.map
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/router.ts"],
|
|
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 +5 (e.g. 'about')\r\n * dynamic single +4 (e.g. '[id]')\r\n * optional single +3 (e.g. '[[id]]')\r\n * required catch-all +2 (e.g. '[...slug]')\r\n * optional catch-all +1 (e.g. '[[...path]]')\r\n * (scores match getRouteSpecificity in this file)\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 5\r\n * [dynamic] 4\r\n * [[optSingle]] 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": "AAwBA,OAAO,UAAU;AACjB,OAAO,QAAU;AAcV,SAAS,cAAc,KAAa,UAAkB,KAAe;AAC1E,MAAI,CAAC,GAAG,WAAW,GAAG,EAAG,QAAO,CAAC;AAEjC,QAAM,SAAmB,CAAC;AAC1B,aAAW,SAAS,GAAG,YAAY,KAAK,EAAE,eAAe,KAAK,CAAC,GAAG;AAChE,UAAM,WAAW,KAAK,KAAK,KAAK,MAAM,IAAI;AAC1C,QAAI,MAAM,YAAY,GAAG;AACvB,aAAO,KAAK,GAAG,cAAc,UAAU,OAAO,CAAC;AAAA,IACjD,WAAW,MAAM,KAAK,SAAS,MAAM,KAAK,MAAM,KAAK,SAAS,KAAK,GAAG;AACpE,YAAM,OAAO,MAAM,KAAK,QAAQ,eAAe,EAAE;AACjD,UAAI,SAAS,SAAU;AACvB,aAAO,KAAK,KAAK,SAAS,SAAS,QAAQ,EAAE,QAAQ,eAAe,EAAE,CAAC;AAAA,IACzE;AAAA,EACF;AACA,SAAO;AACT;AAeO,SAAS,kBACd,aACA,WACsD;AACtD,QAAM,gBAAgB,UAAU,MAAM,KAAK,GAAG;AAG9C,MAAI,cAAc,GAAG,EAAE,MAAM,QAAS,eAAc,IAAI;AAExD,QAAM,SAA4C,CAAC;AACnD,MAAI,KAAK;AACT,MAAI,KAAK;AAET,SAAO,KAAK,cAAc,QAAQ;AAChC,UAAM,MAAM,cAAc,EAAE;AAG5B,UAAM,cAAc,IAAI,MAAM,sBAAsB;AACpD,QAAI,aAAa;AACf,aAAO,YAAY,CAAC,CAAC,IAAI,YAAY,MAAM,EAAE;AAC7C,aAAO,EAAE,OAAO;AAAA,IAClB;AAGA,UAAM,aAAa,IAAI,MAAM,wBAAwB;AACrD,QAAI,YAAY;AACd,UAAI,KAAK,YAAY,QAAQ;AAC3B,eAAO,WAAW,CAAC,CAAC,IAAI,YAAY,IAAI;AAAA,MAC1C,OAAO;AACL,eAAO,WAAW,CAAC,CAAC,IAAI;AAAA,MAC1B;AACA;AACA;AAAA,IACF;AAGA,UAAM,WAAW,IAAI,MAAM,kBAAkB;AAC7C,QAAI,UAAU;AACZ,YAAM,YAAY,YAAY,MAAM,EAAE;AACtC,UAAI,CAAC,UAAU,OAAQ,QAAO;AAC9B,aAAO,SAAS,CAAC,CAAC,IAAI;AACtB,aAAO,EAAE,OAAO;AAAA,IAClB;AAGA,UAAM,UAAU,IAAI,MAAM,YAAY;AACtC,QAAI,SAAS;AACX,UAAI,MAAM,YAAY,OAAQ,QAAO;AACrC,aAAO,QAAQ,CAAC,CAAC,IAAI,YAAY,IAAI;AACrC;AACA;AAAA,IACF;AAGA,QAAI,MAAM,YAAY,UAAU,QAAQ,YAAY,EAAE,EAAG,QAAO;AAChE;AAAM;AAAA,EACR;AAGA,SAAO,KAAK,YAAY,SAAS,OAAO,EAAE,OAAO;AACnD;AAeO,SAAS,oBAAoB,WAA2B;AAC7D,SAAO,UAAU,MAAM,KAAK,GAAG,EAAE,OAAO,CAAC,OAAO,QAAQ;AACtD,QAAI,IAAI,MAAM,sBAAsB,EAAQ,QAAO,QAAQ;AAC3D,QAAI,IAAI,MAAM,kBAAkB,EAAa,QAAO,QAAQ;AAC5D,QAAI,IAAI,MAAM,wBAAwB,EAAO,QAAO,QAAQ;AAC5D,QAAI,IAAI,MAAM,YAAY,EAAmB,QAAO,QAAQ;AAC5D,WAAO,QAAQ;AAAA,EACjB,GAAG,CAAC;AACN;AAgBA,SAAS,aAAa,SAAiB,UAA2B;AAChE,QAAM,MAAM,KAAK,SAAS,SAAS,QAAQ;AAC3C,SAAO,QAAQ,GAAG,KAAK,CAAC,IAAI,WAAW,IAAI,KAAK,CAAC,KAAK,WAAW,GAAG;AACtE;AAkBO,SAAS,WACd,SACA,SACA,YAAY,QACO;AAEnB,QAAM,WAAc,QAAQ,SAAS,IAAI,QAAQ,QAAQ,QAAQ,EAAE,IAAI;AAEvE,QAAM,cAAc,aAAa,MAAM,CAAC,IAAI,SAAS,MAAM,CAAC,EAAE,MAAM,GAAG;AACvE,MAAI,YAAY,KAAK,OAAK,MAAM,QAAQ,MAAM,GAAG,EAAG,QAAO;AAG3D,QAAM,WAAW,YAAY,WAAW,IAAI,CAAC,OAAO,IAAI;AAGxD,QAAM,YAAY,KAAK,KAAK,SAAS,GAAG,QAAQ,IAAI;AACpD,QAAM,YAAY,KAAK,SAAS,WAAW,SAAS;AACpD,MAAI,CAAC,aAAa,SAAS,SAAS,EAAG,QAAO;AAC9C,MAAI,cAAc,YAAY,GAAG,WAAW,SAAS,GAAG;AACtD,WAAO,EAAE,UAAU,WAAW,QAAQ,CAAC,GAAG,cAAc,SAAS,KAAK,GAAG,EAAE;AAAA,EAC7E;AAIA,QAAM,eAAe,cAAc,OAAO,EAAE;AAAA,IAC1C,CAAC,GAAG,MAAM,oBAAoB,CAAC,IAAI,oBAAoB,CAAC;AAAA,EAC1D;AAEA,aAAW,SAAS,cAAc;AAChC,UAAM,QAAQ,kBAAkB,aAAa,KAAK;AAClD,QAAI,CAAC,MAAO;AACZ,UAAM,WAAW,KAAK,KAAK,SAAS,KAAK,IAAI;AAC7C,QAAI,CAAC,aAAa,SAAS,QAAQ,EAAG;AACtC,QAAI,GAAG,WAAW,QAAQ,GAAG;AAC3B,aAAO,EAAE,UAAU,QAAQ,MAAM,QAAQ,cAAc,MAAM;AAAA,IAC/D;AAAA,EACF;AAEA,SAAO;AACT;AAeO,SAAS,oBAAoB,eAAuB,UAA4B;AACrF,QAAM,UAAoB,CAAC;AAG3B,QAAM,aAAa,KAAK,KAAK,UAAU,YAAY;AACnD,MAAI,GAAG,WAAW,UAAU,EAAG,SAAQ,KAAK,UAAU;AAGtD,QAAM,eAAe,KAAK,SAAS,UAAU,KAAK,QAAQ,aAAa,CAAC;AACxE,MAAI,CAAC,gBAAgB,iBAAiB,IAAK,QAAO;AAElD,QAAM,WAAW,aAAa,MAAM,KAAK,GAAG,EAAE,OAAO,OAAK,MAAM,GAAG;AACnE,WAAS,IAAI,GAAG,KAAK,SAAS,QAAQ,KAAK;AACzC,UAAM,aAAa,KAAK,KAAK,UAAU,GAAG,SAAS,MAAM,GAAG,CAAC,GAAG,YAAY;AAC5E,QAAI,GAAG,WAAW,UAAU,EAAG,SAAQ,KAAK,UAAU;AAAA,EACxD;AAEA,SAAO;AACT;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
package/dist/ssr.d.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ssr.ts — Server-Side Rendering Pipeline (Dev Mode)
|
|
3
|
-
*
|
|
4
|
-
* Handles the full page render cycle for `nuke dev`:
|
|
5
|
-
*
|
|
6
|
-
* 1. Match the incoming URL to a page file in app/pages using the file-system
|
|
7
|
-
* router.
|
|
8
|
-
* 2. Discover the layout chain (layout.tsx files from root to the page dir).
|
|
9
|
-
* 3. Dynamically import the page and layout modules (always fresh via tsImport).
|
|
10
|
-
* 4. Walk the import tree to discover all "use client" components.
|
|
11
|
-
* 5. Render the wrapped element tree with the async renderer.
|
|
12
|
-
* 6. Flush the html-store (title, meta, link, script, style tags).
|
|
13
|
-
* 7. Assemble and send the full HTML document including:
|
|
14
|
-
* - The rendered app HTML
|
|
15
|
-
* - An importmap pointing react/nukejs to the bundled versions
|
|
16
|
-
* - A __n_data JSON blob with hydration IDs and runtime config
|
|
17
|
-
* - An inline bootstrap <script> that calls initRuntime()
|
|
18
|
-
* - (dev only) A /__hmr.js script for Hot Module Replacement
|
|
19
|
-
*
|
|
20
|
-
* In production, a pre-built standalone handler (generated by build-common.ts)
|
|
21
|
-
* handles each page without dynamic imports or file-system access.
|
|
22
|
-
*
|
|
23
|
-
* HMR fast path:
|
|
24
|
-
* When the request URL contains `__hmr=1` (added by the HMR client during
|
|
25
|
-
* soft navigation), the renderer skips client-component renderToString
|
|
26
|
-
* (ctx.skipClientSSR = true). This speeds up HMR reloads because the
|
|
27
|
-
* client already has the DOM in place and only needs fresh server markup.
|
|
28
|
-
*
|
|
29
|
-
* Head tag sentinels:
|
|
30
|
-
* Every useHtml()-generated <meta>, <link>, <style>, and <script> tag is
|
|
31
|
-
* wrapped in <!--n-head-->…<!--/n-head--> comment sentinels. The client
|
|
32
|
-
* runtime uses these to diff and sync head tags on SPA navigation without
|
|
33
|
-
* touching permanent tags (charset, viewport, importmap, runtime script).
|
|
34
|
-
* Pages with no useHtml head tags emit no sentinels.
|
|
35
|
-
*/
|
|
36
|
-
import type { ServerResponse } from 'http';
|
|
37
|
-
import type { IncomingMessage } from 'http';
|
|
38
|
-
/**
|
|
39
|
-
* Renders a page for the given URL and writes the full HTML response.
|
|
40
|
-
*
|
|
41
|
-
* @param url The raw request URL (may include query string).
|
|
42
|
-
* @param res Node ServerResponse to write to.
|
|
43
|
-
* @param pagesDir Absolute path to the app/pages directory.
|
|
44
|
-
* @param isDev When true, injects the HMR client script into the page.
|
|
45
|
-
*/
|
|
46
|
-
export declare function serverSideRender(url: string, res: ServerResponse, pagesDir: string, isDev?: boolean, req?: IncomingMessage): Promise<void>;
|
package/dist/ssr.js.map
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/ssr.ts"],
|
|
4
|
-
"sourcesContent": ["/**\r\n * ssr.ts \u2014 Server-Side Rendering Pipeline (Dev Mode)\r\n *\r\n * Handles the full page render cycle for `nuke dev`:\r\n *\r\n * 1. Match the incoming URL to a page file in app/pages using the file-system\r\n * router.\r\n * 2. Discover the layout chain (layout.tsx files from root to the page dir).\r\n * 3. Dynamically import the page and layout modules (always fresh via tsImport).\r\n * 4. Walk the import tree to discover all \"use client\" components.\r\n * 5. Render the wrapped element tree with the async renderer.\r\n * 6. Flush the html-store (title, meta, link, script, style tags).\r\n * 7. Assemble and send the full HTML document including:\r\n * - The rendered app HTML\r\n * - An importmap pointing react/nukejs to the bundled versions\r\n * - A __n_data JSON blob with hydration IDs and runtime config\r\n * - An inline bootstrap <script> that calls initRuntime()\r\n * - (dev only) A /__hmr.js script for Hot Module Replacement\r\n *\r\n * In production, a pre-built standalone handler (generated by build-common.ts)\r\n * handles each page without dynamic imports or file-system access.\r\n *\r\n * HMR fast path:\r\n * When the request URL contains `__hmr=1` (added by the HMR client during\r\n * soft navigation), the renderer skips client-component renderToString\r\n * (ctx.skipClientSSR = true). This speeds up HMR reloads because the\r\n * client already has the DOM in place and only needs fresh server markup.\r\n *\r\n * Head tag sentinels:\r\n * Every useHtml()-generated <meta>, <link>, <style>, and <script> tag is\r\n * wrapped in <!--n-head-->\u2026<!--/n-head--> comment sentinels. The client\r\n * runtime uses these to diff and sync head tags on SPA navigation without\r\n * touching permanent tags (charset, viewport, importmap, runtime script).\r\n * Pages with no useHtml head tags emit no sentinels.\r\n */\r\n\r\nimport path from 'path';\r\nimport { createElement } from 'react';\r\nimport { pathToFileURL } from 'url';\r\nimport { tsImport } from 'tsx/esm/api';\r\nimport type { ServerResponse } from 'http';\r\nimport { log, getDebugLevel, type DebugLevel } from './logger';\r\nimport { matchRoute, findLayoutsForRoute } from './router';\r\nimport { findClientComponentsInTree } from './component-analyzer';\r\nimport { renderElementToHtml, type RenderContext } from './renderer';\r\nimport { runWithRequestStore, normaliseHeaders, sanitiseHeaders } from './request-store';\r\nimport type { IncomingMessage } from 'http';\r\nimport {\r\n runWithHtmlStore,\r\n resolveTitle,\r\n type HtmlStore,\r\n type HtmlAttrs,\r\n type BodyAttrs,\r\n type MetaTag,\r\n type LinkTag,\r\n type ScriptTag,\r\n type StyleTag,\r\n} from './html-store';\r\n\r\n// \u2500\u2500\u2500 Layout wrapping \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Wraps a page element in its layout chain, outermost-first.\r\n * Each layout receives `{ children: innerElement }` as props.\r\n *\r\n * tsImport creates a fresh isolated module namespace per call so edits to\r\n * layouts are reflected immediately without a server restart.\r\n */\r\nasync function wrapWithLayouts(pageElement: any, layoutPaths: string[]): Promise<any> {\r\n let element = pageElement;\r\n // Iterate in reverse so the outermost layout wraps last (becomes the root).\r\n for (let i = layoutPaths.length - 1; i >= 0; i--) {\r\n const { default: LayoutComponent } = await tsImport(\r\n pathToFileURL(layoutPaths[i]).href,\r\n { parentURL: import.meta.url },\r\n );\r\n element = createElement(LayoutComponent, { children: element });\r\n }\r\n return element;\r\n}\r\n\r\n// \u2500\u2500\u2500 Debug level conversion \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 the server-side DebugLevel to the string format expected by the\r\n * browser's makeLogger() function in bundle.ts.\r\n */\r\nfunction toClientDebugLevel(level: DebugLevel): string {\r\n if (level === true) return 'verbose';\r\n if (level === 'info') return 'info';\r\n if (level === 'error') return 'error';\r\n return 'silent';\r\n}\r\n\r\n// \u2500\u2500\u2500 HTML serialization helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\nfunction escapeAttr(str: string): string {\r\n return str.replace(/&/g, '&').replace(/\"/g, '"');\r\n}\r\n\r\n/** Serializes an attribute map to `key=\"value\" \u2026`, omitting undefined/false values. */\r\nfunction renderAttrs(attrs: Record<string, string | boolean | undefined>): string {\r\n return Object.entries(attrs)\r\n .filter(([, v]) => v !== undefined && v !== false)\r\n .map(([k, v]) => v === true ? k : `${k}=\"${escapeAttr(String(v))}\"`)\r\n .join(' ');\r\n}\r\n\r\n/** Returns `<tag attrs>` or `<tag>` depending on whether attrs are non-empty. */\r\nfunction openTag(tag: string, attrs: Record<string, string | undefined>): string {\r\n const str = renderAttrs(attrs);\r\n return str ? `<${tag} ${str}>` : `<${tag}>`;\r\n}\r\n\r\n// \u2500\u2500\u2500 Head tag renderers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\nfunction metaKey(k: string): string {\r\n return k === 'httpEquiv' ? 'http-equiv' : k;\r\n}\r\n\r\nfunction linkKey(k: string): string {\r\n if (k === 'hrefLang') return 'hreflang';\r\n if (k === 'crossOrigin') return 'crossorigin';\r\n return k;\r\n}\r\n\r\nfunction renderMetaTag(tag: MetaTag): string {\r\n const attrs: Record<string, string | undefined> = {};\r\n for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[metaKey(k)] = v;\r\n return ` <meta ${renderAttrs(attrs)} />`;\r\n}\r\n\r\nfunction renderLinkTag(tag: LinkTag): string {\r\n const attrs: Record<string, string | undefined> = {};\r\n for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[linkKey(k)] = v;\r\n return ` <link ${renderAttrs(attrs)} />`;\r\n}\r\n\r\nfunction renderScriptTag(tag: ScriptTag): string {\r\n const attrs: Record<string, string | boolean | undefined> = {\r\n src: tag.src,\r\n type: tag.type,\r\n crossorigin: tag.crossOrigin,\r\n integrity: tag.integrity,\r\n defer: tag.defer,\r\n async: tag.async,\r\n nomodule: tag.noModule,\r\n };\r\n const attrStr = renderAttrs(attrs);\r\n const open = attrStr ? `<script ${attrStr}>` : '<script>';\r\n return ` ${open}${tag.src ? '' : (tag.content ?? '')}</script>`;\r\n}\r\n\r\nfunction renderStyleTag(tag: StyleTag): string {\r\n const media = tag.media ? ` media=\"${escapeAttr(tag.media)}\"` : '';\r\n return ` <style${media}>${tag.content ?? ''}</style>`;\r\n}\r\n\r\n/**\r\n * Renders all useHtml()-sourced head tags wrapped in <!--n-head--> sentinels.\r\n * Scripts with position='body' are excluded here \u2014 they go in renderManagedBodyScripts().\r\n * Returns an empty array when none of the stores have any tags (no sentinels\r\n * emitted for pages that don't call useHtml).\r\n */\r\nfunction renderManagedHeadTags(store: HtmlStore): string[] {\r\n const headScripts = store.script.filter(s => (s.position ?? 'head') === 'head');\r\n const tags = [\r\n ...store.meta.map(renderMetaTag),\r\n ...store.link.map(renderLinkTag),\r\n ...store.style.map(renderStyleTag),\r\n ...headScripts.map(renderScriptTag),\r\n ];\r\n if (tags.length === 0) return [];\r\n return [' <!--n-head-->', ...tags, ' <!--/n-head-->'];\r\n}\r\n\r\n/**\r\n * Renders all useHtml()-sourced scripts with position='body', wrapped in\r\n * <!--n-body-scripts-->\u2026<!--/n-body-scripts--> sentinels.\r\n * Injected just before </body> so scripts execute after page content is in the DOM.\r\n * Returns an empty array when there are no body-position scripts.\r\n */\r\nfunction renderManagedBodyScripts(store: HtmlStore): string[] {\r\n const bodyScripts = store.script.filter(s => s.position === 'body');\r\n if (bodyScripts.length === 0) return [];\r\n return [' <!--n-body-scripts-->', ...bodyScripts.map(renderScriptTag), ' <!--/n-body-scripts-->'];\r\n}\r\n\r\n// \u2500\u2500\u2500 Main SSR handler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 page for the given URL and writes the full HTML response.\r\n *\r\n * @param url The raw request URL (may include query string).\r\n * @param res Node ServerResponse to write to.\r\n * @param pagesDir Absolute path to the app/pages directory.\r\n * @param isDev When true, injects the HMR client script into the page.\r\n */\r\nexport async function serverSideRender(\r\n url: string,\r\n res: ServerResponse,\r\n pagesDir: string,\r\n isDev = false,\r\n req?: IncomingMessage,\r\n): Promise<void> {\r\n const skipClientSSR = url.includes('__hmr=1');\r\n const cleanUrl = url.split('?')[0];\r\n\r\n // \u2500\u2500 Route resolution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 const routeMatch = matchRoute(cleanUrl, pagesDir);\r\n if (!routeMatch) {\r\n log.verbose(`No route found for: ${url}`);\r\n res.statusCode = 404;\r\n res.end('Page not found');\r\n return;\r\n }\r\n\r\n const { filePath, params, routePattern } = routeMatch;\r\n log.verbose(`SSR ${cleanUrl} -> ${path.relative(process.cwd(), filePath)}`);\r\n\r\n // Merge query string params into props so page components can access them\r\n // the same way in dev and production. Route params take precedence so a\r\n // ?slug=x in the query string cannot shadow a [slug] dynamic segment.\r\n const searchParams = new URL(url, 'http://localhost').searchParams;\r\n const queryParams: Record<string, string | string[]> = {};\r\n searchParams.forEach((_, k) => {\r\n if (!(k in params)) {\r\n const all = searchParams.getAll(k);\r\n queryParams[k] = all.length > 1 ? all : all[0];\r\n }\r\n });\r\n const mergedParams = { ...queryParams, ...params };\r\n\r\n // normaliseHeaders keeps every header (including cookies) for server components.\r\n // sanitiseHeaders additionally strips credentials before embedding in HTML.\r\n const rawHeaders = req?.headers ?? {};\r\n const normHeaders = normaliseHeaders(rawHeaders as Record<string, string | string[] | undefined>);\r\n const safeHeaders = sanitiseHeaders(rawHeaders as Record<string, string | string[] | undefined>);\r\n\r\n // \u2500\u2500 Module import \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 // tsImport bypasses Node's ESM module cache entirely so edits are reflected\r\n // immediately on every request in dev.\r\n const layoutPaths = findLayoutsForRoute(filePath, pagesDir);\r\n const { default: PageComponent } = await tsImport(\r\n pathToFileURL(filePath).href,\r\n { parentURL: import.meta.url },\r\n );\r\n const wrappedElement = await wrapWithLayouts(\r\n createElement(PageComponent, mergedParams),\r\n layoutPaths,\r\n );\r\n\r\n // \u2500\u2500 Client component 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\r\n // Walk the import tree for both the page and its layout chain.\r\n const registry = new Map<string, string>();\r\n for (const [id, p] of findClientComponentsInTree(filePath, pagesDir))\r\n registry.set(id, p);\r\n for (const layoutPath of layoutPaths)\r\n for (const [id, p] of findClientComponentsInTree(layoutPath, pagesDir))\r\n registry.set(id, p);\r\n\r\n log.verbose(\r\n `Page ${routePattern}: found ${registry.size} client component(s)`,\r\n `[${[...registry.keys()].join(', ')}]`,\r\n );\r\n\r\n // \u2500\u2500 Rendering \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 const ctx: RenderContext = { registry, hydrated: new Set(), skipClientSSR };\r\n\r\n let appHtml = '';\r\n // runWithRequestStore makes { params, query, headers } available to any server\r\n // component that calls useRequest() during this render.\r\n const store: HtmlStore = await runWithRequestStore(\r\n {\r\n url: url,\r\n pathname: cleanUrl,\r\n params,\r\n query: queryParams,\r\n headers: normHeaders,\r\n },\r\n () => runWithHtmlStore(async () => {\r\n appHtml = await renderElementToHtml(wrappedElement, ctx);\r\n }),\r\n );\r\n\r\n // \u2500\u2500 Head assembly \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 const pageTitle = resolveTitle(store.titleOps, 'NukeJS');\r\n\r\n const headLines: string[] = [\r\n ' <meta charset=\"utf-8\" />',\r\n ' <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />',\r\n ` <title>${escapeAttr(pageTitle)}</title>`,\r\n ...renderManagedHeadTags(store),\r\n ];\r\n\r\n // \u2500\u2500 Runtime data blob \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 // Escape </script> sequences so the JSON cannot break out of the script tag.\r\n // `query` and `headers` (sanitised) are included so useRequest() can read\r\n // them from the client without an extra network round-trip.\r\n const runtimeData = JSON.stringify({\r\n hydrateIds: [...ctx.hydrated],\r\n allIds: [...registry.keys()],\r\n url,\r\n params,\r\n query: queryParams,\r\n headers: safeHeaders,\r\n debug: toClientDebugLevel(getDebugLevel()),\r\n })\r\n .replace(/</g, '\\\\u003c')\r\n .replace(/>/g, '\\\\u003e')\r\n .replace(/&/g, '\\\\u0026');\r\n\r\n // \u2500\u2500 Body scripts (position='body') \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 const bodyScriptLines = renderManagedBodyScripts(store);\r\n const bodyScriptsHtml = bodyScriptLines.length > 0\r\n ? '\\n' + bodyScriptLines.join('\\n') + '\\n'\r\n : '';\r\n\r\n // \u2500\u2500 Full document \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 const html = `<!DOCTYPE html>\r\n${openTag('html', store.htmlAttrs)}\r\n<head>\r\n${headLines.join('\\n')}\r\n</head>\r\n${openTag('body', store.bodyAttrs)}\r\n <div id=\"app\">${appHtml}</div>\r\n\r\n <script id=\"__n_data\" type=\"application/json\">${runtimeData}</script>\r\n\r\n <script type=\"importmap\">\r\n{\r\n \"imports\": {\r\n \"react\": \"/__react.js\",\r\n \"react-dom/client\": \"/__react.js\",\r\n \"react/jsx-runtime\": \"/__react.js\",\r\n \"nukejs\": \"/__n.js\"\r\n }\r\n}\r\n </script>\r\n\r\n <script type=\"module\">\r\n await import('react');\r\n const { initRuntime } = await import('nukejs');\r\n const data = JSON.parse(document.getElementById('__n_data').textContent);\r\n initRuntime(data);\r\n </script>\r\n\r\n ${isDev ? '<script type=\"module\" src=\"/__hmr.js\"></script>' : ''}\r\n${bodyScriptsHtml}</body>\r\n</html>`;\r\n\r\n res.setHeader('Content-Type', 'text/html');\r\n res.end(html);\r\n}"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
package/dist/use-html.js.map
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/use-html.ts"],
|
|
4
|
-
"sourcesContent": ["/**\r\n * use-html.ts \u2014 useHtml() Hook\r\n *\r\n * A universal hook that lets React components control the HTML document's\r\n * <head>, <html> attributes, and <body> attributes from within JSX \u2014 on both\r\n * the server (SSR) and the client (hydration / SPA navigation).\r\n *\r\n * Server behaviour:\r\n * Writes directly into the per-request html-store. The store is flushed\r\n * into the HTML document after the component tree is fully rendered.\r\n * useHtml() is called synchronously during rendering so no actual React\r\n * hook is used \u2014 it's just a function that pokes the globalThis store.\r\n *\r\n * Client behaviour:\r\n * Uses useEffect() to apply changes to the live document and clean them up\r\n * when the component unmounts (navigation, unmount). Each effect is keyed\r\n * to its options object via JSON.stringify so React re-runs it when the\r\n * options change.\r\n *\r\n * Layout title templates:\r\n * Layouts typically set title as a function so they can append a site suffix:\r\n *\r\n * ```tsx\r\n * // Root layout\r\n * useHtml({ title: (prev) => `${prev} | Acme` });\r\n *\r\n * // A page\r\n * useHtml({ title: 'About' });\r\n * // \u2192 'About | Acme'\r\n * ```\r\n *\r\n * Example usage:\r\n * ```tsx\r\n * useHtml({\r\n * title: 'Blog Post',\r\n * meta: [{ name: 'description', content: 'A great post' }],\r\n * link: [{ rel: 'canonical', href: 'https://example.com/post' }],\r\n * });\r\n * ```\r\n */\r\n\r\nimport { useEffect } from 'react';\r\nimport { getHtmlStore } from './html-store';\r\nimport type {\r\n TitleValue,\r\n HtmlAttrs,\r\n BodyAttrs,\r\n MetaTag,\r\n LinkTag,\r\n ScriptTag,\r\n StyleTag,\r\n} from './html-store';\r\n\r\n// Re-export types so consumers can import them from 'nukejs' directly.\r\nexport type { TitleValue, HtmlAttrs, BodyAttrs, MetaTag, LinkTag, ScriptTag, StyleTag };\r\n\r\n// \u2500\u2500\u2500 Options type \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 HtmlOptions {\r\n /**\r\n * Page title.\r\n * string \u2192 sets the title directly (page wins over layout).\r\n * function \u2192 receives the inner title; use in layouts to append a suffix:\r\n * `(prev) => \\`${prev} | MySite\\``\r\n */\r\n title?: TitleValue;\r\n /** Attributes merged onto <html>. Per-attribute last-write-wins. */\r\n htmlAttrs?: HtmlAttrs;\r\n /** Attributes merged onto <body>. Per-attribute last-write-wins. */\r\n bodyAttrs?: BodyAttrs;\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 Universal hook \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Applies HTML document customisations from a React component.\r\n * Automatically detects whether it is running on the server or the client.\r\n */\r\nexport function useHtml(options: HtmlOptions): void {\r\n if (typeof document === 'undefined') {\r\n // Running on the server (SSR) \u2014 write synchronously to the request store.\r\n serverUseHtml(options);\r\n } else {\r\n // Running in the browser \u2014 use React effects.\r\n clientUseHtml(options);\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Server implementation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Writes options directly into the active per-request html-store.\r\n * Called synchronously during SSR; no React hooks are used.\r\n *\r\n * Title operations are *pushed* (not replaced) so both layout and page values\r\n * are preserved for resolveTitle() to process in the correct order.\r\n */\r\nfunction serverUseHtml(options: HtmlOptions): void {\r\n const store = getHtmlStore();\r\n if (!store) return; // Called outside of a runWithHtmlStore context \u2014 ignore.\r\n\r\n if (options.title !== undefined) store.titleOps.push(options.title);\r\n if (options.htmlAttrs) Object.assign(store.htmlAttrs, options.htmlAttrs);\r\n if (options.bodyAttrs) Object.assign(store.bodyAttrs, options.bodyAttrs);\r\n if (options.meta?.length) store.meta.push(...options.meta);\r\n if (options.link?.length) store.link.push(...options.link);\r\n if (options.script?.length) store.script.push(...options.script);\r\n if (options.style?.length) store.style.push(...options.style);\r\n}\r\n\r\n// \u2500\u2500\u2500 Client implementation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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/** Monotonically incrementing counter for generating unique dataset IDs. */\r\nlet _uid = 0;\r\nconst uid = () => `uh${++_uid}`;\r\n\r\n/**\r\n * Applies options to the live document using React effects.\r\n * Each effect type is independent so a change to `title` does not re-run the\r\n * `meta` effect and vice versa.\r\n *\r\n * Cleanup functions restore the previous state so unmounting a component that\r\n * called useHtml() reverses its changes (important for SPA navigation).\r\n */\r\nfunction clientUseHtml(options: HtmlOptions): void {\r\n // \u2500\u2500 title \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 useEffect(() => { // eslint-disable-line react-hooks/rules-of-hooks\r\n if (options.title === undefined) return;\r\n const prev = document.title;\r\n document.title = typeof options.title === 'function'\r\n ? options.title(prev)\r\n : options.title;\r\n return () => { document.title = prev; };\r\n }, [typeof options.title === 'function' // eslint-disable-line react-hooks/exhaustive-deps\r\n ? options.title.toString()\r\n : options.title]);\r\n\r\n // \u2500\u2500 <html> attributes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 useEffect(() => { // eslint-disable-line react-hooks/rules-of-hooks\r\n if (!options.htmlAttrs) return;\r\n return applyAttrs(document.documentElement, options.htmlAttrs);\r\n }, [JSON.stringify(options.htmlAttrs)]); // eslint-disable-line react-hooks/exhaustive-deps\r\n\r\n // \u2500\u2500 <body> attributes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 useEffect(() => { // eslint-disable-line react-hooks/rules-of-hooks\r\n if (!options.bodyAttrs) return;\r\n return applyAttrs(document.body, options.bodyAttrs);\r\n }, [JSON.stringify(options.bodyAttrs)]); // eslint-disable-line react-hooks/exhaustive-deps\r\n\r\n // \u2500\u2500 <meta> tags \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 useEffect(() => { // eslint-disable-line react-hooks/rules-of-hooks\r\n if (!options.meta?.length) return;\r\n const id = uid();\r\n const nodes = options.meta.map((tag) => {\r\n const el = document.createElement('meta');\r\n for (const [k, v] of Object.entries(tag)) {\r\n if (v !== undefined) el.setAttribute(domAttr(k), v);\r\n }\r\n el.dataset.usehtml = id;\r\n document.head.appendChild(el);\r\n return el;\r\n });\r\n return () => nodes.forEach(n => n.remove());\r\n }, [JSON.stringify(options.meta)]); // eslint-disable-line react-hooks/exhaustive-deps\r\n\r\n // \u2500\u2500 <link> tags \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 useEffect(() => { // eslint-disable-line react-hooks/rules-of-hooks\r\n if (!options.link?.length) return;\r\n const id = uid();\r\n const nodes = options.link.map((tag) => {\r\n const el = document.createElement('link');\r\n for (const [k, v] of Object.entries(tag)) {\r\n if (v !== undefined) el.setAttribute(domAttr(k), v);\r\n }\r\n el.dataset.usehtml = id;\r\n document.head.appendChild(el);\r\n return el;\r\n });\r\n return () => nodes.forEach(n => n.remove());\r\n }, [JSON.stringify(options.link)]); // eslint-disable-line react-hooks/exhaustive-deps\r\n\r\n // \u2500\u2500 <script> tags \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 useEffect(() => { // eslint-disable-line react-hooks/rules-of-hooks\r\n if (!options.script?.length) return;\r\n const id = uid();\r\n const nodes = options.script.map((tag) => {\r\n const el = document.createElement('script');\r\n if (tag.src) el.src = tag.src;\r\n if (tag.type) el.type = tag.type;\r\n if (tag.defer) el.defer = true;\r\n if (tag.async) el.async = true;\r\n if (tag.noModule) el.setAttribute('nomodule', '');\r\n if (tag.crossOrigin) el.crossOrigin = tag.crossOrigin;\r\n if (tag.integrity) el.integrity = tag.integrity;\r\n if (tag.content) el.textContent = tag.content;\r\n el.dataset.usehtml = id;\r\n // Respect position: 'body' scripts are appended at the end of <body>.\r\n if (tag.position === 'body') {\r\n document.body.appendChild(el);\r\n } else {\r\n document.head.appendChild(el);\r\n }\r\n return el;\r\n });\r\n return () => nodes.forEach(n => n.remove());\r\n }, [JSON.stringify(options.script)]); // eslint-disable-line react-hooks/exhaustive-deps\r\n\r\n // \u2500\u2500 <style> tags \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 useEffect(() => { // eslint-disable-line react-hooks/rules-of-hooks\r\n if (!options.style?.length) return;\r\n const id = uid();\r\n const nodes = options.style.map((tag) => {\r\n const el = document.createElement('style');\r\n if (tag.media) el.media = tag.media;\r\n if (tag.content) el.textContent = tag.content;\r\n el.dataset.usehtml = id;\r\n document.head.appendChild(el);\r\n return el;\r\n });\r\n return () => nodes.forEach(n => n.remove());\r\n }, [JSON.stringify(options.style)]); // eslint-disable-line react-hooks/exhaustive-deps\r\n}\r\n\r\n// \u2500\u2500\u2500 Attribute helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Applies an attribute map to a DOM element, storing the previous values so\r\n * the returned cleanup function can restore them on unmount.\r\n */\r\nfunction applyAttrs(\r\n el: Element,\r\n attrs: Record<string, string | undefined>,\r\n): () => void {\r\n const prev: Record<string, string | null> = {};\r\n for (const [k, v] of Object.entries(attrs)) {\r\n if (v === undefined) continue;\r\n const attr = domAttr(k);\r\n prev[attr] = el.getAttribute(attr);\r\n el.setAttribute(attr, v);\r\n }\r\n return () => {\r\n for (const [attr, was] of Object.entries(prev)) {\r\n if (was === null) el.removeAttribute(attr);\r\n else el.setAttribute(attr, was);\r\n }\r\n };\r\n}\r\n\r\n/**\r\n * Converts camelCase React prop names to their HTML attribute equivalents.\r\n * httpEquiv \u2192 http-equiv\r\n * hrefLang \u2192 hreflang\r\n * crossOrigin \u2192 crossorigin\r\n */\r\nfunction domAttr(key: string): string {\r\n if (key === 'httpEquiv') return 'http-equiv';\r\n if (key === 'hrefLang') return 'hreflang';\r\n if (key === 'crossOrigin') return 'crossorigin';\r\n return key;\r\n}"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
package/dist/use-request.js.map
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/use-request.ts"],
|
|
4
|
-
"sourcesContent": ["/**\r\n * use-request.ts \u2014 useRequest() Hook\r\n *\r\n * Universal hook that exposes the current request's URL parameters, query\r\n * string, and headers to any React component \u2014 server or client, dev or prod.\r\n *\r\n * \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\r\n * \u2502 Environment \u2502 Data source \u2502\r\n * \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\r\n * \u2502 SSR (server) \u2502 request-store, populated by ssr.ts before rendering \u2502\r\n * \u2502 Client \u2502 __n_data JSON blob + window.location (reactive) \u2502\r\n * \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\r\n *\r\n * The hook stays reactive on the client: it listens to 'locationchange' events\r\n * fired by NukeJS's SPA router so values update on soft navigation without a\r\n * full page reload.\r\n *\r\n * --- Usage ---\r\n *\r\n * Basic:\r\n * ```tsx\r\n * // Works in server components (SSR) and client components (\"use client\")\r\n * const { params, query, headers, pathname } = useRequest();\r\n * const slug = params.slug as string;\r\n * const lang = query.lang as string;\r\n * const locale = headers['accept-language'];\r\n * ```\r\n *\r\n * Building useI18n on top:\r\n * ```tsx\r\n * // hooks/useI18n.ts\r\n * import { useRequest } from 'nukejs';\r\n *\r\n * const translations = {\r\n * en: { welcome: 'Welcome' },\r\n * fr: { welcome: 'Bienvenue' },\r\n * } as const;\r\n * type Locale = keyof typeof translations;\r\n *\r\n * function parseLocale(header = ''): Locale {\r\n * const tag = header.split(',')[0]?.split('-')[0]?.trim().toLowerCase();\r\n * return (tag in translations ? tag : 'en') as Locale;\r\n * }\r\n *\r\n * export function useI18n() {\r\n * const { query, headers } = useRequest();\r\n * // ?lang=fr wins over Accept-Language header\r\n * const locale = ((query.lang as string) ?? parseLocale(headers['accept-language'])) as Locale;\r\n * return { t: translations[locale] ?? translations.en, locale };\r\n * }\r\n *\r\n * // Page.tsx\r\n * const { t } = useI18n();\r\n * return <h1>{t.welcome}</h1>;\r\n * ```\r\n *\r\n * --- Notes ---\r\n * - `headers` on the client never contains `cookie`, `authorization`, or\r\n * `proxy-authorization` \u2014 these are stripped by the SSR pipeline before\r\n * embedding in __n_data. See request-store.ts for the full exclusion list.\r\n * - In a \"use client\" component, `params` always reflects the __n_data blob\r\n * written at the time of the most recent SSR/navigation. For the freshest\r\n * pathname use `useRouter().path` instead.\r\n */\r\n\r\nimport { useState, useEffect } from 'react';\r\nimport { getRequestStore } from './request-store';\r\nimport type { RequestContext } from './request-store';\r\n\r\nexport type { RequestContext };\r\n\r\n// \u2500\u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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/** Fallback context used when data is unavailable. */\r\nconst EMPTY_CTX: RequestContext = {\r\n url: '',\r\n pathname: '',\r\n params: {},\r\n query: {},\r\n headers: {},\r\n};\r\n\r\n/**\r\n * Reads the current request context from the `__n_data` script tag embedded\r\n * by the SSR renderer, merged with `window.location` for live accuracy.\r\n *\r\n * Called on initial render and on every 'locationchange' event so the hook\r\n * stays fresh across SPA navigation.\r\n */\r\nfunction readClientContext(): RequestContext {\r\n try {\r\n // __n_data is a JSON blob with { url, params, query, headers, \u2026 }.\r\n const raw = document.getElementById('__n_data')?.textContent ?? '{}';\r\n const data = JSON.parse(raw) as Partial<RequestContext & { params: Record<string, any> }>;\r\n\r\n // Always re-parse the query string from the live URL so navigation\r\n // to ?lang=fr is reflected immediately without waiting for a new SSR.\r\n const search = window.location.search;\r\n const query: Record<string, string | string[]> = {};\r\n if (search) {\r\n const sp = new URLSearchParams(search);\r\n sp.forEach((_, k) => {\r\n const all = sp.getAll(k);\r\n query[k] = all.length > 1 ? all : all[0];\r\n });\r\n }\r\n\r\n return {\r\n url: window.location.pathname + window.location.search,\r\n pathname: window.location.pathname,\r\n params: data.params ?? {},\r\n query,\r\n headers: data.headers ?? {},\r\n };\r\n } catch {\r\n return EMPTY_CTX;\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Universal hook \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 the current request context: URL params, query string, and headers.\r\n *\r\n * Automatically detects SSR vs browser and returns the correct data for\r\n * each environment. On the client it is reactive \u2014 values update on SPA\r\n * navigation without a page reload.\r\n */\r\nexport function useRequest(): RequestContext {\r\n // \u2500\u2500 Server path (SSR) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 // typeof document === 'undefined' is the standard SSR guard in NukeJS\r\n // (mirrors the pattern used in use-html.ts and use-router.ts).\r\n if (typeof document === 'undefined') {\r\n return getRequestStore() ?? EMPTY_CTX;\r\n }\r\n\r\n // \u2500\u2500 Client path (browser) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 // eslint-disable-next-line react-hooks/rules-of-hooks\r\n const [ctx, setCtx] = useState<RequestContext>(readClientContext);\r\n\r\n // eslint-disable-next-line react-hooks/rules-of-hooks\r\n useEffect(() => {\r\n // 'locationchange' is fired by NukeJS's history patch (setupLocationChangeMonitor)\r\n // on every pushState / replaceState / popstate so this handler covers both\r\n // Link-driven navigation and programmatic useRouter().push() calls.\r\n const handleLocationChange = () => setCtx(readClientContext());\r\n\r\n window.addEventListener('locationchange', handleLocationChange);\r\n return () => window.removeEventListener('locationchange', handleLocationChange);\r\n }, []);\r\n\r\n return ctx;\r\n}"],
|
|
5
|
-
"mappings": "AAiEA,SAAS,UAAU,iBAAiB;AACpC,SAAS,uBAAuB;AAQhC,MAAM,YAA4B;AAAA,EAChC,KAAU;AAAA,EACV,UAAU;AAAA,EACV,QAAU,CAAC;AAAA,EACX,OAAU,CAAC;AAAA,EACX,SAAU,CAAC;AACb;AASA,SAAS,oBAAoC;AAC3C,MAAI;AAEF,UAAM,MAAQ,SAAS,eAAe,UAAU,GAAG,eAAe;AAClE,UAAM,OAAQ,KAAK,MAAM,GAAG;AAI5B,UAAM,SAAS,OAAO,SAAS;AAC/B,UAAM,QAA2C,CAAC;AAClD,QAAI,QAAQ;AACV,YAAM,KAAK,IAAI,gBAAgB,MAAM;AACrC,SAAG,QAAQ,CAAC,GAAG,MAAM;AACnB,cAAM,MAAM,GAAG,OAAO,CAAC;AACvB,cAAM,CAAC,IAAI,IAAI,SAAS,IAAI,MAAM,IAAI,CAAC;AAAA,MACzC,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,MACL,KAAU,OAAO,SAAS,WAAW,OAAO,SAAS;AAAA,MACrD,UAAU,OAAO,SAAS;AAAA,MAC1B,QAAU,KAAK,UAAW,CAAC;AAAA,MAC3B;AAAA,MACA,SAAU,KAAK,WAAW,CAAC;AAAA,IAC7B;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAWO,SAAS,aAA6B;AAI3C,MAAI,OAAO,aAAa,aAAa;AACnC,WAAO,gBAAgB,KAAK;AAAA,EAC9B;AAIA,QAAM,CAAC,KAAK,MAAM,IAAI,SAAyB,iBAAiB;AAGhE,YAAU,MAAM;AAId,UAAM,uBAAuB,MAAM,OAAO,kBAAkB,CAAC;AAE7D,WAAO,iBAAiB,kBAAkB,oBAAoB;AAC9D,WAAO,MAAM,OAAO,oBAAoB,kBAAkB,oBAAoB;AAAA,EAChF,GAAG,CAAC,CAAC;AAEL,SAAO;AACT;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
package/dist/use-router.js.map
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/use-router.ts"],
|
|
4
|
-
"sourcesContent": ["import { useCallback, useEffect, useState } from \"react\";\r\n\r\ntype Router = {\r\n path: string;\r\n push: (url: string) => void;\r\n replace: (url: string) => void;\r\n};\r\n\r\nexport default function useRouter(): Router {\r\n try {\r\n const [path, setPath] = useState(() => window.location.pathname);\r\n\r\n useEffect(() => {\r\n const handleLocationChange = () => setPath(window.location.pathname);\r\n window.addEventListener(\"locationchange\", handleLocationChange);\r\n return () => window.removeEventListener(\"locationchange\", handleLocationChange);\r\n }, []);\r\n\r\n const push = useCallback((url: string) => {\r\n window.history.pushState({}, \"\", url);\r\n setPath(url);\r\n }, []);\r\n\r\n const replace = useCallback((url: string) => {\r\n window.history.replaceState({}, \"\", url);\r\n setPath(url);\r\n }, []);\r\n\r\n return { path, push, replace };\r\n } catch {\r\n return { push: () => {}, replace: () => {}, path: \"\" };\r\n }\r\n}"],
|
|
5
|
-
"mappings": "AAAA,SAAS,aAAa,WAAW,gBAAgB;AAQlC,SAAR,YAAqC;AACxC,MAAI;AACA,UAAM,CAAC,MAAM,OAAO,IAAI,SAAS,MAAM,OAAO,SAAS,QAAQ;AAE/D,cAAU,MAAM;AACZ,YAAM,uBAAuB,MAAM,QAAQ,OAAO,SAAS,QAAQ;AACnE,aAAO,iBAAiB,kBAAkB,oBAAoB;AAC9D,aAAO,MAAM,OAAO,oBAAoB,kBAAkB,oBAAoB;AAAA,IAClF,GAAG,CAAC,CAAC;AAEL,UAAM,OAAO,YAAY,CAAC,QAAgB;AACtC,aAAO,QAAQ,UAAU,CAAC,GAAG,IAAI,GAAG;AACpC,cAAQ,GAAG;AAAA,IACf,GAAG,CAAC,CAAC;AAEL,UAAM,UAAU,YAAY,CAAC,QAAgB;AACzC,aAAO,QAAQ,aAAa,CAAC,GAAG,IAAI,GAAG;AACvC,cAAQ,GAAG;AAAA,IACf,GAAG,CAAC,CAAC;AAEL,WAAO,EAAE,MAAM,MAAM,QAAQ;AAAA,EACjC,QAAQ;AACJ,WAAO,EAAE,MAAM,MAAM;AAAA,IAAC,GAAG,SAAS,MAAM;AAAA,IAAC,GAAG,MAAM,GAAG;AAAA,EACzD;AACJ;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
package/dist/utils.js.map
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/utils.ts"],
|
|
4
|
-
"sourcesContent": ["/**\r\n * utils.ts \u2014 Shared Utility Functions\r\n *\r\n * Small, dependency-free helpers used across both server and client code.\r\n */\r\n\r\n/**\r\n * Escapes a string for safe inclusion in HTML content or attribute values.\r\n *\r\n * Replaces the five characters that have special meaning in HTML:\r\n * & \u2192 & (must come first to avoid double-escaping)\r\n * < \u2192 <\r\n * > \u2192 >\r\n * \" \u2192 "\r\n * ' \u2192 '\r\n */\r\nexport function escapeHtml(str: string): string {\r\n return str\r\n .replace(/&/g, '&')\r\n .replace(/</g, '<')\r\n .replace(/>/g, '>')\r\n .replace(/\"/g, '"')\r\n .replace(/'/g, ''');\r\n}\r\n\r\n/**\r\n * Returns the correct Content-Type header value for a given file extension.\r\n *\r\n * Covers the full range of file types that are realistic in a public/ directory:\r\n * scripts, styles, images, fonts, media, documents, and data formats.\r\n *\r\n * Falls back to 'application/octet-stream' for unknown extensions so the\r\n * browser downloads rather than tries to render unknown binary content.\r\n */\r\nexport function getMimeType(ext: string): string {\r\n const map: Record<string, string> = {\r\n // \u2500\u2500 Web \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 '.html': 'text/html; charset=utf-8',\r\n '.htm': 'text/html; charset=utf-8',\r\n '.css': 'text/css; charset=utf-8',\r\n '.js': 'application/javascript; charset=utf-8',\r\n '.mjs': 'application/javascript; charset=utf-8',\r\n '.cjs': 'application/javascript; charset=utf-8',\r\n '.map': 'application/json; charset=utf-8',\r\n\r\n // \u2500\u2500 Data \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 '.json': 'application/json; charset=utf-8',\r\n '.xml': 'application/xml; charset=utf-8',\r\n '.txt': 'text/plain; charset=utf-8',\r\n '.csv': 'text/csv; charset=utf-8',\r\n\r\n // \u2500\u2500 Images \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 '.png': 'image/png',\r\n '.jpg': 'image/jpeg',\r\n '.jpeg': 'image/jpeg',\r\n '.gif': 'image/gif',\r\n '.webp': 'image/webp',\r\n '.avif': 'image/avif',\r\n '.svg': 'image/svg+xml',\r\n '.ico': 'image/x-icon',\r\n '.bmp': 'image/bmp',\r\n '.tiff': 'image/tiff',\r\n '.tif': 'image/tiff',\r\n\r\n // \u2500\u2500 Fonts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 '.woff': 'font/woff',\r\n '.woff2': 'font/woff2',\r\n '.ttf': 'font/ttf',\r\n '.otf': 'font/otf',\r\n '.eot': 'application/vnd.ms-fontobject',\r\n\r\n // \u2500\u2500 Video \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 '.mp4': 'video/mp4',\r\n '.webm': 'video/webm',\r\n '.ogv': 'video/ogg',\r\n '.mov': 'video/quicktime',\r\n '.avi': 'video/x-msvideo',\r\n\r\n // \u2500\u2500 Audio \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 '.mp3': 'audio/mpeg',\r\n '.wav': 'audio/wav',\r\n '.ogg': 'audio/ogg',\r\n '.flac': 'audio/flac',\r\n '.aac': 'audio/aac',\r\n\r\n // \u2500\u2500 Documents / archives \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 '.pdf': 'application/pdf',\r\n '.zip': 'application/zip',\r\n '.gz': 'application/gzip',\r\n '.tar': 'application/x-tar',\r\n '.wasm': 'application/wasm',\r\n };\r\n return map[ext.toLowerCase()] ?? 'application/octet-stream';\r\n}"],
|
|
5
|
-
"mappings": "AAgBO,SAAS,WAAW,KAAqB;AAC9C,SAAO,IACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,QAAQ;AAC3B;AAWO,SAAS,YAAY,KAAqB;AAC/C,QAAM,MAA8B;AAAA;AAAA,IAElC,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ;AAAA;AAAA,IAGR,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ;AAAA;AAAA,IAGR,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,QAAQ;AAAA;AAAA,IAGR,SAAS;AAAA,IACT,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ;AAAA;AAAA,IAGR,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ;AAAA;AAAA,IAGR,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,QAAQ;AAAA;AAAA,IAGR,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,SAAS;AAAA,EACX;AACA,SAAO,IAAI,IAAI,YAAY,CAAC,KAAK;AACnC;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|