@timber-js/app 0.1.35 → 0.1.36

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.
@@ -1 +1 @@
1
- {"version":3,"file":"nitro.d.ts","sourceRoot":"","sources":["../../src/adapters/nitro.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,qBAAqB,EAAgB,MAAM,SAAS,CAAC;AAqBnE;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GACnB,QAAQ,GACR,aAAa,GACb,SAAS,GACT,cAAc,GACd,YAAY,GACZ,aAAa,GACb,iBAAiB,GACjB,aAAa,GACb,KAAK,CAAC;AAEV,2CAA2C;AAC3C,UAAU,YAAY;IACpB,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,iBAAiB,EAAE,OAAO,CAAC;IAC3B,sEAAsE;IACtE,kBAAkB,EAAE,OAAO,CAAC;IAC5B,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAuED,qCAAqC;AACrC,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IAErB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAID;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,KAAK,CAAC,OAAO,GAAE,mBAAwB,GAAG,qBAAqB,CA0E9E;AAID,sCAAsC;AACtC,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,WAAW,EACnB,eAAe,UAAQ,GACtB,MAAM,CA6CR;AAED,sCAAsC;AACtC,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,WAAW,EACnB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAyBR;AAOD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,MAAM,CAgKnF;AAED,wEAAwE;AACxE,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACb;AAED,sCAAsC;AACtC,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,WAAW,GAClB,mBAAmB,GAAG,IAAI,CAY5B;AAuCD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,YAAY,CAEjE"}
1
+ {"version":3,"file":"nitro.d.ts","sourceRoot":"","sources":["../../src/adapters/nitro.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,qBAAqB,EAAgB,MAAM,SAAS,CAAC;AAqBnE;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GACnB,QAAQ,GACR,aAAa,GACb,SAAS,GACT,cAAc,GACd,YAAY,GACZ,aAAa,GACb,iBAAiB,GACjB,aAAa,GACb,KAAK,CAAC;AAEV,2CAA2C;AAC3C,UAAU,YAAY;IACpB,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,iBAAiB,EAAE,OAAO,CAAC;IAC3B,sEAAsE;IACtE,kBAAkB,EAAE,OAAO,CAAC;IAC5B,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAuED,qCAAqC;AACrC,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IAErB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAID;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,KAAK,CAAC,OAAO,GAAE,mBAAwB,GAAG,qBAAqB,CA0E9E;AAID,sCAAsC;AACtC,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,WAAW,EACnB,eAAe,UAAQ,GACtB,MAAM,CA8CR;AAED,sCAAsC;AACtC,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,WAAW,EACnB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAyBR;AAOD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,MAAM,CAgKnF;AAED,wEAAwE;AACxE,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACb;AAED,sCAAsC;AACtC,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,WAAW,GAClB,mBAAmB,GAAG,IAAI,CAY5B;AAsDD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,YAAY,CAEjE"}
@@ -1,5 +1,4 @@
1
1
  import { join, relative } from "node:path";
2
- import { createRequire } from "node:module";
3
2
  import { cp, mkdir, writeFile } from "node:fs/promises";
4
3
  import { execFile } from "node:child_process";
5
4
  //#region src/adapters/nitro.ts
@@ -120,7 +119,7 @@ function nitro(options = {}) {
120
119
  await writeFile(join(outDir, "entry.ts"), entry);
121
120
  const nitroConfig = generateNitroConfig(preset, options.nitroConfig);
122
121
  await writeFile(join(outDir, "nitro.config.ts"), nitroConfig);
123
- await runNitroBuild(outDir);
122
+ await runNitroBuild(outDir, preset, options.nitroConfig);
124
123
  },
125
124
  preview: LOCALLY_PREVIEWABLE.has(preset) ? async (_config, buildDir) => {
126
125
  const previewScript = generatePreviewScript(buildDir, preset);
@@ -145,7 +144,7 @@ function generateNitroEntry(buildDir, outDir, preset, hasManifestInit = false) {
145
144
  return `// Generated by @timber-js/app/adapters/nitro
146
145
  // Do not edit — this file is regenerated on each build.
147
146
 
148
- ${hasManifestInit ? "import './_timber-manifest-init.js'\n" : ""}import { defineEventHandler, toWebRequest, sendWebResponse } from 'nitro/h3'
147
+ ${hasManifestInit ? "import './_timber-manifest-init.js'\n" : ""}import { defineEventHandler, sendWebResponse } from 'nitro/h3'
149
148
  import handler, { runWithEarlyHintsSender } from '${serverEntryRelative}'
150
149
 
151
150
  // Set TIMBER_RUNTIME for instrumentation.ts conditional SDK initialization.
@@ -153,7 +152,8 @@ import handler, { runWithEarlyHintsSender } from '${serverEntryRelative}'
153
152
  process.env.TIMBER_RUNTIME = '${runtimeName}'
154
153
 
155
154
  export default defineEventHandler(async (event) => {
156
- const webRequest = toWebRequest(event)
155
+ // h3 v2: event.request is a standard Web Request
156
+ const webRequest = event.request
157
157
  ${earlyHints ? ` const nodeRes = event.node?.res
158
158
  const earlyHintsSender = (typeof nodeRes?.writeEarlyHints === 'function')
159
159
  ? (links) => { try { nodeRes.writeEarlyHints({ link: links }) } catch {} }
@@ -366,21 +366,28 @@ function generateNitroPreviewCommand(buildDir, preset) {
366
366
  };
367
367
  }
368
368
  /**
369
- * Run the Nitro production build.
370
- * Resolves the `nitro` CLI binary from node_modules and runs `nitro build`
371
- * in the given directory, which reads nitro.config.ts and produces
372
- * .output/server/index.mjs (for node-server preset).
369
+ * Run the Nitro production build using the programmatic API.
370
+ * Uses dynamic import so nitro is only loaded at build time.
371
+ * Externalizes the timber RSC/SSR output those files are pre-built
372
+ * by timber and have internal references that nitro's bundler can't follow.
373
373
  */
374
- function runNitroBuild(nitroDir) {
375
- const nitroBin = join(createRequire(import.meta.url).resolve("nitro/package.json"), "..", "dist", "cli", "index.mjs");
376
- return new Promise((resolve, reject) => {
377
- const child = execFile("node", [nitroBin, "build"], { cwd: nitroDir }, (err) => {
378
- if (err) reject(/* @__PURE__ */ new Error(`Nitro build failed: ${err.message}`));
379
- else resolve();
380
- });
381
- child.stdout?.pipe(process.stdout);
382
- child.stderr?.pipe(process.stderr);
374
+ async function runNitroBuild(nitroDir, preset, userConfig) {
375
+ const presetConfig = PRESET_CONFIGS[preset];
376
+ const { createNitro, build: nitroBuild, prepare, copyPublicAssets } = await import("nitro");
377
+ const nitro = await createNitro({
378
+ rootDir: nitroDir,
379
+ preset: presetConfig.nitroPreset,
380
+ entry: join(nitroDir, "entry.ts"),
381
+ output: { dir: join(nitroDir, presetConfig.outputDir) },
382
+ routeRules: { "/assets/**": { headers: { "Cache-Control": IMMUTABLE_CACHE } } },
383
+ rollupConfig: { external: [/\.\.\/rsc\//, /\.\/_timber-manifest-init/] },
384
+ ...presetConfig.extraConfig,
385
+ ...userConfig
383
386
  });
387
+ await prepare(nitro);
388
+ await copyPublicAssets(nitro);
389
+ await nitroBuild(nitro);
390
+ await nitro.close();
384
391
  }
385
392
  /** Spawn a Nitro preview process and pipe stdio. */
386
393
  function spawnNitroPreview(command, args, cwd) {
@@ -1 +1 @@
1
- {"version":3,"file":"nitro.js","names":[],"sources":["../../src/adapters/nitro.ts"],"sourcesContent":["// Nitro adapter — multi-platform deployment\n//\n// Covers everything except Cloudflare Workers: Node.js, Bun, Vercel,\n// Netlify, AWS Lambda, Deno Deploy, Azure Functions. Nitro handles\n// compression, graceful shutdown, static file serving, and platform quirks.\n// See design/11-platform.md and design/25-production-deployments.md.\n\nimport { writeFile, mkdir, cp } from 'node:fs/promises';\nimport { execFile, execFileSync } from 'node:child_process';\nimport { join, relative } from 'node:path';\nimport { createRequire } from 'node:module';\nimport type { TimberPlatformAdapter, TimberConfig } from './types';\n// Inlined from server/asset-headers.ts — adapters are loaded by Node at\n// Vite startup time, before Vite's module resolver is available, so cross-\n// directory .ts imports don't resolve.\nconst IMMUTABLE_CACHE = 'public, max-age=31536000, immutable';\nconst STATIC_CACHE = 'public, max-age=3600, must-revalidate';\n\nfunction generateHeadersFile(): string {\n return `# Auto-generated by @timber-js/app — static asset cache headers.\n# See design/25-production-deployments.md §\"CDN / Edge Cache\"\n\n/assets/*\n Cache-Control: ${IMMUTABLE_CACHE}\n\n/*\n Cache-Control: ${STATIC_CACHE}\n`;\n}\n\n// ─── Presets ─────────────────────────────────────────────────────────────────\n\n/**\n * Supported Nitro deployment presets.\n *\n * Each preset maps to a Nitro deployment target. The adapter generates\n * the appropriate configuration and entry point for the selected platform.\n */\nexport type NitroPreset =\n | 'vercel'\n | 'vercel-edge'\n | 'netlify'\n | 'netlify-edge'\n | 'aws-lambda'\n | 'deno-deploy'\n | 'azure-functions'\n | 'node-server'\n | 'bun';\n\n/** Preset-specific Nitro configuration. */\ninterface PresetConfig {\n /** Nitro preset name passed to the Nitro build. */\n nitroPreset: string;\n /** Output directory name within the build dir. */\n outputDir: string;\n /** Whether the runtime supports waitUntil. */\n supportsWaitUntil: boolean;\n /** Whether the runtime supports application-level 103 Early Hints. */\n supportsEarlyHints: boolean;\n /** Value for TIMBER_RUNTIME env var. See design/25-production-deployments.md. */\n runtimeName: string;\n /** Additional nitro.config fields for this preset. */\n extraConfig?: Record<string, unknown>;\n}\n\nconst PRESET_CONFIGS: Record<NitroPreset, PresetConfig> = {\n 'vercel': {\n nitroPreset: 'vercel',\n outputDir: '.vercel/output',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'vercel',\n extraConfig: { vercel: { functions: { maxDuration: 30 } } },\n },\n 'vercel-edge': {\n nitroPreset: 'vercel-edge',\n outputDir: '.vercel/output',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'vercel-edge',\n },\n 'netlify': {\n nitroPreset: 'netlify',\n outputDir: '.netlify/functions-internal',\n supportsWaitUntil: false,\n supportsEarlyHints: false,\n runtimeName: 'netlify',\n },\n 'netlify-edge': {\n nitroPreset: 'netlify-edge',\n outputDir: '.netlify/edge-functions',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'netlify-edge',\n },\n 'aws-lambda': {\n nitroPreset: 'aws-lambda',\n outputDir: '.output',\n supportsWaitUntil: false,\n supportsEarlyHints: false,\n runtimeName: 'aws-lambda',\n },\n 'deno-deploy': {\n nitroPreset: 'deno-deploy',\n outputDir: '.output',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'deno-deploy',\n },\n 'azure-functions': {\n nitroPreset: 'azure-functions',\n outputDir: '.output',\n supportsWaitUntil: false,\n supportsEarlyHints: false,\n runtimeName: 'azure-functions',\n },\n 'node-server': {\n nitroPreset: 'node-server',\n outputDir: '.output',\n supportsWaitUntil: true,\n supportsEarlyHints: true,\n runtimeName: 'node-server',\n },\n 'bun': {\n nitroPreset: 'bun',\n outputDir: '.output',\n supportsWaitUntil: true,\n supportsEarlyHints: true,\n runtimeName: 'bun',\n },\n};\n\n// ─── Options ─────────────────────────────────────────────────────────────────\n\n/** Options for the Nitro adapter. */\nexport interface NitroAdapterOptions {\n /**\n * Deployment preset. Determines the target platform.\n * @default 'node-server'\n */\n preset?: NitroPreset;\n\n /**\n * Additional Nitro configuration to merge into the generated config.\n * Overrides default values for the selected preset.\n */\n nitroConfig?: Record<string, unknown>;\n}\n\n// ─── Adapter ─────────────────────────────────────────────────────────────────\n\n/**\n * Create a Nitro-based adapter for multi-platform deployment.\n *\n * Nitro abstracts deployment targets — the same timber.js app can deploy\n * to Vercel, Netlify, AWS, Deno Deploy, or Azure by changing the preset.\n *\n * @example\n * ```ts\n * import { nitro } from '@timber-js/app/adapters/nitro'\n *\n * export default {\n * output: 'server',\n * adapter: nitro({ preset: 'vercel' }),\n * }\n * ```\n */\nexport function nitro(options: NitroAdapterOptions = {}): TimberPlatformAdapter {\n const preset = options.preset ?? 'node-server';\n const presetConfig = PRESET_CONFIGS[preset];\n const pendingPromises: Promise<unknown>[] = [];\n\n return {\n name: `nitro-${preset}`,\n\n async buildOutput(config: TimberConfig, buildDir: string) {\n const outDir = join(buildDir, 'nitro');\n await mkdir(outDir, { recursive: true });\n\n // Copy client assets to public directory.\n // When client JavaScript is disabled, skip .js files — only CSS,\n // fonts, images, and other static assets are needed.\n const clientDir = join(buildDir, 'client');\n const publicDir = join(outDir, 'public');\n await mkdir(publicDir, { recursive: true });\n await cp(clientDir, publicDir, {\n recursive: true,\n filter: config.clientJavascriptDisabled ? (src: string) => !src.endsWith('.js') : undefined,\n }).catch(() => {\n // Client dir may not exist when client JavaScript is disabled\n });\n\n // Write _headers file for platforms that support it (Netlify, etc.).\n // See design/25-production-deployments.md §\"CDN / Edge Cache\"\n await writeFile(join(publicDir, '_headers'), generateHeadersFile());\n\n // Write the build manifest init module (if manifest data was produced).\n if (config.manifestInit) {\n await writeFile(join(outDir, '_timber-manifest-init.js'), config.manifestInit);\n }\n\n // Generate the Nitro entry point\n const hasManifestInit = !!config.manifestInit;\n const entry = generateNitroEntry(buildDir, outDir, preset, hasManifestInit);\n await writeFile(join(outDir, 'entry.ts'), entry);\n\n // Generate the Nitro config with static asset cache rules\n const nitroConfig = generateNitroConfig(preset, options.nitroConfig);\n await writeFile(join(outDir, 'nitro.config.ts'), nitroConfig);\n\n // Run the Nitro build to produce a production-ready server bundle.\n // The output goes to dist/nitro/.output/server/index.mjs (for node-server preset).\n await runNitroBuild(outDir);\n },\n\n // Only presets that produce a locally-runnable server get preview().\n // Serverless presets (vercel, netlify, aws-lambda, etc.) have no\n // local runtime — Vite's built-in preview is the fallback.\n preview: LOCALLY_PREVIEWABLE.has(preset)\n ? async (_config: TimberConfig, buildDir: string) => {\n // Generate a standalone preview server that uses Node's built-in\n // HTTP server. The Nitro entry.ts can't be run directly because\n // it imports h3 (a Nitro dependency not available at runtime).\n const previewScript = generatePreviewScript(buildDir, preset);\n const scriptPath = join(buildDir, 'nitro', '_preview-server.mjs');\n await writeFile(scriptPath, previewScript);\n\n const command = preset === 'bun' ? 'bun' : 'node';\n await spawnNitroPreview(command, [scriptPath], join(buildDir, 'nitro'));\n }\n : undefined,\n\n waitUntil: presetConfig.supportsWaitUntil\n ? (promise: Promise<unknown>) => {\n const tracked = promise.catch((err) => {\n console.error('[timber] waitUntil promise rejected:', err);\n });\n pendingPromises.push(tracked);\n }\n : undefined,\n };\n}\n\n// ─── Entry Generation ────────────────────────────────────────────────────────\n\n/** @internal Exported for testing. */\nexport function generateNitroEntry(\n buildDir: string,\n outDir: string,\n preset: NitroPreset,\n hasManifestInit = false\n): string {\n // The RSC entry is the main request handler — it exports the fetch handler as default.\n // The Vite RSC plugin outputs it to rsc/index.js.\n let serverEntryRelative = relative(outDir, join(buildDir, 'rsc', 'index.js'));\n // Ensure the import path starts with ./ for ESM compatibility\n if (!serverEntryRelative.startsWith('.')) {\n serverEntryRelative = './' + serverEntryRelative;\n }\n const runtimeName = PRESET_CONFIGS[preset].runtimeName;\n const earlyHints = PRESET_CONFIGS[preset].supportsEarlyHints;\n\n // Build manifest init must be imported before the handler so that\n // globalThis.__TIMBER_BUILD_MANIFEST__ is set when the virtual module evaluates.\n const manifestImport = hasManifestInit ? \"import './_timber-manifest-init.js'\\n\" : '';\n\n // On node-server and bun, wrap the handler with ALS so the pipeline\n // can send 103 Early Hints via res.writeEarlyHints(). Other presets\n // either don't support 103 or handle it at the CDN level.\n const handlerCall = earlyHints\n ? ` const nodeRes = event.node?.res\n const earlyHintsSender = (typeof nodeRes?.writeEarlyHints === 'function')\n ? (links) => { try { nodeRes.writeEarlyHints({ link: links }) } catch {} }\n : undefined\n\n const webResponse = earlyHintsSender\n ? await runWithEarlyHintsSender(earlyHintsSender, () => handler(webRequest))\n : await handler(webRequest)`\n : ` const webResponse = await handler(webRequest)`;\n\n return `// Generated by @timber-js/app/adapters/nitro\n// Do not edit — this file is regenerated on each build.\n\n${manifestImport}import { defineEventHandler, toWebRequest, sendWebResponse } from 'nitro/h3'\nimport handler, { runWithEarlyHintsSender } from '${serverEntryRelative}'\n\n// Set TIMBER_RUNTIME for instrumentation.ts conditional SDK initialization.\n// See design/25-production-deployments.md §\"TIMBER_RUNTIME\".\nprocess.env.TIMBER_RUNTIME = '${runtimeName}'\n\nexport default defineEventHandler(async (event) => {\n const webRequest = toWebRequest(event)\n${handlerCall}\n return sendWebResponse(event, webResponse)\n})\n`;\n}\n\n/** @internal Exported for testing. */\nexport function generateNitroConfig(\n preset: NitroPreset,\n userConfig?: Record<string, unknown>\n): string {\n const presetConfig = PRESET_CONFIGS[preset];\n\n const config: Record<string, unknown> = {\n preset: presetConfig.nitroPreset,\n entry: './entry.ts',\n output: { dir: presetConfig.outputDir },\n // Static asset cache headers — hashed assets are immutable, others get 1h.\n // See design/25-production-deployments.md §\"CDN / Edge Cache\"\n routeRules: {\n '/assets/**': { headers: { 'Cache-Control': IMMUTABLE_CACHE } },\n },\n ...presetConfig.extraConfig,\n ...userConfig,\n };\n\n const configJson = JSON.stringify(config, null, 2);\n\n return `// Generated by @timber-js/app/adapters/nitro\n// Do not edit — this file is regenerated on each build.\n\nimport { defineNitroConfig } from 'nitro/config'\n\nexport default defineNitroConfig(${configJson})\n`;\n}\n\n// ─── Preview ─────────────────────────────────────────────────────────────────\n\n/** Presets that produce a locally-runnable server entry. */\nconst LOCALLY_PREVIEWABLE = new Set<NitroPreset>(['node-server', 'bun']);\n\n/**\n * Generate a standalone preview server script that uses Node's built-in\n * HTTP server. This bypasses Nitro entirely — the Nitro entry.ts imports\n * h3 which isn't available outside a Nitro build. For local preview we\n * just need to serve static files and route requests to the RSC handler.\n *\n * @internal Exported for testing.\n */\nexport function generatePreviewScript(buildDir: string, preset: NitroPreset): string {\n const rscEntryRelative = relative(join(buildDir, 'nitro'), join(buildDir, 'rsc', 'index.js'));\n const rscEntry = rscEntryRelative.startsWith('.') ? rscEntryRelative : './' + rscEntryRelative;\n const publicDir = './public';\n const manifestInitPath = './_timber-manifest-init.js';\n const runtimeName = PRESET_CONFIGS[preset].runtimeName;\n\n return `// Generated by @timber-js/app — standalone preview server.\n// Uses Node's built-in HTTP server to serve static assets and route\n// dynamic requests through the RSC handler. No Nitro/h3 dependency.\n\nimport { createServer } from 'node:http';\nimport { readFile, stat } from 'node:fs/promises';\nimport { join, extname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { existsSync } from 'node:fs';\n\n// Set runtime before importing the handler.\nprocess.env.TIMBER_RUNTIME = '${runtimeName}';\n\n// Load the build manifest if it exists.\nconst __dirname = fileURLToPath(new URL('.', import.meta.url));\nconst manifestPath = join(__dirname, '${manifestInitPath}');\nif (existsSync(manifestPath)) {\n await import('${manifestInitPath}');\n}\n\n// Import the RSC handler (default export is the fetch-like handler).\nconst { default: handler, runWithEarlyHintsSender } = await import('${rscEntry}');\n\nconst MIME_TYPES = {\n '.html': 'text/html',\n '.js': 'application/javascript',\n '.mjs': 'application/javascript',\n '.css': 'text/css',\n '.json': 'application/json',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.svg': 'image/svg+xml',\n '.ico': 'image/x-icon',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.ttf': 'font/ttf',\n '.otf': 'font/otf',\n '.webp': 'image/webp',\n '.avif': 'image/avif',\n '.webm': 'video/webm',\n '.mp4': 'video/mp4',\n '.txt': 'text/plain',\n '.xml': 'application/xml',\n '.wasm': 'application/wasm',\n};\n\nconst publicDir = join(__dirname, '${publicDir}');\nconst port = parseInt(process.env.PORT || '3000', 10);\nconst host = process.env.HOST || process.env.HOSTNAME || 'localhost';\n\nconst server = createServer(async (req, res) => {\n const url = new URL(req.url || '/', \\`http://\\${host}:\\${port}\\`);\n\n // Try serving static files from the public directory first.\n const filePath = join(publicDir, url.pathname);\n // Prevent path traversal.\n if (filePath.startsWith(publicDir)) {\n try {\n const fileStat = await stat(filePath);\n if (fileStat.isFile()) {\n const ext = extname(filePath);\n const contentType = MIME_TYPES[ext] || 'application/octet-stream';\n const body = await readFile(filePath);\n // Hashed assets get immutable cache, others get short cache.\n const cacheControl = url.pathname.startsWith('/assets/')\n ? 'public, max-age=31536000, immutable'\n : 'public, max-age=3600, must-revalidate';\n res.writeHead(200, {\n 'Content-Type': contentType,\n 'Content-Length': body.length,\n 'Cache-Control': cacheControl,\n });\n res.end(body);\n return;\n }\n } catch {\n // File not found — fall through to the RSC handler.\n }\n }\n\n // Convert Node request to Web Request.\n const headers = new Headers();\n for (const [key, value] of Object.entries(req.headers)) {\n if (value) {\n if (Array.isArray(value)) {\n for (const v of value) headers.append(key, v);\n } else {\n headers.set(key, value);\n }\n }\n }\n\n let body = undefined;\n if (req.method !== 'GET' && req.method !== 'HEAD') {\n body = await new Promise((resolve) => {\n const chunks = [];\n req.on('data', (chunk) => chunks.push(chunk));\n req.on('end', () => resolve(Buffer.concat(chunks)));\n });\n }\n\n const webRequest = new Request(url.href, {\n method: req.method,\n headers,\n body,\n duplex: body ? 'half' : undefined,\n });\n\n try {\n // Support 103 Early Hints when available.\n const earlyHintsSender = (typeof res.writeEarlyHints === 'function')\n ? (links) => { try { res.writeEarlyHints({ link: links }); } catch {} }\n : undefined;\n\n const webResponse = earlyHintsSender && runWithEarlyHintsSender\n ? await runWithEarlyHintsSender(earlyHintsSender, () => handler(webRequest))\n : await handler(webRequest);\n\n // Write the response back to the Node response.\n res.writeHead(webResponse.status, Object.fromEntries(webResponse.headers.entries()));\n\n if (webResponse.body) {\n const reader = webResponse.body.getReader();\n const pump = async () => {\n while (true) {\n const { done, value } = await reader.read();\n if (done) { res.end(); return; }\n res.write(value);\n }\n };\n await pump();\n } else {\n res.end();\n }\n } catch (err) {\n console.error('[timber preview] Request error:', err);\n if (!res.headersSent) {\n res.writeHead(500, { 'Content-Type': 'text/plain' });\n }\n res.end('Internal Server Error');\n }\n});\n\nserver.listen(port, host, () => {\n console.log();\n console.log(' ⚡ timber preview server running at:');\n console.log();\n console.log(\\` ➜ http://\\${host}:\\${port}\\`);\n console.log();\n});\n`;\n}\n\n/** Command descriptor for Nitro preview — testable without spawning. */\nexport interface NitroPreviewCommand {\n command: string;\n args: string[];\n cwd: string;\n}\n\n/** @internal Exported for testing. */\nexport function generateNitroPreviewCommand(\n buildDir: string,\n preset: NitroPreset\n): NitroPreviewCommand | null {\n if (!LOCALLY_PREVIEWABLE.has(preset)) return null;\n\n const nitroDir = join(buildDir, 'nitro');\n const entryPath = join(nitroDir, 'entry.ts');\n\n const command = preset === 'bun' ? 'bun' : 'node';\n return {\n command,\n args: [entryPath],\n cwd: nitroDir,\n };\n}\n\n/**\n * Run the Nitro production build.\n * Resolves the `nitro` CLI binary from node_modules and runs `nitro build`\n * in the given directory, which reads nitro.config.ts and produces\n * .output/server/index.mjs (for node-server preset).\n */\nfunction runNitroBuild(nitroDir: string): Promise<void> {\n // Resolve the nitro CLI binary from the dependency graph rather than\n // relying on npx, which may not be available in all environments.\n const require = createRequire(import.meta.url);\n const nitroPkg = require.resolve('nitro/package.json');\n const nitroBin = join(nitroPkg, '..', 'dist', 'cli', 'index.mjs');\n\n return new Promise<void>((resolve, reject) => {\n const child = execFile('node', [nitroBin, 'build'], { cwd: nitroDir }, (err) => {\n if (err) reject(new Error(`Nitro build failed: ${err.message}`));\n else resolve();\n });\n child.stdout?.pipe(process.stdout);\n child.stderr?.pipe(process.stderr);\n });\n}\n\n/** Spawn a Nitro preview process and pipe stdio. */\nfunction spawnNitroPreview(command: string, args: string[], cwd: string): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const child = execFile(command, args, { cwd }, (err) => {\n if (err) reject(err);\n else resolve();\n });\n child.stdout?.pipe(process.stdout);\n child.stderr?.pipe(process.stderr);\n });\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\n/**\n * Get the preset configuration for a given preset name.\n * @internal Exported for testing.\n */\nexport function getPresetConfig(preset: NitroPreset): PresetConfig {\n return PRESET_CONFIGS[preset];\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"nitro.js","names":[],"sources":["../../src/adapters/nitro.ts"],"sourcesContent":["// Nitro adapter — multi-platform deployment\n//\n// Covers everything except Cloudflare Workers: Node.js, Bun, Vercel,\n// Netlify, AWS Lambda, Deno Deploy, Azure Functions. Nitro handles\n// compression, graceful shutdown, static file serving, and platform quirks.\n// See design/11-platform.md and design/25-production-deployments.md.\n\nimport { writeFile, mkdir, cp } from 'node:fs/promises';\nimport { execFile } from 'node:child_process';\nimport { join, relative } from 'node:path';\nimport type { TimberPlatformAdapter, TimberConfig } from './types';\n// Inlined from server/asset-headers.ts — adapters are loaded by Node at\n// Vite startup time, before Vite's module resolver is available, so cross-\n// directory .ts imports don't resolve.\nconst IMMUTABLE_CACHE = 'public, max-age=31536000, immutable';\nconst STATIC_CACHE = 'public, max-age=3600, must-revalidate';\n\nfunction generateHeadersFile(): string {\n return `# Auto-generated by @timber-js/app — static asset cache headers.\n# See design/25-production-deployments.md §\"CDN / Edge Cache\"\n\n/assets/*\n Cache-Control: ${IMMUTABLE_CACHE}\n\n/*\n Cache-Control: ${STATIC_CACHE}\n`;\n}\n\n// ─── Presets ─────────────────────────────────────────────────────────────────\n\n/**\n * Supported Nitro deployment presets.\n *\n * Each preset maps to a Nitro deployment target. The adapter generates\n * the appropriate configuration and entry point for the selected platform.\n */\nexport type NitroPreset =\n | 'vercel'\n | 'vercel-edge'\n | 'netlify'\n | 'netlify-edge'\n | 'aws-lambda'\n | 'deno-deploy'\n | 'azure-functions'\n | 'node-server'\n | 'bun';\n\n/** Preset-specific Nitro configuration. */\ninterface PresetConfig {\n /** Nitro preset name passed to the Nitro build. */\n nitroPreset: string;\n /** Output directory name within the build dir. */\n outputDir: string;\n /** Whether the runtime supports waitUntil. */\n supportsWaitUntil: boolean;\n /** Whether the runtime supports application-level 103 Early Hints. */\n supportsEarlyHints: boolean;\n /** Value for TIMBER_RUNTIME env var. See design/25-production-deployments.md. */\n runtimeName: string;\n /** Additional nitro.config fields for this preset. */\n extraConfig?: Record<string, unknown>;\n}\n\nconst PRESET_CONFIGS: Record<NitroPreset, PresetConfig> = {\n 'vercel': {\n nitroPreset: 'vercel',\n outputDir: '.vercel/output',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'vercel',\n extraConfig: { vercel: { functions: { maxDuration: 30 } } },\n },\n 'vercel-edge': {\n nitroPreset: 'vercel-edge',\n outputDir: '.vercel/output',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'vercel-edge',\n },\n 'netlify': {\n nitroPreset: 'netlify',\n outputDir: '.netlify/functions-internal',\n supportsWaitUntil: false,\n supportsEarlyHints: false,\n runtimeName: 'netlify',\n },\n 'netlify-edge': {\n nitroPreset: 'netlify-edge',\n outputDir: '.netlify/edge-functions',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'netlify-edge',\n },\n 'aws-lambda': {\n nitroPreset: 'aws-lambda',\n outputDir: '.output',\n supportsWaitUntil: false,\n supportsEarlyHints: false,\n runtimeName: 'aws-lambda',\n },\n 'deno-deploy': {\n nitroPreset: 'deno-deploy',\n outputDir: '.output',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'deno-deploy',\n },\n 'azure-functions': {\n nitroPreset: 'azure-functions',\n outputDir: '.output',\n supportsWaitUntil: false,\n supportsEarlyHints: false,\n runtimeName: 'azure-functions',\n },\n 'node-server': {\n nitroPreset: 'node-server',\n outputDir: '.output',\n supportsWaitUntil: true,\n supportsEarlyHints: true,\n runtimeName: 'node-server',\n },\n 'bun': {\n nitroPreset: 'bun',\n outputDir: '.output',\n supportsWaitUntil: true,\n supportsEarlyHints: true,\n runtimeName: 'bun',\n },\n};\n\n// ─── Options ─────────────────────────────────────────────────────────────────\n\n/** Options for the Nitro adapter. */\nexport interface NitroAdapterOptions {\n /**\n * Deployment preset. Determines the target platform.\n * @default 'node-server'\n */\n preset?: NitroPreset;\n\n /**\n * Additional Nitro configuration to merge into the generated config.\n * Overrides default values for the selected preset.\n */\n nitroConfig?: Record<string, unknown>;\n}\n\n// ─── Adapter ─────────────────────────────────────────────────────────────────\n\n/**\n * Create a Nitro-based adapter for multi-platform deployment.\n *\n * Nitro abstracts deployment targets — the same timber.js app can deploy\n * to Vercel, Netlify, AWS, Deno Deploy, or Azure by changing the preset.\n *\n * @example\n * ```ts\n * import { nitro } from '@timber-js/app/adapters/nitro'\n *\n * export default {\n * output: 'server',\n * adapter: nitro({ preset: 'vercel' }),\n * }\n * ```\n */\nexport function nitro(options: NitroAdapterOptions = {}): TimberPlatformAdapter {\n const preset = options.preset ?? 'node-server';\n const presetConfig = PRESET_CONFIGS[preset];\n const pendingPromises: Promise<unknown>[] = [];\n\n return {\n name: `nitro-${preset}`,\n\n async buildOutput(config: TimberConfig, buildDir: string) {\n const outDir = join(buildDir, 'nitro');\n await mkdir(outDir, { recursive: true });\n\n // Copy client assets to public directory.\n // When client JavaScript is disabled, skip .js files — only CSS,\n // fonts, images, and other static assets are needed.\n const clientDir = join(buildDir, 'client');\n const publicDir = join(outDir, 'public');\n await mkdir(publicDir, { recursive: true });\n await cp(clientDir, publicDir, {\n recursive: true,\n filter: config.clientJavascriptDisabled ? (src: string) => !src.endsWith('.js') : undefined,\n }).catch(() => {\n // Client dir may not exist when client JavaScript is disabled\n });\n\n // Write _headers file for platforms that support it (Netlify, etc.).\n // See design/25-production-deployments.md §\"CDN / Edge Cache\"\n await writeFile(join(publicDir, '_headers'), generateHeadersFile());\n\n // Write the build manifest init module (if manifest data was produced).\n if (config.manifestInit) {\n await writeFile(join(outDir, '_timber-manifest-init.js'), config.manifestInit);\n }\n\n // Generate the Nitro entry point\n const hasManifestInit = !!config.manifestInit;\n const entry = generateNitroEntry(buildDir, outDir, preset, hasManifestInit);\n await writeFile(join(outDir, 'entry.ts'), entry);\n\n // Generate the Nitro config with static asset cache rules\n const nitroConfig = generateNitroConfig(preset, options.nitroConfig);\n await writeFile(join(outDir, 'nitro.config.ts'), nitroConfig);\n\n // Run the Nitro build to produce a production-ready server bundle.\n // The output goes to dist/nitro/.output/server/index.mjs (for node-server preset).\n await runNitroBuild(outDir, preset, options.nitroConfig);\n },\n\n // Only presets that produce a locally-runnable server get preview().\n // Serverless presets (vercel, netlify, aws-lambda, etc.) have no\n // local runtime — Vite's built-in preview is the fallback.\n preview: LOCALLY_PREVIEWABLE.has(preset)\n ? async (_config: TimberConfig, buildDir: string) => {\n // Generate a standalone preview server that uses Node's built-in\n // HTTP server. The Nitro entry.ts can't be run directly because\n // it imports h3 (a Nitro dependency not available at runtime).\n const previewScript = generatePreviewScript(buildDir, preset);\n const scriptPath = join(buildDir, 'nitro', '_preview-server.mjs');\n await writeFile(scriptPath, previewScript);\n\n const command = preset === 'bun' ? 'bun' : 'node';\n await spawnNitroPreview(command, [scriptPath], join(buildDir, 'nitro'));\n }\n : undefined,\n\n waitUntil: presetConfig.supportsWaitUntil\n ? (promise: Promise<unknown>) => {\n const tracked = promise.catch((err) => {\n console.error('[timber] waitUntil promise rejected:', err);\n });\n pendingPromises.push(tracked);\n }\n : undefined,\n };\n}\n\n// ─── Entry Generation ────────────────────────────────────────────────────────\n\n/** @internal Exported for testing. */\nexport function generateNitroEntry(\n buildDir: string,\n outDir: string,\n preset: NitroPreset,\n hasManifestInit = false\n): string {\n // The RSC entry is the main request handler — it exports the fetch handler as default.\n // The Vite RSC plugin outputs it to rsc/index.js.\n let serverEntryRelative = relative(outDir, join(buildDir, 'rsc', 'index.js'));\n // Ensure the import path starts with ./ for ESM compatibility\n if (!serverEntryRelative.startsWith('.')) {\n serverEntryRelative = './' + serverEntryRelative;\n }\n const runtimeName = PRESET_CONFIGS[preset].runtimeName;\n const earlyHints = PRESET_CONFIGS[preset].supportsEarlyHints;\n\n // Build manifest init must be imported before the handler so that\n // globalThis.__TIMBER_BUILD_MANIFEST__ is set when the virtual module evaluates.\n const manifestImport = hasManifestInit ? \"import './_timber-manifest-init.js'\\n\" : '';\n\n // On node-server and bun, wrap the handler with ALS so the pipeline\n // can send 103 Early Hints via res.writeEarlyHints(). Other presets\n // either don't support 103 or handle it at the CDN level.\n const handlerCall = earlyHints\n ? ` const nodeRes = event.node?.res\n const earlyHintsSender = (typeof nodeRes?.writeEarlyHints === 'function')\n ? (links) => { try { nodeRes.writeEarlyHints({ link: links }) } catch {} }\n : undefined\n\n const webResponse = earlyHintsSender\n ? await runWithEarlyHintsSender(earlyHintsSender, () => handler(webRequest))\n : await handler(webRequest)`\n : ` const webResponse = await handler(webRequest)`;\n\n return `// Generated by @timber-js/app/adapters/nitro\n// Do not edit — this file is regenerated on each build.\n\n${manifestImport}import { defineEventHandler, sendWebResponse } from 'nitro/h3'\nimport handler, { runWithEarlyHintsSender } from '${serverEntryRelative}'\n\n// Set TIMBER_RUNTIME for instrumentation.ts conditional SDK initialization.\n// See design/25-production-deployments.md §\"TIMBER_RUNTIME\".\nprocess.env.TIMBER_RUNTIME = '${runtimeName}'\n\nexport default defineEventHandler(async (event) => {\n // h3 v2: event.request is a standard Web Request\n const webRequest = event.request\n${handlerCall}\n return sendWebResponse(event, webResponse)\n})\n`;\n}\n\n/** @internal Exported for testing. */\nexport function generateNitroConfig(\n preset: NitroPreset,\n userConfig?: Record<string, unknown>\n): string {\n const presetConfig = PRESET_CONFIGS[preset];\n\n const config: Record<string, unknown> = {\n preset: presetConfig.nitroPreset,\n entry: './entry.ts',\n output: { dir: presetConfig.outputDir },\n // Static asset cache headers — hashed assets are immutable, others get 1h.\n // See design/25-production-deployments.md §\"CDN / Edge Cache\"\n routeRules: {\n '/assets/**': { headers: { 'Cache-Control': IMMUTABLE_CACHE } },\n },\n ...presetConfig.extraConfig,\n ...userConfig,\n };\n\n const configJson = JSON.stringify(config, null, 2);\n\n return `// Generated by @timber-js/app/adapters/nitro\n// Do not edit — this file is regenerated on each build.\n\nimport { defineNitroConfig } from 'nitro/config'\n\nexport default defineNitroConfig(${configJson})\n`;\n}\n\n// ─── Preview ─────────────────────────────────────────────────────────────────\n\n/** Presets that produce a locally-runnable server entry. */\nconst LOCALLY_PREVIEWABLE = new Set<NitroPreset>(['node-server', 'bun']);\n\n/**\n * Generate a standalone preview server script that uses Node's built-in\n * HTTP server. This bypasses Nitro entirely — the Nitro entry.ts imports\n * h3 which isn't available outside a Nitro build. For local preview we\n * just need to serve static files and route requests to the RSC handler.\n *\n * @internal Exported for testing.\n */\nexport function generatePreviewScript(buildDir: string, preset: NitroPreset): string {\n const rscEntryRelative = relative(join(buildDir, 'nitro'), join(buildDir, 'rsc', 'index.js'));\n const rscEntry = rscEntryRelative.startsWith('.') ? rscEntryRelative : './' + rscEntryRelative;\n const publicDir = './public';\n const manifestInitPath = './_timber-manifest-init.js';\n const runtimeName = PRESET_CONFIGS[preset].runtimeName;\n\n return `// Generated by @timber-js/app — standalone preview server.\n// Uses Node's built-in HTTP server to serve static assets and route\n// dynamic requests through the RSC handler. No Nitro/h3 dependency.\n\nimport { createServer } from 'node:http';\nimport { readFile, stat } from 'node:fs/promises';\nimport { join, extname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { existsSync } from 'node:fs';\n\n// Set runtime before importing the handler.\nprocess.env.TIMBER_RUNTIME = '${runtimeName}';\n\n// Load the build manifest if it exists.\nconst __dirname = fileURLToPath(new URL('.', import.meta.url));\nconst manifestPath = join(__dirname, '${manifestInitPath}');\nif (existsSync(manifestPath)) {\n await import('${manifestInitPath}');\n}\n\n// Import the RSC handler (default export is the fetch-like handler).\nconst { default: handler, runWithEarlyHintsSender } = await import('${rscEntry}');\n\nconst MIME_TYPES = {\n '.html': 'text/html',\n '.js': 'application/javascript',\n '.mjs': 'application/javascript',\n '.css': 'text/css',\n '.json': 'application/json',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.svg': 'image/svg+xml',\n '.ico': 'image/x-icon',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.ttf': 'font/ttf',\n '.otf': 'font/otf',\n '.webp': 'image/webp',\n '.avif': 'image/avif',\n '.webm': 'video/webm',\n '.mp4': 'video/mp4',\n '.txt': 'text/plain',\n '.xml': 'application/xml',\n '.wasm': 'application/wasm',\n};\n\nconst publicDir = join(__dirname, '${publicDir}');\nconst port = parseInt(process.env.PORT || '3000', 10);\nconst host = process.env.HOST || process.env.HOSTNAME || 'localhost';\n\nconst server = createServer(async (req, res) => {\n const url = new URL(req.url || '/', \\`http://\\${host}:\\${port}\\`);\n\n // Try serving static files from the public directory first.\n const filePath = join(publicDir, url.pathname);\n // Prevent path traversal.\n if (filePath.startsWith(publicDir)) {\n try {\n const fileStat = await stat(filePath);\n if (fileStat.isFile()) {\n const ext = extname(filePath);\n const contentType = MIME_TYPES[ext] || 'application/octet-stream';\n const body = await readFile(filePath);\n // Hashed assets get immutable cache, others get short cache.\n const cacheControl = url.pathname.startsWith('/assets/')\n ? 'public, max-age=31536000, immutable'\n : 'public, max-age=3600, must-revalidate';\n res.writeHead(200, {\n 'Content-Type': contentType,\n 'Content-Length': body.length,\n 'Cache-Control': cacheControl,\n });\n res.end(body);\n return;\n }\n } catch {\n // File not found — fall through to the RSC handler.\n }\n }\n\n // Convert Node request to Web Request.\n const headers = new Headers();\n for (const [key, value] of Object.entries(req.headers)) {\n if (value) {\n if (Array.isArray(value)) {\n for (const v of value) headers.append(key, v);\n } else {\n headers.set(key, value);\n }\n }\n }\n\n let body = undefined;\n if (req.method !== 'GET' && req.method !== 'HEAD') {\n body = await new Promise((resolve) => {\n const chunks = [];\n req.on('data', (chunk) => chunks.push(chunk));\n req.on('end', () => resolve(Buffer.concat(chunks)));\n });\n }\n\n const webRequest = new Request(url.href, {\n method: req.method,\n headers,\n body,\n duplex: body ? 'half' : undefined,\n });\n\n try {\n // Support 103 Early Hints when available.\n const earlyHintsSender = (typeof res.writeEarlyHints === 'function')\n ? (links) => { try { res.writeEarlyHints({ link: links }); } catch {} }\n : undefined;\n\n const webResponse = earlyHintsSender && runWithEarlyHintsSender\n ? await runWithEarlyHintsSender(earlyHintsSender, () => handler(webRequest))\n : await handler(webRequest);\n\n // Write the response back to the Node response.\n res.writeHead(webResponse.status, Object.fromEntries(webResponse.headers.entries()));\n\n if (webResponse.body) {\n const reader = webResponse.body.getReader();\n const pump = async () => {\n while (true) {\n const { done, value } = await reader.read();\n if (done) { res.end(); return; }\n res.write(value);\n }\n };\n await pump();\n } else {\n res.end();\n }\n } catch (err) {\n console.error('[timber preview] Request error:', err);\n if (!res.headersSent) {\n res.writeHead(500, { 'Content-Type': 'text/plain' });\n }\n res.end('Internal Server Error');\n }\n});\n\nserver.listen(port, host, () => {\n console.log();\n console.log(' ⚡ timber preview server running at:');\n console.log();\n console.log(\\` ➜ http://\\${host}:\\${port}\\`);\n console.log();\n});\n`;\n}\n\n/** Command descriptor for Nitro preview — testable without spawning. */\nexport interface NitroPreviewCommand {\n command: string;\n args: string[];\n cwd: string;\n}\n\n/** @internal Exported for testing. */\nexport function generateNitroPreviewCommand(\n buildDir: string,\n preset: NitroPreset\n): NitroPreviewCommand | null {\n if (!LOCALLY_PREVIEWABLE.has(preset)) return null;\n\n const nitroDir = join(buildDir, 'nitro');\n const entryPath = join(nitroDir, 'entry.ts');\n\n const command = preset === 'bun' ? 'bun' : 'node';\n return {\n command,\n args: [entryPath],\n cwd: nitroDir,\n };\n}\n\n/**\n * Run the Nitro production build using the programmatic API.\n * Uses dynamic import so nitro is only loaded at build time.\n * Externalizes the timber RSC/SSR output — those files are pre-built\n * by timber and have internal references that nitro's bundler can't follow.\n */\nasync function runNitroBuild(\n nitroDir: string,\n preset: NitroPreset,\n userConfig?: Record<string, unknown>\n): Promise<void> {\n const presetConfig = PRESET_CONFIGS[preset];\n const { createNitro, build: nitroBuild, prepare, copyPublicAssets } = await import('nitro');\n\n const nitro = await createNitro({\n rootDir: nitroDir,\n preset: presetConfig.nitroPreset,\n entry: join(nitroDir, 'entry.ts'),\n output: { dir: join(nitroDir, presetConfig.outputDir) },\n routeRules: {\n '/assets/**': { headers: { 'Cache-Control': IMMUTABLE_CACHE } },\n },\n // Don't bundle the timber RSC/SSR build output — it has its own\n // internal file references that nitro's bundler can't follow.\n // Mark them as external so rollup leaves the imports as-is.\n rollupConfig: {\n external: [/\\.\\.\\/rsc\\//, /\\.\\/_timber-manifest-init/],\n },\n ...presetConfig.extraConfig,\n ...userConfig,\n });\n\n await prepare(nitro);\n await copyPublicAssets(nitro);\n await nitroBuild(nitro);\n await nitro.close();\n}\n\n/** Spawn a Nitro preview process and pipe stdio. */\nfunction spawnNitroPreview(command: string, args: string[], cwd: string): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n const child = execFile(command, args, { cwd }, (err) => {\n if (err) reject(err);\n else resolve();\n });\n child.stdout?.pipe(process.stdout);\n child.stderr?.pipe(process.stderr);\n });\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\n/**\n * Get the preset configuration for a given preset name.\n * @internal Exported for testing.\n */\nexport function getPresetConfig(preset: NitroPreset): PresetConfig {\n return PRESET_CONFIGS[preset];\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.1.35",
3
+ "version": "0.1.36",
4
4
  "description": "Vite-native React framework for Cloudflare Workers — correct HTTP semantics, real status codes, pages that work without JavaScript",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -6,9 +6,8 @@
6
6
  // See design/11-platform.md and design/25-production-deployments.md.
7
7
 
8
8
  import { writeFile, mkdir, cp } from 'node:fs/promises';
9
- import { execFile, execFileSync } from 'node:child_process';
9
+ import { execFile } from 'node:child_process';
10
10
  import { join, relative } from 'node:path';
11
- import { createRequire } from 'node:module';
12
11
  import type { TimberPlatformAdapter, TimberConfig } from './types';
13
12
  // Inlined from server/asset-headers.ts — adapters are loaded by Node at
14
13
  // Vite startup time, before Vite's module resolver is available, so cross-
@@ -210,7 +209,7 @@ export function nitro(options: NitroAdapterOptions = {}): TimberPlatformAdapter
210
209
 
211
210
  // Run the Nitro build to produce a production-ready server bundle.
212
211
  // The output goes to dist/nitro/.output/server/index.mjs (for node-server preset).
213
- await runNitroBuild(outDir);
212
+ await runNitroBuild(outDir, preset, options.nitroConfig);
214
213
  },
215
214
 
216
215
  // Only presets that produce a locally-runnable server get preview().
@@ -281,7 +280,7 @@ export function generateNitroEntry(
281
280
  return `// Generated by @timber-js/app/adapters/nitro
282
281
  // Do not edit — this file is regenerated on each build.
283
282
 
284
- ${manifestImport}import { defineEventHandler, toWebRequest, sendWebResponse } from 'nitro/h3'
283
+ ${manifestImport}import { defineEventHandler, sendWebResponse } from 'nitro/h3'
285
284
  import handler, { runWithEarlyHintsSender } from '${serverEntryRelative}'
286
285
 
287
286
  // Set TIMBER_RUNTIME for instrumentation.ts conditional SDK initialization.
@@ -289,7 +288,8 @@ import handler, { runWithEarlyHintsSender } from '${serverEntryRelative}'
289
288
  process.env.TIMBER_RUNTIME = '${runtimeName}'
290
289
 
291
290
  export default defineEventHandler(async (event) => {
292
- const webRequest = toWebRequest(event)
291
+ // h3 v2: event.request is a standard Web Request
292
+ const webRequest = event.request
293
293
  ${handlerCall}
294
294
  return sendWebResponse(event, webResponse)
295
295
  })
@@ -528,26 +528,41 @@ export function generateNitroPreviewCommand(
528
528
  }
529
529
 
530
530
  /**
531
- * Run the Nitro production build.
532
- * Resolves the `nitro` CLI binary from node_modules and runs `nitro build`
533
- * in the given directory, which reads nitro.config.ts and produces
534
- * .output/server/index.mjs (for node-server preset).
531
+ * Run the Nitro production build using the programmatic API.
532
+ * Uses dynamic import so nitro is only loaded at build time.
533
+ * Externalizes the timber RSC/SSR output those files are pre-built
534
+ * by timber and have internal references that nitro's bundler can't follow.
535
535
  */
536
- function runNitroBuild(nitroDir: string): Promise<void> {
537
- // Resolve the nitro CLI binary from the dependency graph rather than
538
- // relying on npx, which may not be available in all environments.
539
- const require = createRequire(import.meta.url);
540
- const nitroPkg = require.resolve('nitro/package.json');
541
- const nitroBin = join(nitroPkg, '..', 'dist', 'cli', 'index.mjs');
536
+ async function runNitroBuild(
537
+ nitroDir: string,
538
+ preset: NitroPreset,
539
+ userConfig?: Record<string, unknown>
540
+ ): Promise<void> {
541
+ const presetConfig = PRESET_CONFIGS[preset];
542
+ const { createNitro, build: nitroBuild, prepare, copyPublicAssets } = await import('nitro');
542
543
 
543
- return new Promise<void>((resolve, reject) => {
544
- const child = execFile('node', [nitroBin, 'build'], { cwd: nitroDir }, (err) => {
545
- if (err) reject(new Error(`Nitro build failed: ${err.message}`));
546
- else resolve();
547
- });
548
- child.stdout?.pipe(process.stdout);
549
- child.stderr?.pipe(process.stderr);
544
+ const nitro = await createNitro({
545
+ rootDir: nitroDir,
546
+ preset: presetConfig.nitroPreset,
547
+ entry: join(nitroDir, 'entry.ts'),
548
+ output: { dir: join(nitroDir, presetConfig.outputDir) },
549
+ routeRules: {
550
+ '/assets/**': { headers: { 'Cache-Control': IMMUTABLE_CACHE } },
551
+ },
552
+ // Don't bundle the timber RSC/SSR build output — it has its own
553
+ // internal file references that nitro's bundler can't follow.
554
+ // Mark them as external so rollup leaves the imports as-is.
555
+ rollupConfig: {
556
+ external: [/\.\.\/rsc\//, /\.\/_timber-manifest-init/],
557
+ },
558
+ ...presetConfig.extraConfig,
559
+ ...userConfig,
550
560
  });
561
+
562
+ await prepare(nitro);
563
+ await copyPublicAssets(nitro);
564
+ await nitroBuild(nitro);
565
+ await nitro.close();
551
566
  }
552
567
 
553
568
  /** Spawn a Nitro preview process and pipe stdio. */