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.
Files changed (77) hide show
  1. package/README.md +147 -0
  2. package/dist/Link.js +0 -1
  3. package/dist/app.js +0 -1
  4. package/dist/build-common.js +64 -14
  5. package/dist/build-node.js +63 -5
  6. package/dist/build-vercel.js +76 -9
  7. package/dist/builder.js +32 -4
  8. package/dist/bundle.js +47 -4
  9. package/dist/bundler.js +0 -1
  10. package/dist/component-analyzer.js +0 -1
  11. package/dist/config.js +0 -1
  12. package/dist/hmr-bundle.js +10 -1
  13. package/dist/hmr.js +7 -1
  14. package/dist/html-store.js +0 -1
  15. package/dist/http-server.js +0 -1
  16. package/dist/index.d.ts +2 -1
  17. package/dist/index.js +0 -1
  18. package/dist/logger.js +0 -1
  19. package/dist/metadata.js +0 -1
  20. package/dist/middleware-loader.js +0 -1
  21. package/dist/middleware.example.js +0 -1
  22. package/dist/middleware.js +0 -1
  23. package/dist/renderer.js +3 -9
  24. package/dist/request-store.js +0 -1
  25. package/dist/router.js +0 -1
  26. package/dist/ssr.js +73 -16
  27. package/dist/use-html.js +0 -1
  28. package/dist/use-request.js +0 -1
  29. package/dist/use-router.js +0 -1
  30. package/dist/utils.js +0 -1
  31. package/package.json +1 -1
  32. package/dist/Link.js.map +0 -7
  33. package/dist/app.d.ts +0 -19
  34. package/dist/app.js.map +0 -7
  35. package/dist/build-common.d.ts +0 -178
  36. package/dist/build-common.js.map +0 -7
  37. package/dist/build-node.d.ts +0 -15
  38. package/dist/build-node.js.map +0 -7
  39. package/dist/build-vercel.d.ts +0 -19
  40. package/dist/build-vercel.js.map +0 -7
  41. package/dist/builder.d.ts +0 -11
  42. package/dist/builder.js.map +0 -7
  43. package/dist/bundle.js.map +0 -7
  44. package/dist/bundler.d.ts +0 -58
  45. package/dist/bundler.js.map +0 -7
  46. package/dist/component-analyzer.d.ts +0 -75
  47. package/dist/component-analyzer.js.map +0 -7
  48. package/dist/config.d.ts +0 -35
  49. package/dist/config.js.map +0 -7
  50. package/dist/hmr-bundle.d.ts +0 -25
  51. package/dist/hmr-bundle.js.map +0 -7
  52. package/dist/hmr.d.ts +0 -55
  53. package/dist/hmr.js.map +0 -7
  54. package/dist/html-store.js.map +0 -7
  55. package/dist/http-server.d.ts +0 -92
  56. package/dist/http-server.js.map +0 -7
  57. package/dist/index.js.map +0 -7
  58. package/dist/logger.js.map +0 -7
  59. package/dist/metadata.d.ts +0 -51
  60. package/dist/metadata.js.map +0 -7
  61. package/dist/middleware-loader.d.ts +0 -50
  62. package/dist/middleware-loader.js.map +0 -7
  63. package/dist/middleware.d.ts +0 -22
  64. package/dist/middleware.example.d.ts +0 -8
  65. package/dist/middleware.example.js.map +0 -7
  66. package/dist/middleware.js.map +0 -7
  67. package/dist/renderer.d.ts +0 -44
  68. package/dist/renderer.js.map +0 -7
  69. package/dist/request-store.js.map +0 -7
  70. package/dist/router.d.ts +0 -92
  71. package/dist/router.js.map +0 -7
  72. package/dist/ssr.d.ts +0 -46
  73. package/dist/ssr.js.map +0 -7
  74. package/dist/use-html.js.map +0 -7
  75. package/dist/use-request.js.map +0 -7
  76. package/dist/use-router.js.map +0 -7
  77. package/dist/utils.js.map +0 -7
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/component-analyzer.ts"],
4
- "sourcesContent": ["/**\r\n * component-analyzer.ts \u2014 Static Import Analyzer & Client Component Registry\r\n *\r\n * This module solves a core problem in NukeJS's partial hydration model:\r\n * the server needs to know *at render time* which components in a page's\r\n * import tree are \"use client\" boundaries so it can:\r\n *\r\n * 1. Emit <span data-hydrate-id=\"\u2026\"> markers instead of rendering them.\r\n * 2. Inject the matching bundle URLs into the page's runtime data blob.\r\n * 3. Serialize the props passed to those components so the browser can\r\n * reconstruct them after loading the bundle.\r\n *\r\n * How it works:\r\n * - analyzeComponent() checks whether a file starts with \"use client\"\r\n * and assigns a stable content-hash ID if it does.\r\n * - extractImports() parses `import \u2026 from '\u2026'` statements with a\r\n * regex and resolves relative/absolute paths.\r\n * - findClientComponentsInTree() recursively walks the import graph, stopping\r\n * at client boundaries (they own their subtree).\r\n *\r\n * Results are memoised in `componentCache` (process-lifetime) so repeated SSR\r\n * renders don't re-read and re-hash files they've already seen.\r\n *\r\n * ID scheme:\r\n * The ID for a client component is `cc_` + the first 8 hex chars of the MD5\r\n * hash of its path relative to pagesDir. This is stable across restarts and\r\n * matches what the browser will request from /__client-component/<id>.js.\r\n */\r\n\r\nimport path from 'path';\r\nimport fs from 'fs';\r\nimport { createHash } from 'node:crypto';\r\nimport { fileURLToPath } from 'url';\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 ComponentInfo {\r\n filePath: string;\r\n /** True when the file's first non-comment line is \"use client\". */\r\n isClientComponent: boolean;\r\n /** Stable hash-based ID, present only for client components. */\r\n clientComponentId?: string;\r\n /**\r\n * The name of the default-exported component function.\r\n * Handles both source format (`export default Link`) and esbuild's compiled\r\n * format (`var Link_default = Link; export { Link_default as default }`).\r\n */\r\n exportedName?: string;\r\n}\r\n\r\n// \u2500\u2500\u2500 In-process cache \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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// Memoises analyze results for the lifetime of the dev server process.\r\n// In production builds the analysis runs once per build, so no cache is needed.\r\nconst componentCache = new Map<string, ComponentInfo>();\r\n\r\n// \u2500\u2500\u2500 Client boundary detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 when a file begins with a `\"use client\"` or `'use client'`\r\n * directive (ignoring blank lines and line/block comment prefixes).\r\n *\r\n * Only the first five lines are checked \u2014 the directive must appear before\r\n * any executable code.\r\n */\r\nfunction isClientComponent(filePath: string): boolean {\r\n const content = fs.readFileSync(filePath, 'utf-8');\r\n for (const line of content.split('\\n').slice(0, 5)) {\r\n const trimmed = line.trim();\r\n if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('/*')) continue;\r\n if (/^[\"']use client[\"'];?$/.test(trimmed)) return true;\r\n break; // First substantive line is not \"use client\"\r\n }\r\n return false;\r\n}\r\n\r\n/**\r\n * Generates a deterministic, short ID for a client component.\r\n * The path is made relative to pagesDir before hashing so the ID is\r\n * portable across machines (absolute paths differ per developer).\r\n */\r\nfunction getClientComponentId(filePath: string, pagesDir: string): string {\r\n return 'cc_' + createHash('md5')\r\n .update(path.relative(pagesDir, filePath))\r\n .digest('hex')\r\n .substring(0, 8);\r\n}\r\n\r\n// \u2500\u2500\u2500 Default export name extraction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Extracts the name of the default-exported function from a component file.\r\n *\r\n * Handles three formats:\r\n * 1. Source: `export default function Link(\u2026)` or `export default Link`\r\n * 2. esbuild: `var Link_default = Link;` (compiled arrow-function component)\r\n * 3. Re-export: `export { Link as default }`\r\n */\r\nfunction getExportedDefaultName(filePath: string): string | undefined {\r\n const content = fs.readFileSync(filePath, 'utf-8');\r\n\r\n // Format 1 \u2013 source files: `export default function Foo` or `export default Foo`\r\n let m = content.match(/export\\s+default\\s+(?:function\\s+)?(\\w+)/);\r\n if (m?.[1]) return m[1];\r\n\r\n // Format 2 \u2013 esbuild compiled arrow components: `var Foo_default = Foo`\r\n // esbuild renames the variable to `<name>_default` and keeps the original name.\r\n m = content.match(/var\\s+\\w+_default\\s*=\\s*(\\w+)/);\r\n if (m?.[1]) return m[1];\r\n\r\n // Format 3 \u2013 explicit re-export: `export { Foo as default }`\r\n m = content.match(/export\\s*\\{[^}]*\\b(\\w+)\\s+as\\s+default\\b[^}]*\\}/);\r\n if (m?.[1] && !m[1].endsWith('_default')) return m[1];\r\n\r\n return undefined;\r\n}\r\n\r\n// \u2500\u2500\u2500 Analysis \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Analyses a component file and returns cached results on subsequent calls.\r\n *\r\n * @param filePath Absolute path to the source file.\r\n * @param pagesDir Absolute path to the pages root (used for ID generation).\r\n */\r\nexport function analyzeComponent(filePath: string, pagesDir: string): ComponentInfo {\r\n if (componentCache.has(filePath)) return componentCache.get(filePath)!;\r\n\r\n const isClient = isClientComponent(filePath);\r\n const info: ComponentInfo = {\r\n filePath,\r\n isClientComponent: isClient,\r\n clientComponentId: isClient ? getClientComponentId(filePath, pagesDir) : undefined,\r\n exportedName: isClient ? getExportedDefaultName(filePath) : undefined,\r\n };\r\n\r\n componentCache.set(filePath, info);\r\n return info;\r\n}\r\n\r\n// \u2500\u2500\u2500 Import extraction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Parses `import \u2026 from '\u2026'` and `export \u2026 from '\u2026'` statements in a file\r\n * and returns a list of resolved absolute paths for all *local* imports.\r\n *\r\n * Non-local specifiers (npm packages) are skipped, except `nukejs` itself \u2014\r\n * which is resolved to our own index file so built-in \"use client\" components\r\n * like `<Link>` are included in the client component discovery walk.\r\n *\r\n * Extensions are tried in priority order if the specifier has none.\r\n */\r\nfunction extractImports(filePath: string): string[] {\r\n const content = fs.readFileSync(filePath, 'utf-8');\r\n const dir = path.dirname(filePath);\r\n const imports: string[] = [];\r\n\r\n const importRegex =\r\n /(?:import|export)\\s+(?:(?:\\{[^}]*\\}|\\*\\s+as\\s+\\w+|\\w+)\\s+from\\s+)?['\"]([^'\"]+)['\"]/g;\r\n let match: RegExpExecArray | null;\r\n\r\n while ((match = importRegex.exec(content)) !== null) {\r\n const spec = match[1];\r\n\r\n // Special case: resolve the 'nukejs' package to our own source so\r\n // built-in \"use client\" exports (Link, useRouter, etc.) are discovered.\r\n if (spec === 'nukejs') {\r\n const selfDir = path.dirname(fileURLToPath(import.meta.url));\r\n for (const candidate of [\r\n path.join(selfDir, 'index.ts'),\r\n path.join(selfDir, 'index.js'),\r\n ]) {\r\n if (fs.existsSync(candidate)) { imports.push(candidate); break; }\r\n }\r\n continue;\r\n }\r\n\r\n // Skip npm packages and other non-local specifiers.\r\n if (!spec.startsWith('.') && !spec.startsWith('/')) continue;\r\n\r\n // Resolve to an absolute path, trying extensions if needed.\r\n let resolved = path.resolve(dir, spec);\r\n const EXTS = ['.tsx', '.ts', '.jsx', '.js'] as const;\r\n const isFile = (p: string) => fs.existsSync(p) && fs.statSync(p).isFile();\r\n\r\n if (!isFile(resolved)) {\r\n let found = false;\r\n\r\n // 1. Try appending an extension (./Button \u2192 ./Button.tsx)\r\n for (const ext of EXTS) {\r\n if (isFile(resolved + ext)) { resolved += ext; found = true; break; }\r\n }\r\n\r\n // 2. Try an index file inside the directory (./components \u2192 ./components/index.tsx)\r\n if (!found && fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {\r\n for (const ext of EXTS) {\r\n const candidate = path.join(resolved, `index${ext}`);\r\n if (isFile(candidate)) { resolved = candidate; found = true; break; }\r\n }\r\n }\r\n\r\n if (!found) continue; // Unresolvable \u2014 skip silently\r\n }\r\n\r\n imports.push(resolved);\r\n }\r\n\r\n return imports;\r\n}\r\n\r\n// \u2500\u2500\u2500 Tree walk \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 walks the import graph from `filePath`, collecting every\r\n * \"use client\" file encountered.\r\n *\r\n * The walk stops at client boundaries: a \"use client\" file is recorded and\r\n * its own imports are NOT walked (the client runtime handles their subtree).\r\n *\r\n * The `visited` set prevents infinite loops from circular imports.\r\n *\r\n * @returns Map<id, absoluteFilePath> for every client component reachable\r\n * from `filePath` (including `filePath` itself if it's a client).\r\n */\r\nexport function findClientComponentsInTree(\r\n filePath: string,\r\n pagesDir: string,\r\n visited = new Set<string>(),\r\n): Map<string, string> {\r\n const found = new Map<string, string>();\r\n if (visited.has(filePath)) return found;\r\n visited.add(filePath);\r\n\r\n const info = analyzeComponent(filePath, pagesDir);\r\n\r\n if (info.isClientComponent && info.clientComponentId) {\r\n found.set(info.clientComponentId, filePath);\r\n return found; // Stop \u2014 client boundary owns its subtree\r\n }\r\n\r\n for (const importPath of extractImports(filePath)) {\r\n for (const [id, p] of findClientComponentsInTree(importPath, pagesDir, visited)) {\r\n found.set(id, p);\r\n }\r\n }\r\n\r\n return found;\r\n}\r\n\r\n// \u2500\u2500\u2500 Cache access \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Looks up the absolute file path for a client component by its ID.\r\n * Returns undefined when the ID is not in the cache.\r\n */\r\nexport function getComponentById(id: string): string | undefined {\r\n for (const [filePath, info] of componentCache) {\r\n if (info.clientComponentId === id) return filePath;\r\n }\r\n return undefined;\r\n}\r\n\r\n/** Returns the live component cache (used by bundler.ts for ID\u2192path lookup). */\r\nexport function getComponentCache(): Map<string, ComponentInfo> {\r\n return componentCache;\r\n}\r\n\r\n/**\r\n * Removes a single file's analysis entry from the cache.\r\n * Call this whenever a source file changes in dev mode so the next render\r\n * re-analyses the file (picks up added/removed \"use client\" directives and\r\n * changed import graphs).\r\n */\r\nexport function invalidateComponentCache(filePath: string): void {\r\n componentCache.delete(filePath);\r\n}"],
5
- "mappings": "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",
6
- "names": []
7
- }
package/dist/config.d.ts DELETED
@@ -1,35 +0,0 @@
1
- /**
2
- * config.ts — NukeJS Configuration Loader
3
- *
4
- * Loads `nuke.config.ts` from the project root at startup.
5
- * If no config file exists, sensible defaults are returned so most projects
6
- * work with zero configuration.
7
- *
8
- * Config file example (nuke.config.ts):
9
- *
10
- * ```ts
11
- * export default {
12
- * serverDir: './server', // where API route files live
13
- * port: 3000, // HTTP port for the dev server
14
- * debug: 'info', // false | 'error' | 'info' | true (verbose)
15
- * };
16
- * ```
17
- */
18
- import type { DebugLevel } from './logger';
19
- export interface Config {
20
- /** Path to the API server directory, relative to project root. */
21
- serverDir: string;
22
- /** TCP port for the dev server. Increments automatically if in use. */
23
- port: number;
24
- /** Logging verbosity. false = silent, true = verbose. */
25
- debug?: DebugLevel;
26
- }
27
- /**
28
- * Dynamically imports `nuke.config.ts` from process.cwd() and merges it with
29
- * the defaults. Falls back to defaults silently when no config file exists.
30
- *
31
- * Throws if the config file exists but cannot be imported (syntax error, etc.)
32
- * so the developer sees the problem immediately rather than running on stale
33
- * defaults.
34
- */
35
- export declare function loadConfig(): Promise<Config>;
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/config.ts"],
4
- "sourcesContent": ["/**\r\n * config.ts \u2014 NukeJS Configuration Loader\r\n *\r\n * Loads `nuke.config.ts` from the project root at startup.\r\n * If no config file exists, sensible defaults are returned so most projects\r\n * work with zero configuration.\r\n *\r\n * Config file example (nuke.config.ts):\r\n *\r\n * ```ts\r\n * export default {\r\n * serverDir: './server', // where API route files live\r\n * port: 3000, // HTTP port for the dev server\r\n * debug: 'info', // false | 'error' | 'info' | true (verbose)\r\n * };\r\n * ```\r\n */\r\n\r\nimport path from 'path';\r\nimport fs from 'fs';\r\nimport { pathToFileURL } from 'url';\r\nimport { log } from './logger';\r\nimport type { DebugLevel } from './logger';\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 Config {\r\n /** Path to the API server directory, relative to project root. */\r\n serverDir: string;\r\n /** TCP port for the dev server. Increments automatically if in use. */\r\n port: number;\r\n /** Logging verbosity. false = silent, true = verbose. */\r\n debug?: DebugLevel;\r\n}\r\n\r\n// \u2500\u2500\u2500 Loader \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Dynamically imports `nuke.config.ts` from process.cwd() and merges it with\r\n * the defaults. Falls back to defaults silently when no config file exists.\r\n *\r\n * Throws if the config file exists but cannot be imported (syntax error, etc.)\r\n * so the developer sees the problem immediately rather than running on stale\r\n * defaults.\r\n */\r\nexport async function loadConfig(): Promise<Config> {\r\n const configPath = path.join(process.cwd(), 'nuke.config.ts');\r\n\r\n if (!fs.existsSync(configPath)) {\r\n // No config file \u2014 use defaults. This is expected for new projects.\r\n return {\r\n serverDir: './server',\r\n port: 3000,\r\n debug: false,\r\n };\r\n }\r\n\r\n try {\r\n // pathToFileURL ensures the import works correctly on Windows (absolute\r\n // paths with drive letters are not valid ESM specifiers without file://).\r\n const mod = await import(pathToFileURL(configPath).href);\r\n const config = mod.default;\r\n\r\n return {\r\n serverDir: config.serverDir || './server',\r\n port: config.port || 3000,\r\n debug: config.debug ?? false,\r\n };\r\n } catch (error) {\r\n log.error('Error loading config:', error);\r\n throw error;\r\n }\r\n}\r\n"],
5
- "mappings": "AAkBA,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,SAAS,qBAAqB;AAC9B,SAAS,WAAW;AAwBpB,eAAsB,aAA8B;AAClD,QAAM,aAAa,KAAK,KAAK,QAAQ,IAAI,GAAG,gBAAgB;AAE5D,MAAI,CAAC,GAAG,WAAW,UAAU,GAAG;AAE9B,WAAO;AAAA,MACL,WAAW;AAAA,MACX,MAAW;AAAA,MACX,OAAW;AAAA,IACb;AAAA,EACF;AAEA,MAAI;AAGF,UAAM,MAAS,MAAM,OAAO,cAAc,UAAU,EAAE;AACtD,UAAM,SAAS,IAAI;AAEnB,WAAO;AAAA,MACL,WAAW,OAAO,aAAa;AAAA,MAC/B,MAAW,OAAO,QAAa;AAAA,MAC/B,OAAW,OAAO,SAAS;AAAA,IAC7B;AAAA,EACF,SAAS,OAAO;AACd,QAAI,MAAM,yBAAyB,KAAK;AACxC,UAAM;AAAA,EACR;AACF;",
6
- "names": []
7
- }
@@ -1,25 +0,0 @@
1
- /**
2
- * hmr-bundle.ts — HMR Client Script
3
- *
4
- * This file is compiled on-demand by middleware.ts and served to the browser
5
- * as /__hmr.js (injected into every dev-mode page as a module script).
6
- *
7
- * It opens an EventSource connection to /__hmr and reacts to three message
8
- * types from the server:
9
- *
10
- * 'reload' — A page or stylesheet changed.
11
- * url === '*' → reload stylesheets in-place (no flicker)
12
- * url === window.location.pathname → soft-navigate the current page
13
- *
14
- * 'replace' — A component/utility changed.
15
- * Re-navigate the current page so SSR picks up the new code.
16
- *
17
- * 'restart' — The server is restarting (config or middleware changed).
18
- * Close the SSE connection and poll /__hmr_ping until the
19
- * server is back, then hard-reload the page.
20
- *
21
- * The same reconnect polling is used when the SSE connection drops unexpectedly
22
- * (e.g. the dev server crashed).
23
- */
24
- /** Opens the SSE connection and starts listening for HMR events. */
25
- export default function hmr(): void;
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/hmr-bundle.ts"],
4
- "sourcesContent": ["/**\r\n * hmr-bundle.ts \u2014 HMR Client Script\r\n *\r\n * This file is compiled on-demand by middleware.ts and served to the browser\r\n * as /__hmr.js (injected into every dev-mode page as a module script).\r\n *\r\n * It opens an EventSource connection to /__hmr and reacts to three message\r\n * types from the server:\r\n *\r\n * 'reload' \u2014 A page or stylesheet changed.\r\n * url === '*' \u2192 reload stylesheets in-place (no flicker)\r\n * url === window.location.pathname \u2192 soft-navigate the current page\r\n *\r\n * 'replace' \u2014 A component/utility changed.\r\n * Re-navigate the current page so SSR picks up the new code.\r\n *\r\n * 'restart' \u2014 The server is restarting (config or middleware changed).\r\n * Close the SSE connection and poll /__hmr_ping until the\r\n * server is back, then hard-reload the page.\r\n *\r\n * The same reconnect polling is used when the SSE connection drops unexpectedly\r\n * (e.g. the dev server crashed).\r\n */\r\n\r\nimport { log } from './logger';\r\n\r\n// \u2500\u2500\u2500 Entry point \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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/** Opens the SSE connection and starts listening for HMR events. */\r\nexport default function hmr(): void {\r\n const es = new EventSource('/__hmr');\r\n\r\n es.onopen = () => {\r\n log.info('[HMR] Connected');\r\n };\r\n\r\n es.onerror = () => {\r\n // Connection dropped without a restart message (e.g. crash or network\r\n // blip). Close cleanly and poll until the server is back.\r\n es.close();\r\n waitForReconnect();\r\n };\r\n\r\n es.onmessage = async (event) => {\r\n try {\r\n const msg = JSON.parse(event.data);\r\n\r\n if (msg.type === 'restart') {\r\n log.info('[HMR] Server restarting \u2014 waiting to reconnect...');\r\n es.close();\r\n waitForReconnect();\r\n return;\r\n }\r\n\r\n if (msg.type === 'reload') {\r\n if (msg.url === '*') {\r\n // CSS / global style change \u2014 bust stylesheet hrefs in-place.\r\n // This avoids a full page reload and its associated FOUC.\r\n reloadStylesheets();\r\n return;\r\n }\r\n // A specific page changed \u2014 only navigate if we're on that page.\r\n if (patternMatchesPathname(msg.url, window.location.pathname)) {\r\n log.info('[HMR] Page changed:', msg.url);\r\n navigate(window.location.pathname + window.location.search);\r\n }\r\n return;\r\n }\r\n\r\n if (msg.type === 'replace') {\r\n // A shared component or utility changed. The current page might use\r\n // it, so we re-navigate to pick up the latest server render.\r\n log.info('[HMR] Component changed:', msg.component);\r\n navigate(window.location.pathname + window.location.search);\r\n return;\r\n }\r\n } catch (err) {\r\n log.error('[HMR] Message parse error:', err);\r\n }\r\n };\r\n}\r\n\r\n// \u2500\u2500\u2500 Soft navigation helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Triggers a soft (SPA-style) navigation via the locationchange event that\r\n * bundle.ts listens to. Adds `hmr: true` in the detail so the navigation\r\n * handler appends `?__hmr=1`, which tells SSR to skip client-component\r\n * renderToString (faster HMR round-trips).\r\n */\r\nfunction navigate(href: string): void {\r\n window.dispatchEvent(new CustomEvent('locationchange', { detail: { href, hmr: true } }));\r\n}\r\n\r\n// \u2500\u2500\u2500 Dynamic route pattern 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\r\n\r\n/**\r\n * Returns true when `pathname` matches the route `pattern` emitted by the\r\n * server. Patterns use the file-system conventions:\r\n * [param] \u2192 any single non-slash segment\r\n * [...slug] \u2192 one or more segments\r\n * [[...slug]] \u2192 zero or more segments\r\n * [[param]] \u2192 zero or one segment\r\n *\r\n * Each segment is classified before any escaping so that bracket characters\r\n * in param names are never mistaken for regex metacharacters.\r\n */\r\nfunction patternMatchesPathname(pattern: string, pathname: string): boolean {\r\n // Normalise trailing slashes so /a/ matches pattern /a and vice versa.\r\n const normPattern = pattern.length > 1 ? pattern.replace(/\\/+$/, '') : pattern;\r\n const normPathname = pathname.length > 1 ? pathname.replace(/\\/+$/, '') : pathname;\r\n const segments = normPattern.replace(/^\\//, '').split('/');\r\n const regexParts = segments.map(seg => {\r\n if (/^\\[\\[\\.\\.\\..+\\]\\]$/.test(seg)) return '(?:\\/.*)?' ; // [[...x]] optional catch-all\r\n if (/^\\[\\.\\.\\./.test(seg)) return '(?:\\/.+)' ; // [...x] required catch-all\r\n if (/^\\[\\[/.test(seg)) return '(?:\\/[^/]*)?' ;// [[x]] optional single\r\n if (/^\\[/.test(seg)) return '\\/[^/]+' ; // [x] required single\r\n return '\\/' + seg.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&'); // static \u2014 escape metacharacters\r\n });\r\n return new RegExp('^' + regexParts.join('') + '$').test(normPathname);\r\n}\r\n\r\n// \u2500\u2500\u2500 Reconnect polling \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Polls /__hmr_ping at `intervalMs` until the server responds with a 200\r\n * (meaning it's back up), then triggers a full page reload to pick up any\r\n * changes that happened during the downtime.\r\n *\r\n * Gives up after `maxAttempts` (default ~30 seconds at 3000 ms intervals).\r\n */\r\nfunction waitForReconnect(intervalMs = 3000, maxAttempts = 10): void {\r\n let attempts = 0;\r\n\r\n const id = setInterval(async () => {\r\n attempts++;\r\n try {\r\n const res = await fetch('/__hmr_ping', { cache: 'no-store' });\r\n if (res.ok) {\r\n clearInterval(id);\r\n log.info('[HMR] Server back \u2014 reloading');\r\n window.location.reload();\r\n }\r\n } catch {\r\n // Server still down \u2014 keep polling silently.\r\n }\r\n\r\n if (attempts >= maxAttempts) {\r\n clearInterval(id);\r\n log.error('[HMR] Server did not come back after restart');\r\n }\r\n }, intervalMs);\r\n}\r\n\r\n// \u2500\u2500\u2500 Stylesheet cache-buster \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Appends a `?t=<timestamp>` query to every `<link rel=\"stylesheet\">` href.\r\n * The browser treats the new URL as a different resource and re-fetches it,\r\n * updating styles without a page reload or visible flash.\r\n */\r\nfunction reloadStylesheets(): void {\r\n const links = document.querySelectorAll<HTMLLinkElement>('link[rel=\"stylesheet\"]');\r\n log.info(`[HMR] CSS changed \u2014 reloading ${links.length} stylesheet(s)`);\r\n links.forEach(link => {\r\n const url = new URL(link.href);\r\n url.searchParams.set('t', String(Date.now()));\r\n link.href = url.toString();\r\n });\r\n}\r\n\r\n// Auto-start when this module is loaded.\r\nhmr();"],
5
- "mappings": "AAwBA,SAAS,WAAW;AAKL,SAAR,MAA6B;AAClC,QAAM,KAAK,IAAI,YAAY,QAAQ;AAEnC,KAAG,SAAS,MAAM;AAChB,QAAI,KAAK,iBAAiB;AAAA,EAC5B;AAEA,KAAG,UAAU,MAAM;AAGjB,OAAG,MAAM;AACT,qBAAiB;AAAA,EACnB;AAEA,KAAG,YAAY,OAAO,UAAU;AAC9B,QAAI;AACF,YAAM,MAAM,KAAK,MAAM,MAAM,IAAI;AAEjC,UAAI,IAAI,SAAS,WAAW;AAC1B,YAAI,KAAK,wDAAmD;AAC5D,WAAG,MAAM;AACT,yBAAiB;AACjB;AAAA,MACF;AAEA,UAAI,IAAI,SAAS,UAAU;AACzB,YAAI,IAAI,QAAQ,KAAK;AAGnB,4BAAkB;AAClB;AAAA,QACF;AAEA,YAAI,uBAAuB,IAAI,KAAK,OAAO,SAAS,QAAQ,GAAG;AAC7D,cAAI,KAAK,uBAAuB,IAAI,GAAG;AACvC,mBAAS,OAAO,SAAS,WAAW,OAAO,SAAS,MAAM;AAAA,QAC5D;AACA;AAAA,MACF;AAEA,UAAI,IAAI,SAAS,WAAW;AAG1B,YAAI,KAAK,4BAA4B,IAAI,SAAS;AAClD,iBAAS,OAAO,SAAS,WAAW,OAAO,SAAS,MAAM;AAC1D;AAAA,MACF;AAAA,IACF,SAAS,KAAK;AACZ,UAAI,MAAM,8BAA8B,GAAG;AAAA,IAC7C;AAAA,EACF;AACF;AAUA,SAAS,SAAS,MAAoB;AACpC,SAAO,cAAc,IAAI,YAAY,kBAAkB,EAAE,QAAQ,EAAE,MAAM,KAAK,KAAK,EAAE,CAAC,CAAC;AACzF;AAeA,SAAS,uBAAuB,SAAiB,UAA2B;AAE1E,QAAM,cAAe,QAAQ,SAAU,IAAI,QAAQ,QAAQ,QAAQ,EAAE,IAAK;AAC1E,QAAM,eAAe,SAAS,SAAS,IAAI,SAAS,QAAQ,QAAQ,EAAE,IAAI;AAC1E,QAAM,WAAa,YAAY,QAAQ,OAAO,EAAE,EAAE,MAAM,GAAG;AAC3D,QAAM,aAAa,SAAS,IAAI,SAAO;AACrC,QAAI,qBAAqB,KAAK,GAAG,EAAG,QAAO;AAC3C,QAAI,YAAY,KAAK,GAAG,EAAc,QAAO;AAC7C,QAAI,QAAQ,KAAK,GAAG,EAAoB,QAAO;AAC/C,QAAI,MAAM,KAAK,GAAG,EAAsB,QAAO;AAC/C,WAAO,MAAO,IAAI,QAAQ,sBAAsB,MAAM;AAAA,EACxD,CAAC;AACD,SAAO,IAAI,OAAO,MAAM,WAAW,KAAK,EAAE,IAAI,GAAG,EAAE,KAAK,YAAY;AACtE;AAWA,SAAS,iBAAiB,aAAa,KAAM,cAAc,IAAU;AACnE,MAAI,WAAW;AAEf,QAAM,KAAK,YAAY,YAAY;AACjC;AACA,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,eAAe,EAAE,OAAO,WAAW,CAAC;AAC5D,UAAI,IAAI,IAAI;AACV,sBAAc,EAAE;AAChB,YAAI,KAAK,oCAA+B;AACxC,eAAO,SAAS,OAAO;AAAA,MACzB;AAAA,IACF,QAAQ;AAAA,IAER;AAEA,QAAI,YAAY,aAAa;AAC3B,oBAAc,EAAE;AAChB,UAAI,MAAM,8CAA8C;AAAA,IAC1D;AAAA,EACF,GAAG,UAAU;AACf;AASA,SAAS,oBAA0B;AACjC,QAAM,QAAQ,SAAS,iBAAkC,wBAAwB;AACjF,MAAI,KAAK,sCAAiC,MAAM,MAAM,gBAAgB;AACtE,QAAM,QAAQ,UAAQ;AACpB,UAAM,MAAM,IAAI,IAAI,KAAK,IAAI;AAC7B,QAAI,aAAa,IAAI,KAAK,OAAO,KAAK,IAAI,CAAC,CAAC;AAC5C,SAAK,OAAO,IAAI,SAAS;AAAA,EAC3B,CAAC;AACH;AAGA,IAAI;",
6
- "names": []
7
- }
package/dist/hmr.d.ts DELETED
@@ -1,55 +0,0 @@
1
- /**
2
- * hmr.ts — Server-Side HMR (Hot Module Replacement) Engine
3
- *
4
- * Manages the set of connected SSE clients and broadcasts change payloads to
5
- * them when source files are modified.
6
- *
7
- * The HMR protocol uses three message types:
8
- *
9
- * { type: 'reload', url: '/path' } — A page file changed. The browser
10
- * re-fetches and swaps that URL if it
11
- * is currently active.
12
- * url: '*' means CSS changed — only
13
- * stylesheets are reloaded in-place.
14
- *
15
- * { type: 'replace', component: 'X' } — A non-page file changed (shared
16
- * component, util, etc.). The browser
17
- * re-fetches the current page to pick
18
- * up the new version.
19
- *
20
- * { type: 'restart' } — The server is about to restart
21
- * (middleware.ts or nuke.config.ts
22
- * changed). The client polls
23
- * /__hmr_ping until it gets a 200.
24
- *
25
- * File change → payload mapping:
26
- * pages/** → reload (URL derived from the file path)
27
- * *.css / *.scss … → reload (url: '*' triggers stylesheet cache-bust)
28
- * anything else → replace (component name used for logging only)
29
- *
30
- * Debouncing:
31
- * Editors often emit multiple fs events for a single save. Changes are
32
- * debounced per filename with a 100 ms window so each save produces exactly
33
- * one broadcast.
34
- */
35
- import { ServerResponse } from 'http';
36
- /**
37
- * All currently connected SSE clients (long-lived ServerResponse objects).
38
- * Exported so middleware.ts can register new connections.
39
- */
40
- export declare const hmrClients: Set<ServerResponse<import("node:http").IncomingMessage>>;
41
- /**
42
- * Recursively watches `dir` and broadcasts an HMR message whenever a file
43
- * changes. Changes are debounced per file with a 100 ms window.
44
- *
45
- * @param dir Absolute path to watch.
46
- * @param label Short label for log messages (e.g. 'App', 'Server').
47
- */
48
- export declare function watchDir(dir: string, label: string): void;
49
- /**
50
- * Sends a 'restart' message to all SSE clients, then waits 120 ms to give
51
- * them time to receive it before the process exits.
52
- *
53
- * Called by app.ts before `process.exit(75)` when a config file changes.
54
- */
55
- export declare function broadcastRestart(): Promise<void>;
package/dist/hmr.js.map DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/hmr.ts"],
4
- "sourcesContent": ["/**\r\n * hmr.ts \u2014 Server-Side HMR (Hot Module Replacement) Engine\r\n *\r\n * Manages the set of connected SSE clients and broadcasts change payloads to\r\n * them when source files are modified.\r\n *\r\n * The HMR protocol uses three message types:\r\n *\r\n * { type: 'reload', url: '/path' } \u2014 A page file changed. The browser\r\n * re-fetches and swaps that URL if it\r\n * is currently active.\r\n * url: '*' means CSS changed \u2014 only\r\n * stylesheets are reloaded in-place.\r\n *\r\n * { type: 'replace', component: 'X' } \u2014 A non-page file changed (shared\r\n * component, util, etc.). The browser\r\n * re-fetches the current page to pick\r\n * up the new version.\r\n *\r\n * { type: 'restart' } \u2014 The server is about to restart\r\n * (middleware.ts or nuke.config.ts\r\n * changed). The client polls\r\n * /__hmr_ping until it gets a 200.\r\n *\r\n * File change \u2192 payload mapping:\r\n * pages/** \u2192 reload (URL derived from the file path)\r\n * *.css / *.scss \u2026 \u2192 reload (url: '*' triggers stylesheet cache-bust)\r\n * anything else \u2192 replace (component name used for logging only)\r\n *\r\n * Debouncing:\r\n * Editors often emit multiple fs events for a single save. Changes are\r\n * debounced per filename with a 100 ms window so each save produces exactly\r\n * one broadcast.\r\n */\r\n\r\nimport { ServerResponse } from 'http';\r\nimport { existsSync, watch } from 'fs';\r\nimport path from 'path';\r\nimport { log } from './logger';\r\nimport { invalidateComponentCache } from './component-analyzer';\r\n\r\n// \u2500\u2500\u2500 SSE client registry \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * All currently connected SSE clients (long-lived ServerResponse objects).\r\n * Exported so middleware.ts can register new connections.\r\n */\r\nexport const hmrClients = new Set<ServerResponse>();\r\n\r\n// \u2500\u2500\u2500 Broadcast \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Sends a JSON payload to every connected SSE client.\r\n * Clients that have disconnected are silently removed from the set.\r\n */\r\nfunction broadcastHmr(payload: object): void {\r\n const data = `data: ${JSON.stringify(payload)}\\n\\n`;\r\n for (const client of hmrClients) {\r\n try {\r\n client.write(data);\r\n } catch {\r\n // Write failed \u2014 client disconnected without closing cleanly.\r\n hmrClients.delete(client);\r\n }\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Payload builder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 a relative filename (as emitted by fs.watch) to the URL the browser\r\n * should re-fetch.\r\n *\r\n * Input examples:\r\n * 'pages/index.tsx' \u2192 '/'\r\n * 'pages/about/index.tsx' \u2192 '/about'\r\n * 'pages/blog/[slug].tsx' \u2192 '/blog/[slug]' (dynamic segment preserved)\r\n */\r\nfunction pageFileToUrl(filename: string): string {\r\n // watchDir is called with ./app, so filenames are relative to that dir,\r\n // e.g. 'pages/about/index.tsx'. Strip the 'pages/' prefix to get the URL path.\r\n const withoutPages = filename.slice('pages/'.length);\r\n const withoutExt = withoutPages.replace(/\\.(tsx|ts)$/, '');\r\n\r\n // 'index' and 'layout' at any level map to the directory URL.\r\n // e.g. 'users/layout' \u2192 '/users', 'layout' \u2192 '/', 'users/index' \u2192 '/users'\r\n const url = withoutExt === 'index' || withoutExt === 'layout'\r\n ? '/'\r\n : '/' + withoutExt\r\n .replace(/\\/index$/, '')\r\n .replace(/\\/layout$/, '')\r\n .replace(/\\\\/g, '/');\r\n\r\n return url;\r\n}\r\n\r\n/**\r\n * Determines the appropriate HMR message for a changed file.\r\n *\r\n * Routing logic:\r\n * - Paths under pages/ \u2192 `reload` with the derived URL\r\n * - CSS/Sass/Less files \u2192 `reload` with url='*' (stylesheet cache-bust)\r\n * - Everything else \u2192 `replace` with the component base name\r\n */\r\nfunction buildPayload(filename: string): object {\r\n const normalized = filename.replace(/\\\\/g, '/');\r\n\r\n if (normalized.startsWith('pages/')) {\r\n const url = pageFileToUrl(normalized);\r\n return { type: 'reload', url };\r\n }\r\n\r\n const ext = path.extname(filename).toLowerCase();\r\n if (ext === '.css' || ext === '.scss' || ext === '.sass' || ext === '.less') {\r\n return { type: 'reload', url: '*' };\r\n }\r\n\r\n // Generic component/util change \u2014 browser re-renders the current page.\r\n const componentName = path.basename(filename, path.extname(filename));\r\n return { type: 'replace', component: componentName };\r\n}\r\n\r\n// \u2500\u2500\u2500 File watcher \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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/** Per-filename debounce timers. */\r\nconst pending = new Map<string, NodeJS.Timeout>();\r\n\r\n/**\r\n * Recursively watches `dir` and broadcasts an HMR message whenever a file\r\n * changes. Changes are debounced per file with a 100 ms window.\r\n *\r\n * @param dir Absolute path to watch.\r\n * @param label Short label for log messages (e.g. 'App', 'Server').\r\n */\r\nexport function watchDir(dir: string, label: string): void {\r\n if (!existsSync(dir)) return;\r\n\r\n watch(dir, { recursive: true }, (_event, filename) => {\r\n if (!filename) return;\r\n\r\n // Debounce: cancel any pending timer for this file and restart it.\r\n if (pending.has(filename)) clearTimeout(pending.get(filename)!);\r\n\r\n const timeout = setTimeout(() => {\r\n const payload = buildPayload(filename);\r\n log.info(`[HMR] ${label} changed: ${filename}`, JSON.stringify(payload));\r\n\r\n // Evict this file from the component-analysis cache so the next SSR\r\n // render re-analyses it (catches \"use client\" or import graph changes).\r\n if (dir) invalidateComponentCache(path.resolve(dir, filename));\r\n\r\n broadcastHmr(payload);\r\n pending.delete(filename);\r\n }, 100);\r\n\r\n pending.set(filename, timeout);\r\n });\r\n}\r\n\r\n// \u2500\u2500\u2500 Restart broadcast \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Sends a 'restart' message to all SSE clients, then waits 120 ms to give\r\n * them time to receive it before the process exits.\r\n *\r\n * Called by app.ts before `process.exit(75)` when a config file changes.\r\n */\r\nexport function broadcastRestart(): Promise<void> {\r\n broadcastHmr({ type: 'restart' });\r\n return new Promise<void>(resolve => setTimeout(resolve, 120));\r\n}"],
5
- "mappings": "AAoCA,SAAS,YAAY,aAAa;AAClC,OAAO,UAAU;AACjB,SAAS,WAAW;AACpB,SAAS,gCAAgC;AAQlC,MAAM,aAAa,oBAAI,IAAoB;AAQlD,SAAS,aAAa,SAAuB;AAC3C,QAAM,OAAO,SAAS,KAAK,UAAU,OAAO,CAAC;AAAA;AAAA;AAC7C,aAAW,UAAU,YAAY;AAC/B,QAAI;AACF,aAAO,MAAM,IAAI;AAAA,IACnB,QAAQ;AAEN,iBAAW,OAAO,MAAM;AAAA,IAC1B;AAAA,EACF;AACF;AAaA,SAAS,cAAc,UAA0B;AAG/C,QAAM,eAAe,SAAS,MAAM,SAAS,MAAM;AACnD,QAAM,aAAe,aAAa,QAAQ,eAAe,EAAE;AAI3D,QAAM,MAAM,eAAe,WAAW,eAAe,WACjD,MACA,MAAM,WACH,QAAQ,YAAY,EAAE,EACtB,QAAQ,aAAa,EAAE,EACvB,QAAQ,OAAO,GAAG;AAEzB,SAAO;AACT;AAUA,SAAS,aAAa,UAA0B;AAC9C,QAAM,aAAa,SAAS,QAAQ,OAAO,GAAG;AAE9C,MAAI,WAAW,WAAW,QAAQ,GAAG;AACnC,UAAM,MAAM,cAAc,UAAU;AACpC,WAAO,EAAE,MAAM,UAAU,IAAI;AAAA,EAC/B;AAEA,QAAM,MAAM,KAAK,QAAQ,QAAQ,EAAE,YAAY;AAC/C,MAAI,QAAQ,UAAU,QAAQ,WAAW,QAAQ,WAAW,QAAQ,SAAS;AAC3E,WAAO,EAAE,MAAM,UAAU,KAAK,IAAI;AAAA,EACpC;AAGA,QAAM,gBAAgB,KAAK,SAAS,UAAU,KAAK,QAAQ,QAAQ,CAAC;AACpE,SAAO,EAAE,MAAM,WAAW,WAAW,cAAc;AACrD;AAKA,MAAM,UAAU,oBAAI,IAA4B;AASzC,SAAS,SAAS,KAAa,OAAqB;AACzD,MAAI,CAAC,WAAW,GAAG,EAAG;AAEtB,QAAM,KAAK,EAAE,WAAW,KAAK,GAAG,CAAC,QAAQ,aAAa;AACpD,QAAI,CAAC,SAAU;AAGf,QAAI,QAAQ,IAAI,QAAQ,EAAG,cAAa,QAAQ,IAAI,QAAQ,CAAE;AAE9D,UAAM,UAAU,WAAW,MAAM;AAC/B,YAAM,UAAU,aAAa,QAAQ;AACrC,UAAI,KAAK,SAAS,KAAK,aAAa,QAAQ,IAAI,KAAK,UAAU,OAAO,CAAC;AAIvE,UAAI,IAAK,0BAAyB,KAAK,QAAQ,KAAK,QAAQ,CAAC;AAE7D,mBAAa,OAAO;AACpB,cAAQ,OAAO,QAAQ;AAAA,IACzB,GAAG,GAAG;AAEN,YAAQ,IAAI,UAAU,OAAO;AAAA,EAC/B,CAAC;AACH;AAUO,SAAS,mBAAkC;AAChD,eAAa,EAAE,MAAM,UAAU,CAAC;AAChC,SAAO,IAAI,QAAc,aAAW,WAAW,SAAS,GAAG,CAAC;AAC9D;",
6
- "names": []
7
- }
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/html-store.ts"],
4
- "sourcesContent": ["/**\r\n * html-store.ts \u2014 Per-Request HTML Head Store\r\n *\r\n * Provides a request-scoped store that server components can write to via\r\n * `useHtml()` during SSR. The accumulated values are flushed into the\r\n * rendered HTML document after the component tree is fully rendered.\r\n *\r\n * Why globalThis?\r\n * Node's module system may import this file multiple times if the page\r\n * module and the nukejs package resolve to different copies (e.g. when\r\n * running from source in dev with tsx). Using a well-known Symbol on\r\n * globalThis guarantees all copies share the same store instance.\r\n *\r\n * Request isolation:\r\n * runWithHtmlStore() creates a fresh store before rendering and clears it\r\n * in the `finally` block, so concurrent requests cannot bleed into each other.\r\n *\r\n * Title resolution:\r\n * Layouts and pages can both call useHtml({ title: \u2026 }). Layouts typically\r\n * pass a template function:\r\n *\r\n * useHtml({ title: (prev) => `${prev} | Acme` })\r\n *\r\n * Operations are collected in render order (outermost layout first, page\r\n * last) then resolved *in reverse* so the page's string value is the base\r\n * and layout template functions wrap outward.\r\n */\r\n\r\n// \u2500\u2500\u2500 Public types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** A page sets a literal string; a layout wraps with a template function. */\r\nexport type TitleValue = string | ((prev: string) => string);\r\n\r\nexport interface HtmlAttrs {\r\n lang?: string;\r\n class?: string;\r\n style?: string;\r\n dir?: string;\r\n [attr: string]: string | undefined;\r\n}\r\n\r\nexport interface BodyAttrs {\r\n class?: string;\r\n style?: string;\r\n [attr: string]: string | undefined;\r\n}\r\n\r\nexport interface MetaTag {\r\n name?: string;\r\n property?: string;\r\n httpEquiv?: string;\r\n charset?: string;\r\n content?: string;\r\n [attr: string]: string | undefined;\r\n}\r\n\r\nexport interface LinkTag {\r\n rel?: string;\r\n href?: string;\r\n type?: string;\r\n media?: string;\r\n as?: string;\r\n crossOrigin?: string;\r\n integrity?: string;\r\n hrefLang?: string;\r\n sizes?: string;\r\n [attr: string]: string | undefined;\r\n}\r\n\r\nexport interface ScriptTag {\r\n src?: string;\r\n content?: string;\r\n type?: string;\r\n defer?: boolean;\r\n async?: boolean;\r\n crossOrigin?: string;\r\n integrity?: string;\r\n noModule?: boolean;\r\n /**\r\n * Where to inject the script in the document.\r\n * 'head' (default) \u2014 placed inside <head>, inside the <!--n-head--> block.\r\n * 'body' \u2014 placed at the very end of <body>, inside the\r\n * <!--n-body-scripts--> block, just before </body>.\r\n */\r\n position?: 'head' | 'body';\r\n}\r\n\r\nexport interface StyleTag {\r\n content?: string;\r\n media?: string;\r\n}\r\n\r\nexport interface HtmlStore {\r\n /** Collected in render order; resolved in reverse so the page title wins. */\r\n titleOps: TitleValue[];\r\n /** Attributes merged onto <html>; last write wins per attribute. */\r\n htmlAttrs: HtmlAttrs;\r\n /** Attributes merged onto <body>; last write wins per attribute. */\r\n bodyAttrs: BodyAttrs;\r\n /** Accumulated in render order: layouts first, page last. */\r\n meta: MetaTag[];\r\n link: LinkTag[];\r\n script: ScriptTag[];\r\n style: StyleTag[];\r\n}\r\n\r\n// \u2500\u2500\u2500 GlobalThis storage \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Well-known Symbol used to share the store across duplicate module copies. */\r\nconst KEY = Symbol.for('__nukejs_html_store__');\r\n\r\nconst getGlobal = (): HtmlStore | null => (globalThis as any)[KEY] ?? null;\r\nconst setGlobal = (store: HtmlStore | null): void => { (globalThis as any)[KEY] = store; };\r\n\r\nfunction emptyStore(): HtmlStore {\r\n return {\r\n titleOps: [],\r\n htmlAttrs: {},\r\n bodyAttrs: {},\r\n meta: [],\r\n link: [],\r\n script: [],\r\n style: [],\r\n };\r\n}\r\n\r\n// \u2500\u2500\u2500 Public API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Runs `fn` inside a fresh HTML store and returns the collected values.\r\n *\r\n * Usage in SSR:\r\n * ```ts\r\n * const store = await runWithHtmlStore(async () => {\r\n * appHtml = await renderElementToHtml(element, ctx);\r\n * });\r\n * // store.titleOps, store.meta, etc. are now populated\r\n * ```\r\n */\r\nexport async function runWithHtmlStore(fn: () => Promise<void>): Promise<HtmlStore> {\r\n setGlobal(emptyStore());\r\n try {\r\n await fn();\r\n return { ...(getGlobal() ?? emptyStore()) } as HtmlStore;\r\n } finally {\r\n // Always clear the store, even if rendering throws, to prevent leakage\r\n // into the next request on the same event-loop tick.\r\n setGlobal(null);\r\n }\r\n}\r\n\r\n/**\r\n * Returns the current request's store, or `undefined` if called outside of\r\n * a `runWithHtmlStore` context (e.g. in the browser or in a test).\r\n */\r\nexport function getHtmlStore(): HtmlStore | undefined {\r\n return getGlobal() ?? undefined;\r\n}\r\n\r\n/**\r\n * Resolves the final page title from a list of title operations.\r\n *\r\n * Operations are walked in *reverse* so the page's value is the starting\r\n * point and layout template functions wrap it outward:\r\n *\r\n * ```\r\n * ops = [ (p) => `${p} | Acme`, 'About' ] \u2190 layout pushed first, page last\r\n * Walk in reverse:\r\n * i=1: op = 'About' \u2192 title = 'About'\r\n * i=0: op = (p) => \u2026 \u2192 title = 'About | Acme'\r\n * ```\r\n *\r\n * @param fallback Used when ops is empty (e.g. a page that didn't call useHtml).\r\n */\r\nexport function resolveTitle(ops: TitleValue[], fallback = ''): string {\r\n let title = fallback;\r\n for (let i = ops.length - 1; i >= 0; i--) {\r\n const op = ops[i];\r\n title = typeof op === 'string' ? op : op(title);\r\n }\r\n return title;\r\n}"],
5
- "mappings": "AA6GA,MAAM,MAAM,uBAAO,IAAI,uBAAuB;AAE9C,MAAM,YAAY,MAAyB,WAAmB,GAAG,KAAK;AACtE,MAAM,YAAY,CAAC,UAAkC;AAAE,EAAC,WAAmB,GAAG,IAAI;AAAO;AAEzF,SAAS,aAAwB;AAC/B,SAAO;AAAA,IACL,UAAW,CAAC;AAAA,IACZ,WAAW,CAAC;AAAA,IACZ,WAAW,CAAC;AAAA,IACZ,MAAW,CAAC;AAAA,IACZ,MAAW,CAAC;AAAA,IACZ,QAAW,CAAC;AAAA,IACZ,OAAW,CAAC;AAAA,EACd;AACF;AAeA,eAAsB,iBAAiB,IAA6C;AAClF,YAAU,WAAW,CAAC;AACtB,MAAI;AACF,UAAM,GAAG;AACT,WAAO,EAAE,GAAI,UAAU,KAAK,WAAW,EAAG;AAAA,EAC5C,UAAE;AAGA,cAAU,IAAI;AAAA,EAChB;AACF;AAMO,SAAS,eAAsC;AACpD,SAAO,UAAU,KAAK;AACxB;AAiBO,SAAS,aAAa,KAAmB,WAAW,IAAY;AACrE,MAAI,QAAQ;AACZ,WAAS,IAAI,IAAI,SAAS,GAAG,KAAK,GAAG,KAAK;AACxC,UAAM,KAAK,IAAI,CAAC;AAChB,YAAQ,OAAO,OAAO,WAAW,KAAK,GAAG,KAAK;AAAA,EAChD;AACA,SAAO;AACT;",
6
- "names": []
7
- }
@@ -1,92 +0,0 @@
1
- /**
2
- * http-server.ts — API Route Dispatcher
3
- *
4
- * Handles discovery and dispatch of API routes inside `serverDir`.
5
- *
6
- * Directory conventions (mirrors Next.js):
7
- * server/
8
- * users/ → prefix /users (directory)
9
- * index.ts → GET /users (method exports: GET, POST, …)
10
- * [id].ts → GET /users/:id
11
- * auth.ts → prefix /auth (top-level file)
12
- * index.ts → prefix / (root handler)
13
- *
14
- * Route handler exports:
15
- * export function GET(req, res) { … }
16
- * export function POST(req, res) { … }
17
- * export default function(req, res) { … } // matches any method
18
- *
19
- * Request augmentation:
20
- * req.body — parsed JSON or raw string (10 MB limit)
21
- * req.params — dynamic route segments (e.g. { id: '42' })
22
- * req.query — URL search params
23
- *
24
- * Response augmentation:
25
- * res.json(data, status?) — JSON response shorthand
26
- * res.status(code) — sets statusCode, returns res for chaining
27
- */
28
- import type { IncomingMessage, ServerResponse } from 'http';
29
- /** Describes a single API prefix discovered in serverDir. */
30
- export interface ApiPrefixInfo {
31
- /** URL prefix this entry handles (e.g. '/users', ''). */
32
- prefix: string;
33
- /** Directory to scan for route files. */
34
- directory: string;
35
- /** Set when the prefix comes from a top-level file (not a directory). */
36
- filePath?: string;
37
- }
38
- /** Node's IncomingMessage with parsed body, params, and query. */
39
- export interface ApiRequest extends IncomingMessage {
40
- params?: Record<string, string | string[]>;
41
- query?: Record<string, string>;
42
- body?: any;
43
- }
44
- /** Node's ServerResponse with json() and status() convenience methods. */
45
- export interface ApiResponse extends ServerResponse {
46
- json: (data: any, status?: number) => void;
47
- status: (code: number) => ApiResponse;
48
- }
49
- /**
50
- * Scans `serverDir` and returns one ApiPrefixInfo per directory, top-level
51
- * file, and root index.ts. Directories are returned before same-stem files
52
- * so `/a/b` routes resolve to the directory tree before any flat file.
53
- *
54
- * Called at startup and again whenever the server directory changes (in dev).
55
- */
56
- export declare function discoverApiPrefixes(serverDir: string): ApiPrefixInfo[];
57
- /**
58
- * Buffers the request body and returns:
59
- * - Parsed JSON object if Content-Type is application/json.
60
- * - Raw string otherwise.
61
- *
62
- * Rejects with an error if the body exceeds MAX_BODY_BYTES to prevent
63
- * memory exhaustion attacks. Deletes __proto__ and constructor from parsed
64
- * JSON objects to guard against prototype pollution.
65
- */
66
- export declare function parseBody(req: IncomingMessage): Promise<any>;
67
- /** Extracts URL search params into a plain string map. */
68
- export declare function parseQuery(url: string, port: number): Record<string, string>;
69
- /**
70
- * Adds `json()` and `status()` convenience methods to a raw ServerResponse,
71
- * mirroring the Express API surface that most API handlers expect.
72
- */
73
- export declare function enhanceResponse(res: ServerResponse): ApiResponse;
74
- /**
75
- * Finds the first ApiPrefixInfo whose prefix is a prefix of `url`.
76
- *
77
- * The empty-string prefix ('') acts as a catch-all and only matches when no
78
- * other prefix claims the URL.
79
- *
80
- * Returns `null` when no prefix matches (request should fall through to SSR).
81
- */
82
- export declare function matchApiPrefix(url: string, apiPrefixes: ApiPrefixInfo[]): {
83
- prefix: ApiPrefixInfo;
84
- apiPath: string;
85
- } | null;
86
- interface ApiHandlerOptions {
87
- apiPrefixes: ApiPrefixInfo[];
88
- port: number;
89
- isDev: boolean;
90
- }
91
- export declare function createApiHandler({ apiPrefixes, port, isDev }: ApiHandlerOptions): (url: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
92
- export {};
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/http-server.ts"],
4
- "sourcesContent": ["/**\r\n * http-server.ts \u2014 API Route Dispatcher\r\n *\r\n * Handles discovery and dispatch of API routes inside `serverDir`.\r\n *\r\n * Directory conventions (mirrors Next.js):\r\n * server/\r\n * users/ \u2192 prefix /users (directory)\r\n * index.ts \u2192 GET /users (method exports: GET, POST, \u2026)\r\n * [id].ts \u2192 GET /users/:id\r\n * auth.ts \u2192 prefix /auth (top-level file)\r\n * index.ts \u2192 prefix / (root handler)\r\n *\r\n * Route handler exports:\r\n * export function GET(req, res) { \u2026 }\r\n * export function POST(req, res) { \u2026 }\r\n * export default function(req, res) { \u2026 } // matches any method\r\n *\r\n * Request augmentation:\r\n * req.body \u2014 parsed JSON or raw string (10 MB limit)\r\n * req.params \u2014 dynamic route segments (e.g. { id: '42' })\r\n * req.query \u2014 URL search params\r\n *\r\n * Response augmentation:\r\n * res.json(data, status?) \u2014 JSON response shorthand\r\n * res.status(code) \u2014 sets statusCode, returns res for chaining\r\n */\r\n\r\nimport path from 'path';\r\nimport fs from 'fs';\r\nimport { pathToFileURL } from 'url';\r\nimport type { IncomingMessage, ServerResponse } from 'http';\r\nimport { log } from './logger';\r\nimport { matchRoute } from './router';\r\n\r\n// \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Describes a single API prefix discovered in serverDir. */\r\nexport interface ApiPrefixInfo {\r\n /** URL prefix this entry handles (e.g. '/users', ''). */\r\n prefix: string;\r\n /** Directory to scan for route files. */\r\n directory: string;\r\n /** Set when the prefix comes from a top-level file (not a directory). */\r\n filePath?: string;\r\n}\r\n\r\n/** Node's IncomingMessage with parsed body, params, and query. */\r\nexport interface ApiRequest extends IncomingMessage {\r\n params?: Record<string, string | string[]>;\r\n query?: Record<string, string>;\r\n body?: any;\r\n}\r\n\r\n/** Node's ServerResponse with json() and status() convenience methods. */\r\nexport interface ApiResponse extends ServerResponse {\r\n json: (data: any, status?: number) => void;\r\n status: (code: number) => ApiResponse;\r\n}\r\n\r\ntype HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS';\r\ntype ApiHandler = (req: ApiRequest, res: ApiResponse) => void | Promise<void>;\r\n\r\ninterface ApiModule {\r\n default?: ApiHandler;\r\n GET?: ApiHandler;\r\n POST?: ApiHandler;\r\n PUT?: ApiHandler;\r\n DELETE?: ApiHandler;\r\n PATCH?: ApiHandler;\r\n OPTIONS?: ApiHandler;\r\n}\r\n\r\n// \u2500\u2500\u2500 Route discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Scans `serverDir` and returns one ApiPrefixInfo per directory, top-level\r\n * file, and root index.ts. Directories are returned before same-stem files\r\n * so `/a/b` routes resolve to the directory tree before any flat file.\r\n *\r\n * Called at startup and again whenever the server directory changes (in dev).\r\n */\r\nexport function discoverApiPrefixes(serverDir: string): ApiPrefixInfo[] {\r\n if (!fs.existsSync(serverDir)) {\r\n log.warn('Server directory not found:', serverDir);\r\n return [];\r\n }\r\n\r\n const entries = fs.readdirSync(serverDir, { withFileTypes: true });\r\n const prefixes: ApiPrefixInfo[] = [];\r\n\r\n // Directories first (higher specificity than same-stem files).\r\n for (const e of entries) {\r\n if (e.isDirectory()) {\r\n prefixes.push({ prefix: `/${e.name}`, directory: path.join(serverDir, e.name) });\r\n }\r\n }\r\n\r\n // Top-level .ts/.tsx files (excluding index which is handled separately below).\r\n for (const e of entries) {\r\n if (\r\n e.isFile() &&\r\n (e.name.endsWith('.ts') || e.name.endsWith('.tsx')) &&\r\n e.name !== 'index.ts' &&\r\n e.name !== 'index.tsx'\r\n ) {\r\n const stem = e.name.replace(/\\.tsx?$/, '');\r\n prefixes.push({\r\n prefix: `/${stem}`,\r\n directory: serverDir,\r\n filePath: path.join(serverDir, e.name),\r\n });\r\n }\r\n }\r\n\r\n // index.ts/tsx at the root of serverDir handles unmatched paths (prefix '').\r\n if (fs.existsSync(path.join(serverDir, 'index.ts'))) {\r\n prefixes.push({ prefix: '', directory: serverDir });\r\n }\r\n\r\n return prefixes;\r\n}\r\n\r\n// \u2500\u2500\u2500 Body parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB\r\n\r\n/**\r\n * Buffers the request body and returns:\r\n * - Parsed JSON object if Content-Type is application/json.\r\n * - Raw string otherwise.\r\n *\r\n * Rejects with an error if the body exceeds MAX_BODY_BYTES to prevent\r\n * memory exhaustion attacks. Deletes __proto__ and constructor from parsed\r\n * JSON objects to guard against prototype pollution.\r\n */\r\nexport async function parseBody(req: IncomingMessage): Promise<any> {\r\n return new Promise((resolve, reject) => {\r\n let body = '';\r\n let bytes = 0;\r\n\r\n req.on('data', chunk => {\r\n bytes += chunk.length;\r\n if (bytes > MAX_BODY_BYTES) {\r\n req.destroy();\r\n return reject(new Error('Request body too large'));\r\n }\r\n body += chunk.toString();\r\n });\r\n\r\n req.on('end', () => {\r\n try {\r\n if (body && req.headers['content-type']?.includes('application/json')) {\r\n const parsed = JSON.parse(body);\r\n // Guard against prototype pollution via __proto__ / constructor.\r\n if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {\r\n delete parsed.__proto__;\r\n delete parsed.constructor;\r\n }\r\n resolve(parsed);\r\n } else {\r\n resolve(body);\r\n }\r\n } catch (err) {\r\n reject(err);\r\n }\r\n });\r\n\r\n req.on('error', reject);\r\n });\r\n}\r\n\r\n// \u2500\u2500\u2500 Query parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Extracts URL search params into a plain string map. */\r\nexport function parseQuery(url: string, port: number): Record<string, string> {\r\n const query: Record<string, string> = {};\r\n new URL(url, `http://localhost:${port}`)\r\n .searchParams\r\n .forEach((v, k) => { query[k] = v; });\r\n return query;\r\n}\r\n\r\n// \u2500\u2500\u2500 Response enhancement \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Adds `json()` and `status()` convenience methods to a raw ServerResponse,\r\n * mirroring the Express API surface that most API handlers expect.\r\n */\r\nexport function enhanceResponse(res: ServerResponse): ApiResponse {\r\n const apiRes = res as ApiResponse;\r\n apiRes.json = function (data, statusCode = 200) {\r\n this.statusCode = statusCode;\r\n this.setHeader('Content-Type', 'application/json');\r\n this.end(JSON.stringify(data));\r\n };\r\n apiRes.status = function (code) {\r\n this.statusCode = code;\r\n return this;\r\n };\r\n return apiRes;\r\n}\r\n\r\n/** Responds to an OPTIONS preflight with permissive CORS headers. */\r\nfunction respondOptions(res: ApiResponse): void {\r\n res.setHeader('Access-Control-Allow-Origin', '*');\r\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');\r\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type');\r\n res.statusCode = 204;\r\n res.end();\r\n}\r\n\r\n// \u2500\u2500\u2500 Prefix matching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Finds the first ApiPrefixInfo whose prefix is a prefix of `url`.\r\n *\r\n * The empty-string prefix ('') acts as a catch-all and only matches when no\r\n * other prefix claims the URL.\r\n *\r\n * Returns `null` when no prefix matches (request should fall through to SSR).\r\n */\r\nexport function matchApiPrefix(\r\n url: string,\r\n apiPrefixes: ApiPrefixInfo[],\r\n): { prefix: ApiPrefixInfo; apiPath: string } | null {\r\n for (const prefix of apiPrefixes) {\r\n if (prefix.prefix === '') {\r\n // Empty prefix \u2014 only match if no other prefix has claimed this URL.\r\n const claimedByOther = apiPrefixes.some(\r\n p => p.prefix !== '' && url.startsWith(p.prefix),\r\n );\r\n if (!claimedByOther) return { prefix, apiPath: url || '/' };\r\n } else if (url.startsWith(prefix.prefix)) {\r\n return { prefix, apiPath: url.slice(prefix.prefix.length) || '/' };\r\n }\r\n }\r\n return null;\r\n}\r\n\r\n// \u2500\u2500\u2500 Dev-mode fresh importer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Imports `filePath` fresh on every call using tsx's tsImport, which creates\r\n * an isolated module namespace that bypasses Node's ESM cache entirely.\r\n *\r\n * This is identical to how ssr.ts loads page and layout modules in dev mode.\r\n * tsx handles TypeScript and TSX natively, and bare specifiers (e.g.\r\n * \"@orpc/server/node\") resolve normally through the standard node_modules\r\n * chain \u2014 no bundling, no temp files, no watchers needed.\r\n */\r\nasync function importFreshInDev(filePath: string): Promise<ApiModule> {\r\n const { tsImport } = await import('tsx/esm/api');\r\n return await tsImport(\r\n pathToFileURL(filePath).href,\r\n { parentURL: import.meta.url },\r\n ) as ApiModule;\r\n}\r\n\r\n// \u2500\u2500\u2500 Request handler factory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\ninterface ApiHandlerOptions {\r\n apiPrefixes: ApiPrefixInfo[];\r\n port: number;\r\n isDev: boolean;\r\n}\r\n\r\nexport function createApiHandler({ apiPrefixes, port, isDev }: ApiHandlerOptions) {\r\n return async function handleApiRoute(\r\n url: string,\r\n req: IncomingMessage,\r\n res: ServerResponse,\r\n ): Promise<void> {\r\n const apiRes = enhanceResponse(res);\r\n const apiMatch = matchApiPrefix(url, apiPrefixes);\r\n\r\n if (!apiMatch) {\r\n apiRes.json({ error: 'API endpoint not found' }, 404);\r\n return;\r\n }\r\n\r\n const { prefix, apiPath } = apiMatch;\r\n let filePath: string | null = null;\r\n let params: Record<string, string | string[]> = {};\r\n\r\n // 1. Direct file match (top-level file prefix, e.g. server/auth.ts \u2192 /auth).\r\n if (prefix.filePath) {\r\n filePath = prefix.filePath;\r\n }\r\n\r\n // 2. Root index.ts (prefix === '' and path === '/').\r\n if (!filePath && prefix.prefix === '' && apiPath === '/') {\r\n const indexPath = path.join(prefix.directory, 'index.ts');\r\n if (fs.existsSync(indexPath)) filePath = indexPath;\r\n }\r\n\r\n // 3. Dynamic route matching inside the prefix directory.\r\n if (!filePath) {\r\n const routeMatch =\r\n matchRoute(apiPath, prefix.directory, '.ts') ??\r\n matchRoute(apiPath, prefix.directory, '.tsx');\r\n if (routeMatch) { filePath = routeMatch.filePath; params = routeMatch.params; }\r\n }\r\n\r\n if (!filePath) {\r\n apiRes.json({ error: 'API endpoint not found' }, 404);\r\n return;\r\n }\r\n\r\n try {\r\n const method = (req.method || 'GET').toUpperCase() as HttpMethod;\r\n log.verbose(`API ${method} ${url} -> ${path.relative(process.cwd(), filePath)}`);\r\n\r\n // OPTIONS preflight \u2014 respond immediately with CORS headers.\r\n if (method === 'OPTIONS') { respondOptions(apiRes); return; }\r\n\r\n // Augment the request object with parsed body, params, and query.\r\n const apiReq = req as ApiRequest;\r\n apiReq.body = await parseBody(req);\r\n apiReq.params = params;\r\n apiReq.query = parseQuery(url, port);\r\n\r\n const apiModule: ApiModule = isDev\r\n ? await importFreshInDev(filePath)\r\n : await import(pathToFileURL(filePath).href);\r\n const handler = apiModule[method] ?? apiModule.default;\r\n\r\n if (!handler) {\r\n apiRes.json({ error: `Method ${method} not allowed` }, 405);\r\n return;\r\n }\r\n\r\n await handler(apiReq, apiRes);\r\n } catch (error) {\r\n log.error('API Error:', error);\r\n apiRes.json({ error: 'Internal server error' }, 500);\r\n }\r\n };\r\n}"],
5
- "mappings": "AA4BA,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,SAAS,qBAAqB;AAE9B,SAAS,WAAW;AACpB,SAAS,kBAAkB;AAiDpB,SAAS,oBAAoB,WAAoC;AACtE,MAAI,CAAC,GAAG,WAAW,SAAS,GAAG;AAC7B,QAAI,KAAK,+BAA+B,SAAS;AACjD,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,UAAU,GAAG,YAAY,WAAW,EAAE,eAAe,KAAK,CAAC;AACjE,QAAM,WAA4B,CAAC;AAGnC,aAAW,KAAK,SAAS;AACvB,QAAI,EAAE,YAAY,GAAG;AACnB,eAAS,KAAK,EAAE,QAAQ,IAAI,EAAE,IAAI,IAAI,WAAW,KAAK,KAAK,WAAW,EAAE,IAAI,EAAE,CAAC;AAAA,IACjF;AAAA,EACF;AAGA,aAAW,KAAK,SAAS;AACvB,QACE,EAAE,OAAO,MACR,EAAE,KAAK,SAAS,KAAK,KAAK,EAAE,KAAK,SAAS,MAAM,MACjD,EAAE,SAAS,cACX,EAAE,SAAS,aACX;AACA,YAAM,OAAO,EAAE,KAAK,QAAQ,WAAW,EAAE;AACzC,eAAS,KAAK;AAAA,QACZ,QAAQ,IAAI,IAAI;AAAA,QAChB,WAAW;AAAA,QACX,UAAU,KAAK,KAAK,WAAW,EAAE,IAAI;AAAA,MACvC,CAAC;AAAA,IACH;AAAA,EACF;AAGA,MAAI,GAAG,WAAW,KAAK,KAAK,WAAW,UAAU,CAAC,GAAG;AACnD,aAAS,KAAK,EAAE,QAAQ,IAAI,WAAW,UAAU,CAAC;AAAA,EACpD;AAEA,SAAO;AACT;AAIA,MAAM,iBAAiB,KAAK,OAAO;AAWnC,eAAsB,UAAU,KAAoC;AAClE,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,QAAI,OAAO;AACX,QAAI,QAAQ;AAEZ,QAAI,GAAG,QAAQ,WAAS;AACtB,eAAS,MAAM;AACf,UAAI,QAAQ,gBAAgB;AAC1B,YAAI,QAAQ;AACZ,eAAO,OAAO,IAAI,MAAM,wBAAwB,CAAC;AAAA,MACnD;AACA,cAAQ,MAAM,SAAS;AAAA,IACzB,CAAC;AAED,QAAI,GAAG,OAAO,MAAM;AAClB,UAAI;AACF,YAAI,QAAQ,IAAI,QAAQ,cAAc,GAAG,SAAS,kBAAkB,GAAG;AACrE,gBAAM,SAAS,KAAK,MAAM,IAAI;AAE9B,cAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,CAAC,MAAM,QAAQ,MAAM,GAAG;AAC3E,mBAAO,OAAO;AACd,mBAAO,OAAO;AAAA,UAChB;AACA,kBAAQ,MAAM;AAAA,QAChB,OAAO;AACL,kBAAQ,IAAI;AAAA,QACd;AAAA,MACF,SAAS,KAAK;AACZ,eAAO,GAAG;AAAA,MACZ;AAAA,IACF,CAAC;AAED,QAAI,GAAG,SAAS,MAAM;AAAA,EACxB,CAAC;AACH;AAKO,SAAS,WAAW,KAAa,MAAsC;AAC5E,QAAM,QAAgC,CAAC;AACvC,MAAI,IAAI,KAAK,oBAAoB,IAAI,EAAE,EACpC,aACA,QAAQ,CAAC,GAAG,MAAM;AAAE,UAAM,CAAC,IAAI;AAAA,EAAG,CAAC;AACtC,SAAO;AACT;AAQO,SAAS,gBAAgB,KAAkC;AAChE,QAAM,SAAS;AACf,SAAO,OAAO,SAAU,MAAM,aAAa,KAAK;AAC9C,SAAK,aAAa;AAClB,SAAK,UAAU,gBAAgB,kBAAkB;AACjD,SAAK,IAAI,KAAK,UAAU,IAAI,CAAC;AAAA,EAC/B;AACA,SAAO,SAAS,SAAU,MAAM;AAC9B,SAAK,aAAa;AAClB,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAGA,SAAS,eAAe,KAAwB;AAC9C,MAAI,UAAU,+BAA+B,GAAG;AAChD,MAAI,UAAU,gCAAgC,wCAAwC;AACtF,MAAI,UAAU,gCAAgC,cAAc;AAC5D,MAAI,aAAa;AACjB,MAAI,IAAI;AACV;AAYO,SAAS,eACd,KACA,aACmD;AACnD,aAAW,UAAU,aAAa;AAChC,QAAI,OAAO,WAAW,IAAI;AAExB,YAAM,iBAAiB,YAAY;AAAA,QACjC,OAAK,EAAE,WAAW,MAAM,IAAI,WAAW,EAAE,MAAM;AAAA,MACjD;AACA,UAAI,CAAC,eAAgB,QAAO,EAAE,QAAQ,SAAS,OAAO,IAAI;AAAA,IAC5D,WAAW,IAAI,WAAW,OAAO,MAAM,GAAG;AACxC,aAAO,EAAE,QAAQ,SAAS,IAAI,MAAM,OAAO,OAAO,MAAM,KAAK,IAAI;AAAA,IACnE;AAAA,EACF;AACA,SAAO;AACT;AAaA,eAAe,iBAAiB,UAAsC;AACpE,QAAM,EAAE,SAAS,IAAI,MAAM,OAAO,aAAa;AAC/C,SAAO,MAAM;AAAA,IACX,cAAc,QAAQ,EAAE;AAAA,IACxB,EAAE,WAAW,YAAY,IAAI;AAAA,EAC/B;AACF;AAUO,SAAS,iBAAiB,EAAE,aAAa,MAAM,MAAM,GAAsB;AAChF,SAAO,eAAe,eACpB,KACA,KACA,KACe;AACf,UAAM,SAAS,gBAAgB,GAAG;AAClC,UAAM,WAAW,eAAe,KAAK,WAAW;AAEhD,QAAI,CAAC,UAAU;AACb,aAAO,KAAK,EAAE,OAAO,yBAAyB,GAAG,GAAG;AACpD;AAAA,IACF;AAEA,UAAM,EAAE,QAAQ,QAAQ,IAAI;AAC5B,QAAI,WAA0B;AAC9B,QAAI,SAA4C,CAAC;AAGjD,QAAI,OAAO,UAAU;AACnB,iBAAW,OAAO;AAAA,IACpB;AAGA,QAAI,CAAC,YAAY,OAAO,WAAW,MAAM,YAAY,KAAK;AACxD,YAAM,YAAY,KAAK,KAAK,OAAO,WAAW,UAAU;AACxD,UAAI,GAAG,WAAW,SAAS,EAAG,YAAW;AAAA,IAC3C;AAGA,QAAI,CAAC,UAAU;AACb,YAAM,aACJ,WAAW,SAAS,OAAO,WAAW,KAAK,KAC3C,WAAW,SAAS,OAAO,WAAW,MAAM;AAC9C,UAAI,YAAY;AAAE,mBAAW,WAAW;AAAU,iBAAS,WAAW;AAAA,MAAQ;AAAA,IAChF;AAEA,QAAI,CAAC,UAAU;AACb,aAAO,KAAK,EAAE,OAAO,yBAAyB,GAAG,GAAG;AACpD;AAAA,IACF;AAEA,QAAI;AACF,YAAM,UAAU,IAAI,UAAU,OAAO,YAAY;AACjD,UAAI,QAAQ,OAAO,MAAM,IAAI,GAAG,OAAO,KAAK,SAAS,QAAQ,IAAI,GAAG,QAAQ,CAAC,EAAE;AAG/E,UAAI,WAAW,WAAW;AAAE,uBAAe,MAAM;AAAG;AAAA,MAAQ;AAG5D,YAAM,SAAS;AACf,aAAO,OAAO,MAAM,UAAU,GAAG;AACjC,aAAO,SAAS;AAChB,aAAO,QAAQ,WAAW,KAAK,IAAI;AAEnC,YAAM,YAAuB,QACzB,MAAM,iBAAiB,QAAQ,IAC/B,MAAM,OAAO,cAAc,QAAQ,EAAE;AACzC,YAAM,UAAU,UAAU,MAAM,KAAK,UAAU;AAE/C,UAAI,CAAC,SAAS;AACZ,eAAO,KAAK,EAAE,OAAO,UAAU,MAAM,eAAe,GAAG,GAAG;AAC1D;AAAA,MACF;AAEA,YAAM,QAAQ,QAAQ,MAAM;AAAA,IAC9B,SAAS,OAAO;AACd,UAAI,MAAM,cAAc,KAAK;AAC7B,aAAO,KAAK,EAAE,OAAO,wBAAwB,GAAG,GAAG;AAAA,IACrD;AAAA,EACF;AACF;",
6
- "names": []
7
- }
package/dist/index.js.map DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/index.ts"],
4
- "sourcesContent": ["// \u2500\u2500\u2500 Client-side hooks & components \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\nexport { useHtml } from './use-html';\r\nexport type {\r\n HtmlOptions,\r\n TitleValue,\r\n HtmlAttrs,\r\n BodyAttrs,\r\n MetaTag,\r\n LinkTag,\r\n ScriptTag,\r\n StyleTag,\r\n} from './use-html';\r\n\r\nexport { default as useRouter } from './use-router';\r\n\r\nexport { useRequest } from './use-request';\r\nexport type { RequestContext } from './use-request';\r\n\r\nexport { normaliseHeaders, sanitiseHeaders } from './request-store';\r\n\r\nexport { default as Link } from './Link';\r\n\r\n// \u2500\u2500\u2500 Client runtime (browser bootstrap) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\nexport { setupLocationChangeMonitor, initRuntime } from './bundle';\r\nexport type { RuntimeData } from './bundle';\r\n\r\n// \u2500\u2500\u2500 Shared utilities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\nexport { escapeHtml } from './utils';\r\n\r\nexport { ansi, c, log, setDebugLevel, getDebugLevel } from './logger';\r\nexport type { DebugLevel } from './logger';"],
5
- "mappings": "AACA,SAAS,eAAe;AAYxB,SAAoB,WAAXA,gBAA4B;AAErC,SAAS,kBAAkB;AAG3B,SAAS,kBAAkB,uBAAuB;AAElD,SAAoB,WAAXA,gBAAuB;AAGhC,SAAS,4BAA4B,mBAAmB;AAIxD,SAAS,kBAAkB;AAE3B,SAAS,MAAM,GAAG,KAAK,eAAe,qBAAqB;",
6
- "names": ["default"]
7
- }
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/logger.ts"],
4
- "sourcesContent": ["/**\r\n * logger.ts \u2014 ANSI-Coloured Levelled Logger\r\n *\r\n * Provides a small set of server-side logging utilities used throughout NukeJS.\r\n *\r\n * Debug levels (set via nuke.config.ts `debug` field):\r\n * false \u2014 silent, nothing printed\r\n * 'error' \u2014 error() only\r\n * 'info' \u2014 info(), warn(), error()\r\n * true \u2014 verbose: all of the above plus verbose()\r\n *\r\n * The level can be read back with `getDebugLevel()` and is also forwarded to\r\n * the browser client (as a string) so server and client log at the same level.\r\n */\r\n\r\n// \u2500\u2500\u2500 ANSI escape codes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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/** Map of named ANSI colour/style escape codes. */\r\nexport const ansi = {\r\n reset: '\\x1b[0m',\r\n bold: '\\x1b[1m',\r\n dim: '\\x1b[2m',\r\n black: '\\x1b[30m',\r\n red: '\\x1b[31m',\r\n green: '\\x1b[32m',\r\n yellow: '\\x1b[33m',\r\n blue: '\\x1b[34m',\r\n magenta: '\\x1b[35m',\r\n cyan: '\\x1b[36m',\r\n white: '\\x1b[37m',\r\n gray: '\\x1b[90m',\r\n bgBlue: '\\x1b[44m',\r\n bgGreen: '\\x1b[42m',\r\n bgMagenta: '\\x1b[45m',\r\n} as const;\r\n\r\n/**\r\n * Wraps `text` in the ANSI sequence for `color`, optionally bold.\r\n * Always appends the reset sequence so colour does not bleed into surrounding\r\n * terminal output.\r\n */\r\nexport function c(color: keyof typeof ansi, text: string, bold = false): string {\r\n return `${bold ? ansi.bold : ''}${ansi[color]}${text}${ansi.reset}`;\r\n}\r\n\r\n// \u2500\u2500\u2500 Debug level \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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/** false = silent | 'error' = errors only | 'info' = startup + errors | true = verbose */\r\nexport type DebugLevel = false | 'error' | 'info' | true;\r\n\r\nlet _level: DebugLevel = false;\r\n\r\n/** Sets the active log level. Called once after the config is loaded. */\r\nexport function setDebugLevel(level: DebugLevel): void { _level = level; }\r\n\r\n/** Returns the currently active log level. */\r\nexport function getDebugLevel(): DebugLevel { return _level; }\r\n\r\n// \u2500\u2500\u2500 Logger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Structured log object with four severity methods.\r\n * Each method is a no-op unless the current `_level` allows it.\r\n */\r\nexport const log = {\r\n /** Trace-level detail: component IDs, route matching, bundle paths. */\r\n verbose(...args: any[]): void {\r\n if (_level === true) console.log(c('gray', '[verbose]'), ...args);\r\n },\r\n /** Startup messages, route tables, config summary. */\r\n info(...args: any[]): void {\r\n if (_level === true || _level === 'info') console.log(c('cyan', '[info]'), ...args);\r\n },\r\n /** Non-fatal issues: missing middleware, unrecognised config keys. */\r\n warn(...args: any[]): void {\r\n if (_level === true || _level === 'info') console.warn(c('yellow', '[warn]'), ...args);\r\n },\r\n /** Errors that produce a 500 response or crash the build. */\r\n error(...args: any[]): void {\r\n if (_level !== false) console.error(c('red', '[error]'), ...args);\r\n },\r\n};\r\n"],
5
- "mappings": "AAkBO,MAAM,OAAO;AAAA,EAClB,OAAW;AAAA,EACX,MAAW;AAAA,EACX,KAAW;AAAA,EACX,OAAW;AAAA,EACX,KAAW;AAAA,EACX,OAAW;AAAA,EACX,QAAW;AAAA,EACX,MAAW;AAAA,EACX,SAAW;AAAA,EACX,MAAW;AAAA,EACX,OAAW;AAAA,EACX,MAAW;AAAA,EACX,QAAW;AAAA,EACX,SAAW;AAAA,EACX,WAAW;AACb;AAOO,SAAS,EAAE,OAA0B,MAAc,OAAO,OAAe;AAC9E,SAAO,GAAG,OAAO,KAAK,OAAO,EAAE,GAAG,KAAK,KAAK,CAAC,GAAG,IAAI,GAAG,KAAK,KAAK;AACnE;AAOA,IAAI,SAAqB;AAGlB,SAAS,cAAc,OAAyB;AAAE,WAAS;AAAO;AAGlE,SAAS,gBAA4B;AAAE,SAAO;AAAQ;AAQtD,MAAM,MAAM;AAAA;AAAA,EAEjB,WAAW,MAAmB;AAC5B,QAAI,WAAW,KAAM,SAAQ,IAAI,EAAE,QAAQ,WAAW,GAAG,GAAG,IAAI;AAAA,EAClE;AAAA;AAAA,EAEA,QAAQ,MAAmB;AACzB,QAAI,WAAW,QAAQ,WAAW,OAAQ,SAAQ,IAAI,EAAE,QAAQ,QAAQ,GAAG,GAAG,IAAI;AAAA,EACpF;AAAA;AAAA,EAEA,QAAQ,MAAmB;AACzB,QAAI,WAAW,QAAQ,WAAW,OAAQ,SAAQ,KAAK,EAAE,UAAU,QAAQ,GAAG,GAAG,IAAI;AAAA,EACvF;AAAA;AAAA,EAEA,SAAS,MAAmB;AAC1B,QAAI,WAAW,MAAO,SAAQ,MAAM,EAAE,OAAO,SAAS,GAAG,GAAG,IAAI;AAAA,EAClE;AACF;",
6
- "names": []
7
- }
@@ -1,51 +0,0 @@
1
- /**
2
- * metadata.ts — Legacy Metadata Helpers
3
- *
4
- * @deprecated Use the `useHtml()` hook instead.
5
- *
6
- * This module provided an earlier metadata API where pages exported a
7
- * `metadata` object alongside their default component. It is retained for
8
- * backwards compatibility but new projects should use useHtml().
9
- *
10
- * Example (old pattern):
11
- * export const metadata = {
12
- * title: 'My Page',
13
- * scripts: [{ src: '/analytics.js', defer: true }],
14
- * };
15
- */
16
- import type { ScriptTag } from './html-store';
17
- export type { ScriptTag };
18
- /**
19
- * StyleTag for the legacy metadata API.
20
- * Extends html-store's StyleTag with `href` for external stylesheet links,
21
- * which the metadata API supported but html-store's <style>-only type does not.
22
- */
23
- export interface StyleTag {
24
- href?: string;
25
- content?: string;
26
- media?: string;
27
- }
28
- export interface Metadata {
29
- title?: string;
30
- scripts?: ScriptTag[];
31
- styles?: StyleTag[];
32
- }
33
- /**
34
- * Dynamically imports a page/layout module and returns its exported `metadata`
35
- * object, or an empty object if none is found or the import fails.
36
- */
37
- export declare function loadMetadata(filePath: string): Promise<Metadata>;
38
- /**
39
- * Merges metadata from an array of modules in render order (outermost layout
40
- * first, page last).
41
- *
42
- * Merge strategy:
43
- * title — last non-empty value wins (page overrides layout)
44
- * scripts — concatenated in order
45
- * styles — concatenated in order
46
- */
47
- export declare function mergeMetadata(ordered: Metadata[]): Required<Metadata>;
48
- /** Renders a ScriptTag to an HTML string. */
49
- export declare function renderScriptTag(s: ScriptTag): string;
50
- /** Renders a StyleTag to an HTML string. */
51
- export declare function renderStyleTag(s: StyleTag): string;
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/metadata.ts"],
4
- "sourcesContent": ["/**\r\n * metadata.ts \u2014 Legacy Metadata Helpers\r\n *\r\n * @deprecated Use the `useHtml()` hook instead.\r\n *\r\n * This module provided an earlier metadata API where pages exported a\r\n * `metadata` object alongside their default component. It is retained for\r\n * backwards compatibility but new projects should use useHtml().\r\n *\r\n * Example (old pattern):\r\n * export const metadata = {\r\n * title: 'My Page',\r\n * scripts: [{ src: '/analytics.js', defer: true }],\r\n * };\r\n */\r\n\r\nimport { pathToFileURL } from 'url';\r\nimport { escapeHtml } from './utils';\r\n// ScriptTag is fully compatible with html-store's definition.\r\nimport type { ScriptTag } from './html-store';\r\nexport type { ScriptTag };\r\n\r\n/**\r\n * StyleTag for the legacy metadata API.\r\n * Extends html-store's StyleTag with `href` for external stylesheet links,\r\n * which the metadata API supported but html-store's <style>-only type does not.\r\n */\r\nexport interface StyleTag {\r\n href?: string;\r\n content?: string;\r\n media?: string;\r\n}\r\n\r\nexport interface Metadata {\r\n title?: string;\r\n scripts?: ScriptTag[];\r\n styles?: StyleTag[];\r\n}\r\n\r\n/**\r\n * Dynamically imports a page/layout module and returns its exported `metadata`\r\n * object, or an empty object if none is found or the import fails.\r\n */\r\nexport async function loadMetadata(filePath: string): Promise<Metadata> {\r\n try {\r\n const mod = await import(pathToFileURL(filePath).href);\r\n return (mod.metadata as Metadata) ?? {};\r\n } catch {\r\n return {};\r\n }\r\n}\r\n\r\n/**\r\n * Merges metadata from an array of modules in render order (outermost layout\r\n * first, page last).\r\n *\r\n * Merge strategy:\r\n * title \u2014 last non-empty value wins (page overrides layout)\r\n * scripts \u2014 concatenated in order\r\n * styles \u2014 concatenated in order\r\n */\r\nexport function mergeMetadata(ordered: Metadata[]): Required<Metadata> {\r\n const result: Required<Metadata> = { title: '', scripts: [], styles: [] };\r\n for (const m of ordered) {\r\n if (m.title) result.title = m.title;\r\n if (m.scripts?.length) result.scripts.push(...m.scripts);\r\n if (m.styles?.length) result.styles.push(...m.styles);\r\n }\r\n return result;\r\n}\r\n\r\n/** Renders a ScriptTag to an HTML string. */\r\nexport function renderScriptTag(s: ScriptTag): string {\r\n if (s.src) {\r\n const attrs = [\r\n `src=\"${escapeHtml(s.src)}\"`,\r\n s.type ? `type=\"${escapeHtml(s.type)}\"` : '',\r\n s.defer ? 'defer' : '',\r\n s.async ? 'async' : '',\r\n ].filter(Boolean).join(' ');\r\n return `<script ${attrs}></script>`;\r\n }\r\n const typeAttr = s.type ? ` type=\"${escapeHtml(s.type)}\"` : '';\r\n return `<script${typeAttr}>${s.content ?? ''}</script>`;\r\n}\r\n\r\n/** Renders a StyleTag to an HTML string. */\r\nexport function renderStyleTag(s: StyleTag): string {\r\n if (s.href) return `<link rel=\"stylesheet\" href=\"${escapeHtml(s.href)}\" />`;\r\n return `<style>${s.content ?? ''}</style>`;\r\n}"],
5
- "mappings": "AAgBA,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AA0B3B,eAAsB,aAAa,UAAqC;AACtE,MAAI;AACF,UAAM,MAAM,MAAM,OAAO,cAAc,QAAQ,EAAE;AACjD,WAAQ,IAAI,YAAyB,CAAC;AAAA,EACxC,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAWO,SAAS,cAAc,SAAyC;AACrE,QAAM,SAA6B,EAAE,OAAO,IAAI,SAAS,CAAC,GAAG,QAAQ,CAAC,EAAE;AACxE,aAAW,KAAK,SAAS;AACvB,QAAI,EAAE,MAAO,QAAO,QAAQ,EAAE;AAC9B,QAAI,EAAE,SAAS,OAAQ,QAAO,QAAQ,KAAK,GAAG,EAAE,OAAO;AACvD,QAAI,EAAE,QAAQ,OAAQ,QAAO,OAAO,KAAK,GAAG,EAAE,MAAM;AAAA,EACtD;AACA,SAAO;AACT;AAGO,SAAS,gBAAgB,GAAsB;AACpD,MAAI,EAAE,KAAK;AACT,UAAM,QAAQ;AAAA,MACZ,QAAQ,WAAW,EAAE,GAAG,CAAC;AAAA,MACzB,EAAE,OAAO,SAAS,WAAW,EAAE,IAAI,CAAC,MAAM;AAAA,MAC1C,EAAE,QAAQ,UAAU;AAAA,MACpB,EAAE,QAAQ,UAAU;AAAA,IACtB,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAC1B,WAAO,WAAW,KAAK;AAAA,EACzB;AACA,QAAM,WAAW,EAAE,OAAO,UAAU,WAAW,EAAE,IAAI,CAAC,MAAM;AAC5D,SAAO,UAAU,QAAQ,IAAI,EAAE,WAAW,EAAE;AAC9C;AAGO,SAAS,eAAe,GAAqB;AAClD,MAAI,EAAE,KAAM,QAAO,gCAAgC,WAAW,EAAE,IAAI,CAAC;AACrE,SAAO,UAAU,EAAE,WAAW,EAAE;AAClC;",
6
- "names": []
7
- }
@@ -1,50 +0,0 @@
1
- /**
2
- * middleware-loader.ts — Middleware Chain Manager
3
- *
4
- * Loads and runs the NukeJS middleware stack. Two layers are supported:
5
- *
6
- * 1. Built-in middleware — shipped with the nukejs package.
7
- * Currently handles the /__hmr and /__hmr.js
8
- * routes required by the HMR client.
9
- * Located next to this file as `middleware.ts`
10
- * (or `middleware.js` in the compiled dist/).
11
- *
12
- * 2. User middleware — `middleware.ts` in the project root (cwd).
13
- * Runs after the built-in layer so it can inspect
14
- * or short-circuit every incoming request, including
15
- * API and page routes.
16
- *
17
- * Each middleware function receives (req, res) and may either:
18
- * - End the response (res.end / res.json) to short-circuit further handling.
19
- * - Return without touching res to pass control to the next layer.
20
- *
21
- * runMiddleware() returns `true` if any middleware ended the response,
22
- * allowing app.ts to skip its own routing logic.
23
- *
24
- * Restart behaviour:
25
- * When nuke.config.ts or middleware.ts change in dev, app.ts restarts the
26
- * process. The new process calls loadMiddleware() fresh so stale module
27
- * caches are not an issue.
28
- */
29
- import type { IncomingMessage, ServerResponse } from 'http';
30
- export type MiddlewareFunction = (req: IncomingMessage, res: ServerResponse) => Promise<void> | void;
31
- /**
32
- * Discovers and loads all middleware in priority order:
33
- * 1. Built-in (this package's own middleware.ts / middleware.js)
34
- * 2. User-supplied (cwd/middleware.ts)
35
- *
36
- * Duplicate paths (e.g. if cwd === package dir in a monorepo) are deduplicated
37
- * via a Set so the same file is never loaded twice.
38
- *
39
- * Should be called once at startup after the config is loaded.
40
- */
41
- export declare function loadMiddleware(): Promise<void>;
42
- /**
43
- * Runs all loaded middleware in registration order.
44
- *
45
- * Stops and returns `true` as soon as any middleware ends or sends a response
46
- * (res.writableEnded or res.headersSent), allowing app.ts to skip routing.
47
- *
48
- * Returns `false` if no middleware handled the request.
49
- */
50
- export declare function runMiddleware(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/middleware-loader.ts"],
4
- "sourcesContent": ["/**\r\n * middleware-loader.ts \u2014 Middleware Chain Manager\r\n *\r\n * Loads and runs the NukeJS middleware stack. Two layers are supported:\r\n *\r\n * 1. Built-in middleware \u2014 shipped with the nukejs package.\r\n * Currently handles the /__hmr and /__hmr.js\r\n * routes required by the HMR client.\r\n * Located next to this file as `middleware.ts`\r\n * (or `middleware.js` in the compiled dist/).\r\n *\r\n * 2. User middleware \u2014 `middleware.ts` in the project root (cwd).\r\n * Runs after the built-in layer so it can inspect\r\n * or short-circuit every incoming request, including\r\n * API and page routes.\r\n *\r\n * Each middleware function receives (req, res) and may either:\r\n * - End the response (res.end / res.json) to short-circuit further handling.\r\n * - Return without touching res to pass control to the next layer.\r\n *\r\n * runMiddleware() returns `true` if any middleware ended the response,\r\n * allowing app.ts to skip its own routing logic.\r\n *\r\n * Restart behaviour:\r\n * When nuke.config.ts or middleware.ts change in dev, app.ts restarts the\r\n * process. The new process calls loadMiddleware() fresh so stale module\r\n * caches are not an issue.\r\n */\r\n\r\nimport path from 'path';\r\nimport fs from 'fs';\r\nimport { pathToFileURL, fileURLToPath } from 'url';\r\nimport type { IncomingMessage, ServerResponse } from 'http';\r\nimport { log } from './logger';\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 type MiddlewareFunction = (\r\n req: IncomingMessage,\r\n res: ServerResponse,\r\n) => Promise<void> | void;\r\n\r\n// \u2500\u2500\u2500 Internal state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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/** Ordered list of loaded middleware functions. Populated by loadMiddleware(). */\r\nconst middlewares: MiddlewareFunction[] = [];\r\n\r\n// \u2500\u2500\u2500 Loader 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\r\n\r\n/**\r\n * Attempts to import a middleware file and push its default export onto the\r\n * stack. Skips silently if the file doesn't exist. Logs a warning if the\r\n * file exists but doesn't export a default function.\r\n */\r\nasync function loadMiddlewareFromPath(middlewarePath: string): Promise<void> {\r\n if (!fs.existsSync(middlewarePath)) {\r\n log.verbose(`No middleware found at ${middlewarePath}, skipping`);\r\n return;\r\n }\r\n\r\n try {\r\n const mod = await import(pathToFileURL(middlewarePath).href);\r\n if (typeof mod.default === 'function') {\r\n middlewares.push(mod.default);\r\n log.info(`Middleware loaded from ${middlewarePath}`);\r\n } else {\r\n log.warn(`${middlewarePath} does not export a default function`);\r\n }\r\n } catch (error) {\r\n log.error(`Error loading middleware from ${middlewarePath}:`, error);\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Public API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Discovers and loads all middleware in priority order:\r\n * 1. Built-in (this package's own middleware.ts / middleware.js)\r\n * 2. User-supplied (cwd/middleware.ts)\r\n *\r\n * Duplicate paths (e.g. if cwd === package dir in a monorepo) are deduplicated\r\n * via a Set so the same file is never loaded twice.\r\n *\r\n * Should be called once at startup after the config is loaded.\r\n */\r\nexport async function loadMiddleware(): Promise<void> {\r\n // __dirname equivalent in ESM.\r\n const appDir = path.dirname(fileURLToPath(import.meta.url));\r\n\r\n // The built-in middleware handles /__hmr and /__hmr.js for the HMR client.\r\n const builtinPath = path.join(\r\n appDir,\r\n `middleware.${appDir.endsWith('dist') ? 'js' : 'ts'}`,\r\n );\r\n\r\n const userPath = path.join(process.cwd(), 'middleware.ts');\r\n\r\n // Deduplicate in case the two paths resolve to the same file.\r\n const paths = [...new Set([builtinPath, userPath])];\r\n\r\n for (const middlewarePath of paths) {\r\n await loadMiddlewareFromPath(middlewarePath);\r\n }\r\n}\r\n\r\n/**\r\n * Runs all loaded middleware in registration order.\r\n *\r\n * Stops and returns `true` as soon as any middleware ends or sends a response\r\n * (res.writableEnded or res.headersSent), allowing app.ts to skip routing.\r\n *\r\n * Returns `false` if no middleware handled the request.\r\n */\r\nexport async function runMiddleware(\r\n req: IncomingMessage,\r\n res: ServerResponse,\r\n): Promise<boolean> {\r\n if (middlewares.length === 0) return false;\r\n\r\n for (const middleware of middlewares) {\r\n await middleware(req, res);\r\n\r\n if (res.writableEnded || res.headersSent) {\r\n log.verbose('Middleware handled request, skipping further processing');\r\n return true;\r\n }\r\n }\r\n\r\n return false;\r\n}\r\n"],
5
- "mappings": "AA6BA,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,SAAS,eAAe,qBAAqB;AAE7C,SAAS,WAAW;AAYpB,MAAM,cAAoC,CAAC;AAS3C,eAAe,uBAAuB,gBAAuC;AAC3E,MAAI,CAAC,GAAG,WAAW,cAAc,GAAG;AAClC,QAAI,QAAQ,0BAA0B,cAAc,YAAY;AAChE;AAAA,EACF;AAEA,MAAI;AACF,UAAM,MAAM,MAAM,OAAO,cAAc,cAAc,EAAE;AACvD,QAAI,OAAO,IAAI,YAAY,YAAY;AACrC,kBAAY,KAAK,IAAI,OAAO;AAC5B,UAAI,KAAK,0BAA0B,cAAc,EAAE;AAAA,IACrD,OAAO;AACL,UAAI,KAAK,GAAG,cAAc,qCAAqC;AAAA,IACjE;AAAA,EACF,SAAS,OAAO;AACd,QAAI,MAAM,iCAAiC,cAAc,KAAK,KAAK;AAAA,EACrE;AACF;AAcA,eAAsB,iBAAgC;AAEpD,QAAM,SAAS,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;AAG1D,QAAM,cAAc,KAAK;AAAA,IACvB;AAAA,IACA,cAAc,OAAO,SAAS,MAAM,IAAI,OAAO,IAAI;AAAA,EACrD;AAEA,QAAM,WAAW,KAAK,KAAK,QAAQ,IAAI,GAAG,eAAe;AAGzD,QAAM,QAAQ,CAAC,GAAG,oBAAI,IAAI,CAAC,aAAa,QAAQ,CAAC,CAAC;AAElD,aAAW,kBAAkB,OAAO;AAClC,UAAM,uBAAuB,cAAc;AAAA,EAC7C;AACF;AAUA,eAAsB,cACpB,KACA,KACkB;AAClB,MAAI,YAAY,WAAW,EAAG,QAAO;AAErC,aAAW,cAAc,aAAa;AACpC,UAAM,WAAW,KAAK,GAAG;AAEzB,QAAI,IAAI,iBAAiB,IAAI,aAAa;AACxC,UAAI,QAAQ,yDAAyD;AACrE,aAAO;AAAA,IACT;AAAA,EACF;AAEA,SAAO;AACT;",
6
- "names": []
7
- }
@@ -1,22 +0,0 @@
1
- /**
2
- * middleware.ts — Built-In NukeJS Middleware
3
- *
4
- * This is the internal middleware loaded before any user-defined middleware.
5
- * It handles three responsibilities:
6
- *
7
- * 1. Static public files (app/public/**)
8
- * Any file placed in app/public/ is served at its path relative to
9
- * that directory. E.g. app/public/favicon.ico → GET /favicon.ico.
10
- * The correct Content-Type is set automatically. Path traversal attempts
11
- * are rejected with 400.
12
- *
13
- * 2. HMR client script (/__hmr.js)
14
- * Builds and serves hmr-bundle.ts on demand. Injected into every dev
15
- * page as <script type="module" src="/__hmr.js">.
16
- *
17
- * 3. HMR SSE stream (/__hmr)
18
- * Long-lived Server-Sent Events connection used by the browser to receive
19
- * reload/replace/restart events when source files change.
20
- */
21
- import type { IncomingMessage, ServerResponse } from 'http';
22
- export default function middleware(req: IncomingMessage, res: ServerResponse): Promise<void>;