@timber-js/app 0.2.0-alpha.182 → 0.2.0-alpha.184
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_chunks/{build-output-helper-DF5uScqS.js → build-output-helper-DOGYFb_X.js} +47 -9
- package/dist/_chunks/build-output-helper-DOGYFb_X.js.map +1 -0
- package/dist/_chunks/{cloudflare-AHoWYTYr.js → cloudflare-CnT5Lr7U.js} +21 -3
- package/dist/_chunks/{cloudflare-AHoWYTYr.js.map → cloudflare-CnT5Lr7U.js.map} +1 -1
- package/dist/_chunks/plugin-context---kTF5v8.js.map +1 -1
- package/dist/adapters/build-output-helper.d.ts +6 -0
- package/dist/adapters/build-output-helper.d.ts.map +1 -1
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/adapters/cloudflare.d.ts +10 -0
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +2 -2
- package/dist/adapters/nitro.d.ts +14 -0
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +14 -1
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/adapters/shared.d.ts +1 -1
- package/dist/adapters/shared.d.ts.map +1 -1
- package/dist/adapters/types.d.ts +14 -0
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/client/internal.js +113 -11
- package/dist/client/internal.js.map +1 -1
- package/dist/client/rsc-fetch.d.ts +9 -0
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/index.js +437 -200
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts +12 -0
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/build-report.d.ts +4 -3
- package/dist/plugins/build-report.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +55 -2
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +12 -0
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/sitemap-generator.d.ts +1 -1
- package/dist/server/sitemap-generator.d.ts.map +1 -1
- package/dist/server/static-generator.d.ts +106 -0
- package/dist/server/static-generator.d.ts.map +1 -0
- package/docs/learn/13-configuration.mdx +5 -1
- package/package.json +1 -1
- package/src/adapters/build-output-helper.ts +40 -1
- package/src/adapters/cloudflare.ts +43 -1
- package/src/adapters/nitro.ts +58 -2
- package/src/adapters/shared.ts +42 -11
- package/src/adapters/types.ts +14 -0
- package/src/client/browser-entry/index.ts +7 -1
- package/src/client/rsc-fetch.ts +187 -19
- package/src/index.ts +1 -1
- package/src/plugin-context.ts +12 -0
- package/src/plugins/adapter-build.ts +5 -0
- package/src/plugins/build-report.ts +6 -12
- package/src/plugins/static-build.ts +391 -2
- package/src/server/rsc-entry/index.ts +24 -0
- package/src/server/sitemap-generator.ts +1 -1
- package/src/server/static-generator.ts +692 -0
- package/dist/_chunks/build-output-helper-DF5uScqS.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-context---kTF5v8.js","names":[],"sources":["../../src/utils/startup-timer.ts","../../src/plugin-context.ts"],"sourcesContent":["/**\n * Startup timer — records named phases with their durations.\n *\n * Used by the plugin system to instrument cold start and report a\n * timing breakdown in dev mode. Zero overhead in production (disabled).\n *\n * See design/18-build-system.md, TIM-155.\n */\n\nimport { performance } from 'node:perf_hooks';\n\nexport interface PhaseRecord {\n name: string;\n startMs: number;\n durationMs: number;\n}\n\nexport interface StartupTimer {\n /** Mark the beginning of a named phase. */\n start(phase: string): void;\n /** Mark the end of a named phase. Returns duration in ms. */\n end(phase: string): number;\n /** Get all completed phase records, ordered by start time. */\n getPhases(): PhaseRecord[];\n /** Total elapsed time from first start() to last end(). */\n totalMs(): number;\n /** Format a human-readable summary string. */\n formatSummary(): string;\n}\n\n/**\n * Create a startup timer that records phase durations.\n */\nexport function createStartupTimer(): StartupTimer {\n const pending = new Map<string, number>();\n const phases: PhaseRecord[] = [];\n\n return {\n start(phase: string): void {\n pending.set(phase, performance.now());\n },\n\n end(phase: string): number {\n const startMs = pending.get(phase);\n if (startMs === undefined) {\n return 0;\n }\n pending.delete(phase);\n const durationMs = performance.now() - startMs;\n phases.push({ name: phase, startMs, durationMs });\n return durationMs;\n },\n\n getPhases(): PhaseRecord[] {\n return [...phases].sort((a, b) => a.startMs - b.startMs);\n },\n\n totalMs(): number {\n if (phases.length === 0) return 0;\n const sorted = this.getPhases();\n const first = sorted[0];\n const last = sorted[sorted.length - 1];\n return last.startMs + last.durationMs - first.startMs;\n },\n\n formatSummary(): string {\n const sorted = this.getPhases();\n if (sorted.length === 0) return 'No phases recorded.';\n\n const lines = sorted.map((p) => {\n const ms = p.durationMs.toFixed(1);\n return ` ${p.name.padEnd(30)} ${ms.padStart(8)}ms`;\n });\n\n const total = this.totalMs().toFixed(1);\n lines.push(` ${'total'.padEnd(30)} ${total.padStart(8)}ms`);\n\n return ['[timber] startup timing:', ...lines].join('\\n');\n },\n };\n}\n\n/**\n * No-op timer for production builds — all methods are empty.\n */\nexport function createNoopTimer(): StartupTimer {\n return {\n start() {},\n end() {\n return 0;\n },\n getPhases() {\n return [];\n },\n totalMs() {\n return 0;\n },\n formatSummary() {\n return '';\n },\n };\n}\n","/**\n * Plugin context — internal types and helpers for timber sub-plugins.\n *\n * These are NOT part of the user-facing API. They are consumed by\n * sub-plugins (routing, entries, shims, etc.) via relative imports.\n * The root entry point (index.ts) does not re-export them.\n *\n * Design doc: 18-build-system.md §\"Shared Plugin Context\"\n */\n\nimport { existsSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\nimport { createRequire } from 'node:module';\nimport { parseAst } from 'vite';\nimport type { RouteTree } from './routing/types';\nimport type { BuildManifest } from './server/build-manifest';\nimport type { CaptureSummary } from './server/prebuilt-builder';\nimport type { StartupTimer } from './utils/startup-timer';\nimport { createStartupTimer } from './utils/startup-timer';\nimport type { TimberUserConfig, ClientJavascriptConfig } from './config-types.js';\nimport type { HoldingServer } from './dev-tools/holding-server.js';\nimport type { ProgramNode } from './plugins/callsite-ast.js';\n\n// Re-export for sub-plugin convenience — they import from plugin-context.ts\nexport type { TimberUserConfig, ClientJavascriptConfig } from './config-types.js';\n\n// ── Client JavaScript config ─────────────────────────────────────────────\n\n/** Fully resolved client JavaScript configuration (no optionals). */\nexport interface ResolvedClientJavascript {\n disabled: boolean;\n enableHMRInDev: boolean;\n}\n\n/**\n * Resolve `clientJavascript` into a fully resolved config.\n */\nexport function resolveClientJavascript(config: TimberUserConfig): ResolvedClientJavascript {\n if (config.clientJavascript !== undefined) {\n if (typeof config.clientJavascript === 'boolean') {\n return {\n disabled: !config.clientJavascript,\n enableHMRInDev: !config.clientJavascript,\n };\n }\n return {\n disabled: config.clientJavascript.disabled,\n enableHMRInDev: config.clientJavascript.enableHMRInDev ?? config.clientJavascript.disabled,\n };\n }\n return { disabled: false, enableHMRInDev: false };\n}\n\n// ── Plugin Context ────────────────────────────────────────────────────────\n\n/**\n * Shared context object passed to all sub-plugins via closure.\n *\n * Sub-plugins communicate through this context — not through Vite's\n * plugin API or global state.\n * See design/18-build-system.md §\"Shared Plugin Context\".\n */\nexport interface PluginContext {\n config: TimberUserConfig;\n /** Resolved client JavaScript configuration */\n clientJavascript: ResolvedClientJavascript;\n /** The scanned route tree (populated by timber-routing, consumed by timber-entries) */\n routeTree: RouteTree | null;\n /** Absolute path to the app/ directory */\n appDir: string;\n /** Absolute path to the project root */\n root: string;\n /** Whether the dev server is running (set by timber-root-sync in configResolved) */\n dev: boolean;\n /** CSS build manifest (populated by adapter after client build, null in dev) */\n buildManifest: BuildManifest | null;\n /** Per-build deployment ID for version skew detection (null in dev) */\n deploymentId: string | null;\n /** Startup timer for profiling cold start phases (active in dev, no-op in prod) */\n timer: StartupTimer;\n /** Holding server that binds the port during dev startup (closed in timber-dev-server) */\n holdingServer?: HoldingServer | null;\n /**\n * Resolved absolute path to the build output directory.\n *\n * Defaults to `<root>/.timber/dist`. Can be overridden via\n * `timber.config.ts` `buildDir` or Vite's `build.outDir`.\n * Timber config takes precedence when both are set.\n *\n * Used by adapter-build and cli preview.\n */\n buildDir: string;\n /**\n * Per-route client reference keys captured during the RSC build.\n * Maps absolute route file paths to the set of client reference\n * hash keys they transitively import. Used by adapter-build to\n * populate per-route modulepreload entries from clientReferenceDeps.\n */\n routeClientRefKeys?: Map<string, Set<string>>;\n /**\n * Maps absolute module IDs of client references in non-facade RSC\n * chunks to the absolute paths of their consuming facade modules.\n * Built during the RSC `generateBundle` phase by timber-chunks,\n * consumed by `clientChunkGroup` to reclassify `shared:` modules\n * that are only used by a single route boundary.\n * See design/27-chunking-strategy.md, TIM-1107.\n */\n sharedModuleFacades?: Map<string, string[]>;\n /**\n * Module ID → prebuilt component IDs declared in it. Recorded by the\n * timber-prebuilt transform (RSC environment, production builds only),\n * consumed by the RSC `generateBundle` module-graph walk that computes\n * `routePrebuiltComponents`. See design/44-render-at-build-time.md.\n */\n prebuiltModuleComponents?: Map<string, string[]>;\n /**\n * Module ID → mapping of variable name to component ID for the prebuilt\n * components declared in that module. Used at `generateBundle` time to\n * trace imports from other modules back to specific component IDs.\n */\n prebuiltBindingNames?: Map<string, Map<string, string>>;\n /**\n * Component ID → distinct static call-site prop objects captured from\n * JSX usage sites during the transform and generateBundle phases.\n * Consumed by the post-build capture pass to render with actual props\n * instead of empty `{}`. See design/44 §Props Derivation.\n */\n prebuiltCallSiteProps?: Map<string, Record<string, unknown>[]>;\n /**\n * Route file path (page or layout, absolute) → prebuilt component IDs\n * transitively reachable from it. Computed during RSC `generateBundle`,\n * consumed by the post-build capture pass (TIM-1119) to decide which\n * components to render under which route's param combos.\n */\n routePrebuiltComponents?: Map<string, string[]>;\n /**\n * Page file paths that were desugared from `export const prerender` by\n * timber-prerender-sugar. The prebuilt transform uses this to verify\n * the generated `cache.component(...)` call was picked up.\n */\n prerenderSugarPages?: Set<string>;\n /**\n * Component ID → built chunk file name for prebuilt-declaring modules\n * that are only reachable via dynamic imports (import.meta.glob, lazy\n * routes). The capture pass imports these chunks to trigger\n * cache.component() registration before rendering. Populated during\n * RSC generateBundle alongside routePrebuiltComponents.\n */\n prebuiltLazyChunks?: Map<string, string>;\n /** Post-build prebuilt capture summary (populated by timber-prebuilt, consumed by build-report). */\n captureSummary?: CaptureSummary;\n /**\n * Content-keyed AST parse memo shared across cache/prebuilt/prerender-sugar\n * transforms. Collapses both cross-plugin and cross-environment duplicate\n * parses: identical code hits, rewritten code misses (correct because Vite\n * transforms chain — a rewrite produces different code). Bounded LRU (32\n * entries) so it doesn't hold programs for the whole build.\n *\n * Consumers MUST treat the returned ProgramNode as read-only — they collect\n * ranges and rewrite CODE via MagicString; the AST itself is never mutated.\n */\n parseCached?: ParseMemo;\n}\n\n// ── AST parse memo ───────────────────────────────────────────────────────\n\nconst PARSE_MEMO_CAPACITY = 32;\n\n/**\n * Content-keyed LRU memo for `parseAst`. Transforms for the same module run\n * back-to-back across plugins and environments, so a tiny window captures the\n * wins. The returned ProgramNode is shared — callers MUST NOT mutate it.\n */\nexport class ParseMemo {\n private cache = new Map<string, ProgramNode>();\n\n /**\n * Parse code, returning a cached ProgramNode if the same code was parsed\n * before. The returned AST MUST NOT be mutated — it may be shared across\n * multiple consumers.\n */\n parse(code: string): ProgramNode {\n const existing = this.cache.get(code);\n if (existing) {\n // LRU refresh: move to end\n this.cache.delete(code);\n this.cache.set(code, existing);\n return existing;\n }\n const program = parseAst(code, { lang: 'tsx' }) as unknown as ProgramNode;\n if (this.cache.size >= PARSE_MEMO_CAPACITY) {\n // Evict oldest (first inserted)\n const firstKey = this.cache.keys().next().value!;\n this.cache.delete(firstKey);\n }\n this.cache.set(code, program);\n return program;\n }\n\n get size(): number {\n return this.cache.size;\n }\n}\n\n// ── App directory resolution ──────────────────────────────────────────────\n\n/**\n * Resolve the app directory. Checks (in order):\n * 1. Explicit `configAppDir` from timber.config.ts\n * 2. `<root>/app`\n * 3. `<root>/src/app`\n *\n * Throws if none exist.\n */\nexport function resolveAppDir(root: string, configAppDir?: string): string {\n if (configAppDir) {\n const explicit = join(root, configAppDir);\n if (!existsSync(explicit)) {\n throw new Error(`[timber] Configured appDir \"${configAppDir}\" does not exist at ${explicit}`);\n }\n return explicit;\n }\n\n const rootApp = join(root, 'app');\n if (existsSync(rootApp)) return rootApp;\n\n const srcApp = join(root, 'src', 'app');\n if (existsSync(srcApp)) return srcApp;\n\n throw new Error(\n `[timber] Could not find app directory. Expected \"app/\" or \"src/app/\" in ${root}. ` +\n `You can set appDir in timber.config.ts to specify a custom location.`\n );\n}\n\n// ── Plugin context factory ────────────────────────────────────────────────\n\nexport function createPluginContext(config?: TimberUserConfig, root?: string): PluginContext {\n const projectRoot = root ?? process.cwd();\n const resolvedConfig: TimberUserConfig = { ...config };\n return {\n config: resolvedConfig,\n clientJavascript: resolveClientJavascript(resolvedConfig),\n routeTree: null,\n appDir: join(projectRoot, 'app'),\n root: projectRoot,\n dev: false,\n buildManifest: null,\n deploymentId: null,\n timer: createStartupTimer(),\n holdingServer: null,\n buildDir: resolveBuildDir(projectRoot, resolvedConfig.buildDir),\n parseCached: new ParseMemo(),\n };\n}\n\n// ── Build directory resolution ────────────────────────────────────────────\n\n/** Default build output directory (relative to root). */\nexport const DEFAULT_BUILD_DIR = join('.timber', 'dist');\n\n/**\n * Resolve the build output directory.\n *\n * Priority:\n * 1. Explicit `timberBuildDir` from timber.config.ts\n * 2. Explicit `viteBuildOutDir` from Vite's build.outDir (if not the default 'dist')\n * 3. Default: `.timber/dist`\n *\n * Returns an absolute path.\n */\nexport function resolveBuildDir(\n root: string,\n timberBuildDir?: string,\n viteBuildOutDir?: string\n): string {\n if (timberBuildDir) {\n return resolve(root, timberBuildDir);\n }\n if (viteBuildOutDir && viteBuildOutDir !== 'dist') {\n return resolve(root, viteBuildOutDir);\n }\n return join(root, DEFAULT_BUILD_DIR);\n}\n\n// ── Config file loading ───────────────────────────────────────────────────\n\n/**\n * Map known timber.config load failures to actionable hints (TIM-1040).\n * Returns null when the error has no known remedy beyond its own message.\n *\n * Known failure modes of loading timber.config.ts via native require():\n * - Node < 22.18 has no default type stripping → ERR_UNKNOWN_FILE_EXTENSION\n * - Non-erasable TS syntax (enums, namespaces, parameter properties)\n * → ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX / ERR_INVALID_TYPESCRIPT_SYNTAX\n * - ESM syntax in a `\"type\": \"commonjs\"` package → the stripped .ts is\n * parsed as CJS and throws SyntaxError: \"Unexpected token 'export'\"\n * (export statements) or \"Cannot use import statement outside a\n * module\" (import statements)\n */\nexport function configLoadHint(configName: string, err: unknown): string | null {\n const code = (err as NodeJS.ErrnoException | null)?.code;\n const isTs = configName.endsWith('.ts');\n\n if (isTs && code === 'ERR_UNKNOWN_FILE_EXTENSION') {\n return (\n 'Loading a TypeScript config requires Node.js 22.18+ (built-in type stripping). ' +\n 'Upgrade Node.js or rewrite the config as timber.config.js.'\n );\n }\n if (code === 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX' || code === 'ERR_INVALID_TYPESCRIPT_SYNTAX') {\n return (\n 'Node.js loads the config with type stripping, which only supports erasable TypeScript syntax — ' +\n 'replace enums, namespaces, and constructor parameter properties with plain objects and types.'\n );\n }\n if (\n isTs &&\n err instanceof SyntaxError &&\n /Unexpected token '(export|import)'|Cannot use import statement outside a module/.test(\n err.message\n )\n ) {\n return (\n 'The config uses ESM syntax but Node.js parsed it as CommonJS — ' +\n 'set \"type\": \"module\" in package.json, or use module.exports instead of export default.'\n );\n }\n return null;\n}\n\n/**\n * Load timber.config.ts (or .js, .mjs) from the project root.\n * Returns the config object or null if no config file is found.\n * Throws a descriptive error naming the file when a config file exists\n * but fails to load — a broken config must fail loudly, never silently\n * degrade (see TIM-1066). Known failure modes get an actionable hint\n * appended (see configLoadHint, TIM-1040).\n *\n * Uses require() — Node 22.12+ can require() ESM modules, and Node\n * 22.18+ strips types from .ts files by default (hence the engines\n * field requiring >=22.18.0). This keeps timber() synchronous — no\n * async config loading needed.\n * This is the single config loader — dev, build, and preview all use it.\n */\nexport function loadTimberConfigFile(root: string): TimberUserConfig | null {\n const configNames = ['timber.config.ts', 'timber.config.js', 'timber.config.mjs'];\n const req = createRequire(join(root, 'package.json'));\n\n for (const name of configNames) {\n const configPath = join(root, name);\n if (existsSync(configPath)) {\n try {\n const mod = req(configPath);\n return (mod.default ?? mod) as TimberUserConfig;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const hint = configLoadHint(name, err);\n throw new Error(\n `[timber] Failed to load ${configPath}: ${message}${hint ? `\\n${hint}` : ''}`,\n { cause: err }\n );\n }\n }\n }\n return null;\n}\n\n/**\n * Detect config keys set in both inline (vite.config.ts) and file (timber.config.ts)\n * and warn the user. The `output` key is excluded because it defaults to 'server'\n * in createPluginContext and would always appear as an inline key.\n *\n * Returns the list of conflicting key names (for testing).\n */\nexport function warnConfigConflicts(\n inline: TimberUserConfig,\n fileConfig: TimberUserConfig\n): string[] {\n const conflicts: string[] = [];\n for (const key of Object.keys(fileConfig) as (keyof TimberUserConfig)[]) {\n if (key in inline && inline[key] !== undefined) {\n conflicts.push(key);\n }\n }\n if (conflicts.length > 0) {\n console.warn(\n `[timber] Config conflict: ${conflicts.map((k) => `\"${k}\"`).join(', ')} set in both ` +\n `vite.config.ts (inline) and timber.config.ts. ` +\n `Move all config to timber.config.ts to avoid confusion. ` +\n `The inline value from vite.config.ts will be used.`\n );\n }\n return conflicts;\n}\n\n/**\n * Drop own keys whose value is `undefined` so they don't clobber file\n * config during spread merging. `timber({ buildDir: process.env.BUILD_DIR })`\n * with the var unset must not erase the timber.config.ts value.\n */\nfunction stripUndefined<T extends object>(obj: T): T {\n return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined)) as T;\n}\n\n/**\n * Merge file-based config (timber.config.ts) with inline config (vite.config.ts).\n * Inline config takes precedence — file config only fills in missing fields.\n * Inline keys explicitly set to `undefined` are treated as absent, matching\n * the `!== undefined` check in warnConfigConflicts.\n */\nexport function mergeFileConfig(\n inline: TimberUserConfig,\n fileConfig: TimberUserConfig\n): TimberUserConfig {\n warnConfigConflicts(inline, fileConfig);\n const definedInline = stripUndefined(inline);\n\n return {\n ...fileConfig,\n ...definedInline,\n ...(fileConfig.limits && definedInline.limits\n ? { limits: { ...fileConfig.limits, ...stripUndefined(definedInline.limits) } }\n : {}),\n ...(fileConfig.dev && definedInline.dev\n ? { dev: { ...fileConfig.dev, ...stripUndefined(definedInline.dev) } }\n : {}),\n ...(fileConfig.mdx && definedInline.mdx\n ? { mdx: { ...fileConfig.mdx, ...stripUndefined(definedInline.mdx) } }\n : {}),\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"plugin-context---kTF5v8.js","names":[],"sources":["../../src/utils/startup-timer.ts","../../src/plugin-context.ts"],"sourcesContent":["/**\n * Startup timer — records named phases with their durations.\n *\n * Used by the plugin system to instrument cold start and report a\n * timing breakdown in dev mode. Zero overhead in production (disabled).\n *\n * See design/18-build-system.md, TIM-155.\n */\n\nimport { performance } from 'node:perf_hooks';\n\nexport interface PhaseRecord {\n name: string;\n startMs: number;\n durationMs: number;\n}\n\nexport interface StartupTimer {\n /** Mark the beginning of a named phase. */\n start(phase: string): void;\n /** Mark the end of a named phase. Returns duration in ms. */\n end(phase: string): number;\n /** Get all completed phase records, ordered by start time. */\n getPhases(): PhaseRecord[];\n /** Total elapsed time from first start() to last end(). */\n totalMs(): number;\n /** Format a human-readable summary string. */\n formatSummary(): string;\n}\n\n/**\n * Create a startup timer that records phase durations.\n */\nexport function createStartupTimer(): StartupTimer {\n const pending = new Map<string, number>();\n const phases: PhaseRecord[] = [];\n\n return {\n start(phase: string): void {\n pending.set(phase, performance.now());\n },\n\n end(phase: string): number {\n const startMs = pending.get(phase);\n if (startMs === undefined) {\n return 0;\n }\n pending.delete(phase);\n const durationMs = performance.now() - startMs;\n phases.push({ name: phase, startMs, durationMs });\n return durationMs;\n },\n\n getPhases(): PhaseRecord[] {\n return [...phases].sort((a, b) => a.startMs - b.startMs);\n },\n\n totalMs(): number {\n if (phases.length === 0) return 0;\n const sorted = this.getPhases();\n const first = sorted[0];\n const last = sorted[sorted.length - 1];\n return last.startMs + last.durationMs - first.startMs;\n },\n\n formatSummary(): string {\n const sorted = this.getPhases();\n if (sorted.length === 0) return 'No phases recorded.';\n\n const lines = sorted.map((p) => {\n const ms = p.durationMs.toFixed(1);\n return ` ${p.name.padEnd(30)} ${ms.padStart(8)}ms`;\n });\n\n const total = this.totalMs().toFixed(1);\n lines.push(` ${'total'.padEnd(30)} ${total.padStart(8)}ms`);\n\n return ['[timber] startup timing:', ...lines].join('\\n');\n },\n };\n}\n\n/**\n * No-op timer for production builds — all methods are empty.\n */\nexport function createNoopTimer(): StartupTimer {\n return {\n start() {},\n end() {\n return 0;\n },\n getPhases() {\n return [];\n },\n totalMs() {\n return 0;\n },\n formatSummary() {\n return '';\n },\n };\n}\n","/**\n * Plugin context — internal types and helpers for timber sub-plugins.\n *\n * These are NOT part of the user-facing API. They are consumed by\n * sub-plugins (routing, entries, shims, etc.) via relative imports.\n * The root entry point (index.ts) does not re-export them.\n *\n * Design doc: 18-build-system.md §\"Shared Plugin Context\"\n */\n\nimport { existsSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\nimport { createRequire } from 'node:module';\nimport { parseAst } from 'vite';\nimport type { RouteTree } from './routing/types';\nimport type { BuildManifest } from './server/build-manifest';\nimport type { CaptureSummary } from './server/prebuilt-builder';\nimport type { StartupTimer } from './utils/startup-timer';\nimport { createStartupTimer } from './utils/startup-timer';\nimport type { TimberUserConfig, ClientJavascriptConfig } from './config-types.js';\nimport type { HoldingServer } from './dev-tools/holding-server.js';\nimport type { ProgramNode } from './plugins/callsite-ast.js';\n\n// Re-export for sub-plugin convenience — they import from plugin-context.ts\nexport type { TimberUserConfig, ClientJavascriptConfig } from './config-types.js';\n\n// ── Client JavaScript config ─────────────────────────────────────────────\n\n/** Fully resolved client JavaScript configuration (no optionals). */\nexport interface ResolvedClientJavascript {\n disabled: boolean;\n enableHMRInDev: boolean;\n}\n\n/**\n * Resolve `clientJavascript` into a fully resolved config.\n */\nexport function resolveClientJavascript(config: TimberUserConfig): ResolvedClientJavascript {\n if (config.clientJavascript !== undefined) {\n if (typeof config.clientJavascript === 'boolean') {\n return {\n disabled: !config.clientJavascript,\n enableHMRInDev: !config.clientJavascript,\n };\n }\n return {\n disabled: config.clientJavascript.disabled,\n enableHMRInDev: config.clientJavascript.enableHMRInDev ?? config.clientJavascript.disabled,\n };\n }\n return { disabled: false, enableHMRInDev: false };\n}\n\n// ── Plugin Context ────────────────────────────────────────────────────────\n\n/**\n * Shared context object passed to all sub-plugins via closure.\n *\n * Sub-plugins communicate through this context — not through Vite's\n * plugin API or global state.\n * See design/18-build-system.md §\"Shared Plugin Context\".\n */\nexport interface PluginContext {\n config: TimberUserConfig;\n /** Resolved client JavaScript configuration */\n clientJavascript: ResolvedClientJavascript;\n /** The scanned route tree (populated by timber-routing, consumed by timber-entries) */\n routeTree: RouteTree | null;\n /** Absolute path to the app/ directory */\n appDir: string;\n /** Absolute path to the project root */\n root: string;\n /** Whether the dev server is running (set by timber-root-sync in configResolved) */\n dev: boolean;\n /** CSS build manifest (populated by adapter after client build, null in dev) */\n buildManifest: BuildManifest | null;\n /** Per-build deployment ID for version skew detection (null in dev) */\n deploymentId: string | null;\n /** Startup timer for profiling cold start phases (active in dev, no-op in prod) */\n timer: StartupTimer;\n /** Holding server that binds the port during dev startup (closed in timber-dev-server) */\n holdingServer?: HoldingServer | null;\n /**\n * Resolved absolute path to the build output directory.\n *\n * Defaults to `<root>/.timber/dist`. Can be overridden via\n * `timber.config.ts` `buildDir` or Vite's `build.outDir`.\n * Timber config takes precedence when both are set.\n *\n * Used by adapter-build and cli preview.\n */\n buildDir: string;\n /**\n * Per-route client reference keys captured during the RSC build.\n * Maps absolute route file paths to the set of client reference\n * hash keys they transitively import. Used by adapter-build to\n * populate per-route modulepreload entries from clientReferenceDeps.\n */\n routeClientRefKeys?: Map<string, Set<string>>;\n /**\n * Maps absolute module IDs of client references in non-facade RSC\n * chunks to the absolute paths of their consuming facade modules.\n * Built during the RSC `generateBundle` phase by timber-chunks,\n * consumed by `clientChunkGroup` to reclassify `shared:` modules\n * that are only used by a single route boundary.\n * See design/27-chunking-strategy.md, TIM-1107.\n */\n sharedModuleFacades?: Map<string, string[]>;\n /**\n * Module ID → prebuilt component IDs declared in it. Recorded by the\n * timber-prebuilt transform (RSC environment, production builds only),\n * consumed by the RSC `generateBundle` module-graph walk that computes\n * `routePrebuiltComponents`. See design/44-render-at-build-time.md.\n */\n prebuiltModuleComponents?: Map<string, string[]>;\n /**\n * Module ID → mapping of variable name to component ID for the prebuilt\n * components declared in that module. Used at `generateBundle` time to\n * trace imports from other modules back to specific component IDs.\n */\n prebuiltBindingNames?: Map<string, Map<string, string>>;\n /**\n * Component ID → distinct static call-site prop objects captured from\n * JSX usage sites during the transform and generateBundle phases.\n * Consumed by the post-build capture pass to render with actual props\n * instead of empty `{}`. See design/44 §Props Derivation.\n */\n prebuiltCallSiteProps?: Map<string, Record<string, unknown>[]>;\n /**\n * Route file path (page or layout, absolute) → prebuilt component IDs\n * transitively reachable from it. Computed during RSC `generateBundle`,\n * consumed by the post-build capture pass (TIM-1119) to decide which\n * components to render under which route's param combos.\n */\n routePrebuiltComponents?: Map<string, string[]>;\n /**\n * Page file paths that were desugared from `export const prerender` by\n * timber-prerender-sugar. The prebuilt transform uses this to verify\n * the generated `cache.component(...)` call was picked up.\n */\n prerenderSugarPages?: Set<string>;\n /**\n * Component ID → built chunk file name for prebuilt-declaring modules\n * that are only reachable via dynamic imports (import.meta.glob, lazy\n * routes). The capture pass imports these chunks to trigger\n * cache.component() registration before rendering. Populated during\n * RSC generateBundle alongside routePrebuiltComponents.\n */\n prebuiltLazyChunks?: Map<string, string>;\n /** Post-build prebuilt capture summary (populated by timber-prebuilt, consumed by build-report). */\n captureSummary?: CaptureSummary;\n /**\n * Content-keyed AST parse memo shared across cache/prebuilt/prerender-sugar\n * transforms. Collapses both cross-plugin and cross-environment duplicate\n * parses: identical code hits, rewritten code misses (correct because Vite\n * transforms chain — a rewrite produces different code). Bounded LRU (32\n * entries) so it doesn't hold programs for the whole build.\n *\n * Consumers MUST treat the returned ProgramNode as read-only — they collect\n * ranges and rewrite CODE via MagicString; the AST itself is never mutated.\n */\n parseCached?: ParseMemo;\n /**\n * Content-Type mappings for extensionless static API route outputs.\n * Populated during static generation, consumed by adapter-build to\n * write per-path Content-Type rules in _headers. See TIM-1241.\n */\n staticContentTypes?: Record<string, string>;\n /**\n * RSC flight payload manifest mapping unhashed → hashed URLs.\n * Populated during static generation, consumed by adapters to set\n * immutable cache headers for `_rsc/` files. See TIM-1254.\n */\n rscManifest?: Record<string, string>;\n}\n\n// ── AST parse memo ───────────────────────────────────────────────────────\n\nconst PARSE_MEMO_CAPACITY = 32;\n\n/**\n * Content-keyed LRU memo for `parseAst`. Transforms for the same module run\n * back-to-back across plugins and environments, so a tiny window captures the\n * wins. The returned ProgramNode is shared — callers MUST NOT mutate it.\n */\nexport class ParseMemo {\n private cache = new Map<string, ProgramNode>();\n\n /**\n * Parse code, returning a cached ProgramNode if the same code was parsed\n * before. The returned AST MUST NOT be mutated — it may be shared across\n * multiple consumers.\n */\n parse(code: string): ProgramNode {\n const existing = this.cache.get(code);\n if (existing) {\n // LRU refresh: move to end\n this.cache.delete(code);\n this.cache.set(code, existing);\n return existing;\n }\n const program = parseAst(code, { lang: 'tsx' }) as unknown as ProgramNode;\n if (this.cache.size >= PARSE_MEMO_CAPACITY) {\n // Evict oldest (first inserted)\n const firstKey = this.cache.keys().next().value!;\n this.cache.delete(firstKey);\n }\n this.cache.set(code, program);\n return program;\n }\n\n get size(): number {\n return this.cache.size;\n }\n}\n\n// ── App directory resolution ──────────────────────────────────────────────\n\n/**\n * Resolve the app directory. Checks (in order):\n * 1. Explicit `configAppDir` from timber.config.ts\n * 2. `<root>/app`\n * 3. `<root>/src/app`\n *\n * Throws if none exist.\n */\nexport function resolveAppDir(root: string, configAppDir?: string): string {\n if (configAppDir) {\n const explicit = join(root, configAppDir);\n if (!existsSync(explicit)) {\n throw new Error(`[timber] Configured appDir \"${configAppDir}\" does not exist at ${explicit}`);\n }\n return explicit;\n }\n\n const rootApp = join(root, 'app');\n if (existsSync(rootApp)) return rootApp;\n\n const srcApp = join(root, 'src', 'app');\n if (existsSync(srcApp)) return srcApp;\n\n throw new Error(\n `[timber] Could not find app directory. Expected \"app/\" or \"src/app/\" in ${root}. ` +\n `You can set appDir in timber.config.ts to specify a custom location.`\n );\n}\n\n// ── Plugin context factory ────────────────────────────────────────────────\n\nexport function createPluginContext(config?: TimberUserConfig, root?: string): PluginContext {\n const projectRoot = root ?? process.cwd();\n const resolvedConfig: TimberUserConfig = { ...config };\n return {\n config: resolvedConfig,\n clientJavascript: resolveClientJavascript(resolvedConfig),\n routeTree: null,\n appDir: join(projectRoot, 'app'),\n root: projectRoot,\n dev: false,\n buildManifest: null,\n deploymentId: null,\n timer: createStartupTimer(),\n holdingServer: null,\n buildDir: resolveBuildDir(projectRoot, resolvedConfig.buildDir),\n parseCached: new ParseMemo(),\n };\n}\n\n// ── Build directory resolution ────────────────────────────────────────────\n\n/** Default build output directory (relative to root). */\nexport const DEFAULT_BUILD_DIR = join('.timber', 'dist');\n\n/**\n * Resolve the build output directory.\n *\n * Priority:\n * 1. Explicit `timberBuildDir` from timber.config.ts\n * 2. Explicit `viteBuildOutDir` from Vite's build.outDir (if not the default 'dist')\n * 3. Default: `.timber/dist`\n *\n * Returns an absolute path.\n */\nexport function resolveBuildDir(\n root: string,\n timberBuildDir?: string,\n viteBuildOutDir?: string\n): string {\n if (timberBuildDir) {\n return resolve(root, timberBuildDir);\n }\n if (viteBuildOutDir && viteBuildOutDir !== 'dist') {\n return resolve(root, viteBuildOutDir);\n }\n return join(root, DEFAULT_BUILD_DIR);\n}\n\n// ── Config file loading ───────────────────────────────────────────────────\n\n/**\n * Map known timber.config load failures to actionable hints (TIM-1040).\n * Returns null when the error has no known remedy beyond its own message.\n *\n * Known failure modes of loading timber.config.ts via native require():\n * - Node < 22.18 has no default type stripping → ERR_UNKNOWN_FILE_EXTENSION\n * - Non-erasable TS syntax (enums, namespaces, parameter properties)\n * → ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX / ERR_INVALID_TYPESCRIPT_SYNTAX\n * - ESM syntax in a `\"type\": \"commonjs\"` package → the stripped .ts is\n * parsed as CJS and throws SyntaxError: \"Unexpected token 'export'\"\n * (export statements) or \"Cannot use import statement outside a\n * module\" (import statements)\n */\nexport function configLoadHint(configName: string, err: unknown): string | null {\n const code = (err as NodeJS.ErrnoException | null)?.code;\n const isTs = configName.endsWith('.ts');\n\n if (isTs && code === 'ERR_UNKNOWN_FILE_EXTENSION') {\n return (\n 'Loading a TypeScript config requires Node.js 22.18+ (built-in type stripping). ' +\n 'Upgrade Node.js or rewrite the config as timber.config.js.'\n );\n }\n if (code === 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX' || code === 'ERR_INVALID_TYPESCRIPT_SYNTAX') {\n return (\n 'Node.js loads the config with type stripping, which only supports erasable TypeScript syntax — ' +\n 'replace enums, namespaces, and constructor parameter properties with plain objects and types.'\n );\n }\n if (\n isTs &&\n err instanceof SyntaxError &&\n /Unexpected token '(export|import)'|Cannot use import statement outside a module/.test(\n err.message\n )\n ) {\n return (\n 'The config uses ESM syntax but Node.js parsed it as CommonJS — ' +\n 'set \"type\": \"module\" in package.json, or use module.exports instead of export default.'\n );\n }\n return null;\n}\n\n/**\n * Load timber.config.ts (or .js, .mjs) from the project root.\n * Returns the config object or null if no config file is found.\n * Throws a descriptive error naming the file when a config file exists\n * but fails to load — a broken config must fail loudly, never silently\n * degrade (see TIM-1066). Known failure modes get an actionable hint\n * appended (see configLoadHint, TIM-1040).\n *\n * Uses require() — Node 22.12+ can require() ESM modules, and Node\n * 22.18+ strips types from .ts files by default (hence the engines\n * field requiring >=22.18.0). This keeps timber() synchronous — no\n * async config loading needed.\n * This is the single config loader — dev, build, and preview all use it.\n */\nexport function loadTimberConfigFile(root: string): TimberUserConfig | null {\n const configNames = ['timber.config.ts', 'timber.config.js', 'timber.config.mjs'];\n const req = createRequire(join(root, 'package.json'));\n\n for (const name of configNames) {\n const configPath = join(root, name);\n if (existsSync(configPath)) {\n try {\n const mod = req(configPath);\n return (mod.default ?? mod) as TimberUserConfig;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const hint = configLoadHint(name, err);\n throw new Error(\n `[timber] Failed to load ${configPath}: ${message}${hint ? `\\n${hint}` : ''}`,\n { cause: err }\n );\n }\n }\n }\n return null;\n}\n\n/**\n * Detect config keys set in both inline (vite.config.ts) and file (timber.config.ts)\n * and warn the user. The `output` key is excluded because it defaults to 'server'\n * in createPluginContext and would always appear as an inline key.\n *\n * Returns the list of conflicting key names (for testing).\n */\nexport function warnConfigConflicts(\n inline: TimberUserConfig,\n fileConfig: TimberUserConfig\n): string[] {\n const conflicts: string[] = [];\n for (const key of Object.keys(fileConfig) as (keyof TimberUserConfig)[]) {\n if (key in inline && inline[key] !== undefined) {\n conflicts.push(key);\n }\n }\n if (conflicts.length > 0) {\n console.warn(\n `[timber] Config conflict: ${conflicts.map((k) => `\"${k}\"`).join(', ')} set in both ` +\n `vite.config.ts (inline) and timber.config.ts. ` +\n `Move all config to timber.config.ts to avoid confusion. ` +\n `The inline value from vite.config.ts will be used.`\n );\n }\n return conflicts;\n}\n\n/**\n * Drop own keys whose value is `undefined` so they don't clobber file\n * config during spread merging. `timber({ buildDir: process.env.BUILD_DIR })`\n * with the var unset must not erase the timber.config.ts value.\n */\nfunction stripUndefined<T extends object>(obj: T): T {\n return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined)) as T;\n}\n\n/**\n * Merge file-based config (timber.config.ts) with inline config (vite.config.ts).\n * Inline config takes precedence — file config only fills in missing fields.\n * Inline keys explicitly set to `undefined` are treated as absent, matching\n * the `!== undefined` check in warnConfigConflicts.\n */\nexport function mergeFileConfig(\n inline: TimberUserConfig,\n fileConfig: TimberUserConfig\n): TimberUserConfig {\n warnConfigConflicts(inline, fileConfig);\n const definedInline = stripUndefined(inline);\n\n return {\n ...fileConfig,\n ...definedInline,\n ...(fileConfig.limits && definedInline.limits\n ? { limits: { ...fileConfig.limits, ...stripUndefined(definedInline.limits) } }\n : {}),\n ...(fileConfig.dev && definedInline.dev\n ? { dev: { ...fileConfig.dev, ...stripUndefined(definedInline.dev) } }\n : {}),\n ...(fileConfig.mdx && definedInline.mdx\n ? { mdx: { ...fileConfig.mdx, ...stripUndefined(definedInline.mdx) } }\n : {}),\n };\n}\n"],"mappings":"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"}
|
|
@@ -26,4 +26,10 @@ export interface BuildOutputBaseOptions {
|
|
|
26
26
|
*/
|
|
27
27
|
export declare function runSharedBuildSteps(opts: BuildOutputBaseOptions): Promise<string>;
|
|
28
28
|
export declare function findUnhashedStaticFiles(publicDir: string): Promise<string[]>;
|
|
29
|
+
/**
|
|
30
|
+
* Build a static-only output directory (no server entry). Copies client
|
|
31
|
+
* assets and pre-rendered HTML/RSC/API files into `outDir/publicDirName`.
|
|
32
|
+
* Used by both Cloudflare and Nitro adapters in `output: 'static'` mode.
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildStaticDirectoryOutput(config: TimberConfig, buildDir: string, outDir: string, publicDirName: string): Promise<void>;
|
|
29
35
|
//# sourceMappingURL=build-output-helper.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-output-helper.d.ts","sourceRoot":"","sources":["../../src/adapters/build-output-helper.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C,iDAAiD;AACjD,MAAM,WAAW,sBAAsB;IACrC,8BAA8B;IAC9B,MAAM,EAAE,YAAY,CAAC;IACrB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CA+DvF;AAuBD,wBAAsB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"build-output-helper.d.ts","sourceRoot":"","sources":["../../src/adapters/build-output-helper.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C,iDAAiD;AACjD,MAAM,WAAW,sBAAsB;IACrC,8BAA8B;IAC9B,MAAM,EAAE,YAAY,CAAC;IACrB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CA+DvF;AAuBD,wBAAsB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAwBlF;AAiCD;;;;GAIG;AACH,wBAAsB,0BAA0B,CAC9C,MAAM,EAAE,YAAY,EACpB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,IAAI,CAAC,CA0Bf"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as warnLossyJsonValue } from "../_chunks/json-lossy-check-ClNvBM_3.js";
|
|
2
|
-
import {
|
|
2
|
+
import { l as getCloudflareBindings } from "../_chunks/cloudflare-CnT5Lr7U.js";
|
|
3
3
|
//#region src/adapters/cloudflare-kv-cache.ts
|
|
4
4
|
var LEGACY_GRACE_MS = 30 * 86400 * 1e3;
|
|
5
5
|
/**
|
|
@@ -244,6 +244,16 @@ export declare function generateBindingsConfig(bindings: CloudflareBindings | un
|
|
|
244
244
|
export declare function readUserWranglerConfig(root: string): Promise<Record<string, unknown> | null>;
|
|
245
245
|
/** @internal Exported for testing. */
|
|
246
246
|
export declare function generateWranglerConfig(config: TimberConfig, options: CloudflareAdapterOptions, userConfig?: Record<string, unknown> | null): Record<string, unknown>;
|
|
247
|
+
/**
|
|
248
|
+
* Build a Cloudflare Pages-compatible static output directory.
|
|
249
|
+
*
|
|
250
|
+
* No Workers entry — just pre-rendered HTML, RSC flight files, and client
|
|
251
|
+
* assets in a flat directory. Uses the shared buildStaticDirectoryOutput
|
|
252
|
+
* then adds a `_headers` file for cache control.
|
|
253
|
+
*
|
|
254
|
+
* @internal Exported for testing.
|
|
255
|
+
*/
|
|
256
|
+
export declare function buildStaticOutput(config: TimberConfig, buildDir: string, outDir: string, _options: CloudflareAdapterOptions): Promise<void>;
|
|
247
257
|
/** Command descriptor for preview — testable without spawning a process. */
|
|
248
258
|
export interface PreviewCommand {
|
|
249
259
|
command: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloudflare.d.ts","sourceRoot":"","sources":["../../src/adapters/cloudflare.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"cloudflare.d.ts","sourceRoot":"","sources":["../../src/adapters/cloudflare.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export { cloudflare, generateBindingsConfig, generatePrebuiltSourceModule, generatePreviewCommand, generateRuntimeInitModule, generateWorkerEntry, generateWranglerConfig, getCloudflareBindings, readPrebuiltData, readUserWranglerConfig, runWithBindings };
|
|
1
|
+
import { a as generatePreviewCommand, c as generateWranglerConfig, d as readUserWranglerConfig, f as runWithBindings, i as generatePrebuiltSourceModule, l as getCloudflareBindings, n as cloudflare, o as generateRuntimeInitModule, r as generateBindingsConfig, s as generateWorkerEntry, t as buildStaticOutput, u as readPrebuiltData } from "../_chunks/cloudflare-CnT5Lr7U.js";
|
|
2
|
+
export { buildStaticOutput, cloudflare, generateBindingsConfig, generatePrebuiltSourceModule, generatePreviewCommand, generateRuntimeInitModule, generateWorkerEntry, generateWranglerConfig, getCloudflareBindings, readPrebuiltData, readUserWranglerConfig, runWithBindings };
|
package/dist/adapters/nitro.d.ts
CHANGED
|
@@ -27,6 +27,14 @@ interface PresetConfig {
|
|
|
27
27
|
/** Additional nitro.config fields for this preset. */
|
|
28
28
|
extraConfig?: Record<string, unknown>;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Static deploy target for `output: 'static'` mode.
|
|
32
|
+
*
|
|
33
|
+
* Controls platform-specific files written alongside the static output:
|
|
34
|
+
* - `'github-pages'` — writes `.nojekyll` to skip Jekyll processing
|
|
35
|
+
* - `'generic'` — no extra files (default)
|
|
36
|
+
*/
|
|
37
|
+
export type StaticTarget = 'github-pages' | 'generic';
|
|
30
38
|
/** Options for the Nitro adapter. */
|
|
31
39
|
export interface NitroAdapterOptions {
|
|
32
40
|
/**
|
|
@@ -50,6 +58,12 @@ export interface NitroAdapterOptions {
|
|
|
50
58
|
* @default true
|
|
51
59
|
*/
|
|
52
60
|
compress?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Static deploy target for `output: 'static'` mode. Controls
|
|
63
|
+
* platform-specific files (e.g. `.nojekyll` for GitHub Pages).
|
|
64
|
+
* @default 'generic'
|
|
65
|
+
*/
|
|
66
|
+
staticTarget?: StaticTarget;
|
|
53
67
|
/**
|
|
54
68
|
* Additional Nitro configuration to merge into the generated config.
|
|
55
69
|
* Overrides default values for the selected preset.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nitro.d.ts","sourceRoot":"","sources":["../../src/adapters/nitro.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,qBAAqB,EAAgB,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"nitro.d.ts","sourceRoot":"","sources":["../../src/adapters/nitro.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,qBAAqB,EAAgB,MAAM,SAAS,CAAC;AAWnE;;;;;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;;;;OAIG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB,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;AAyFD;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,cAAc,GAAG,SAAS,CAAC;AAEtD,qCAAqC;AACrC,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IAErB;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;;OAIG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC;IAE5B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AAID;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,KAAK,CAAC,OAAO,GAAE,mBAAwB,GAAG,qBAAqB,CA0H9E;AAID,sCAAsC;AACtC,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,WAAW,EACnB,QAAQ,UAAO,GACd,MAAM,CAmFR;AAOD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,MAAM,CAyNnF;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;AA4ED;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,CAuDnD;AAID;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,YAAY,CAEjE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,MAAM,CAGrF;AAED;;;;;;;;;GASG;AACH,wBAAsB,8BAA8B,CAClD,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC,CAoBf"}
|
package/dist/adapters/nitro.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as generateHeadersFile, i as IMMUTABLE_CACHE, n as findUnhashedStaticFiles, r as runSharedBuildSteps, t as buildStaticDirectoryOutput } from "../_chunks/build-output-helper-DOGYFb_X.js";
|
|
2
2
|
import { transformWithOxc } from "vite";
|
|
3
3
|
import { basename, dirname, join, relative } from "node:path";
|
|
4
4
|
import { cp, glob, readFile, writeFile } from "node:fs/promises";
|
|
@@ -120,6 +120,19 @@ function nitro(options = {}) {
|
|
|
120
120
|
name: `nitro-${preset}`,
|
|
121
121
|
async buildOutput(config, buildDir) {
|
|
122
122
|
const outDir = join(buildDir, "nitro");
|
|
123
|
+
if (config.output === "static" && config.staticPagesDir) {
|
|
124
|
+
await buildStaticDirectoryOutput(config, buildDir, outDir, "public");
|
|
125
|
+
const publicDir = join(outDir, "public");
|
|
126
|
+
const unhashedPaths = await findUnhashedStaticFiles(publicDir);
|
|
127
|
+
await writeFile(join(publicDir, "_headers"), generateHeadersFile(unhashedPaths, config.staticContentTypes));
|
|
128
|
+
if ((options.staticTarget ?? "generic") === "github-pages") {
|
|
129
|
+
await writeFile(join(publicDir, ".nojekyll"), "");
|
|
130
|
+
try {
|
|
131
|
+
if ((await readFile(join(publicDir, "_redirects"), "utf-8")).trim().length > 0) console.warn("[timber] Warning: GitHub Pages does not support _redirects files. Redirect rules from middleware/proxy will not work on this platform.");
|
|
132
|
+
} catch {}
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
123
136
|
await runSharedBuildSteps({
|
|
124
137
|
config,
|
|
125
138
|
buildDir,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nitro.js","names":[],"sources":["../../src/adapters/compress-module.ts","../../src/adapters/nitro.ts"],"sourcesContent":["// Compiles server/compress.ts into a standalone ESM module (_compress.mjs)\n// for self-hosted deployments. The adapter writes this to the build output\n// during buildOutput(). It's imported by the Nitro entry and preview server\n// at runtime.\n//\n// By compiling the actual TypeScript source rather than maintaining a\n// hand-written string template, any fix to server/compress.ts automatically\n// flows to the production module. See design/18-build-system.md (\"entry\n// modules are real TypeScript files, not codegen strings\").\n\nimport { transformWithOxc } from 'vite';\n// Vite's ?raw import embeds the file as a string at build time — works\n// in both dev (Vite resolves the TS source) and the published package\n// (the string is inlined into the compiled adapter entry).\nimport compressSource from '../server/compress.ts?raw';\n\n/**\n * Compile server/compress.ts into a standalone ESM module.\n *\n * Uses Vite's OXC transform to strip TypeScript types from the\n * embedded source. The result is written to `_compress.mjs` during\n * buildOutput.\n *\n * @internal Exported for testing.\n */\nexport async function generateCompressModule(): Promise<string> {\n const result = await transformWithOxc(compressSource, 'compress.ts', {\n lang: 'ts',\n });\n\n return (\n '// Generated by @timber-js/app — compiled from server/compress.ts.\\n' +\n '// Do not edit — this file is regenerated on each build.\\n' +\n result.code\n );\n}\n","// 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, readFile, cp, glob } from 'node:fs/promises';\nimport { execFile } from 'node:child_process';\nimport { join, relative, dirname, basename } from 'node:path';\nimport type { TimberPlatformAdapter, TimberConfig } from './types';\nimport { generateCompressModule } from './compress-module.js';\nimport { IMMUTABLE_CACHE } from './shared.js';\nimport { runSharedBuildSteps } from './build-output-helper.js';\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 /**\n * Path to the server bundle directory relative to outputDir.\n * Nitro places the bundled server entry + _chunks/ here.\n * Most presets use `server`; Vercel uses `functions/__server.func`.\n */\n serverBundleDir: 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 serverBundleDir: 'functions/__server.func',\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 serverBundleDir: 'functions/__server.func',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'vercel-edge',\n },\n 'netlify': {\n nitroPreset: 'netlify',\n outputDir: '.netlify/functions-internal',\n serverBundleDir: 'server',\n supportsWaitUntil: false,\n supportsEarlyHints: false,\n runtimeName: 'netlify',\n },\n 'netlify-edge': {\n nitroPreset: 'netlify-edge',\n outputDir: '.netlify/edge-functions',\n serverBundleDir: 'server',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'netlify-edge',\n },\n 'aws-lambda': {\n nitroPreset: 'aws-lambda',\n outputDir: '.output',\n serverBundleDir: 'server',\n supportsWaitUntil: false,\n supportsEarlyHints: false,\n runtimeName: 'aws-lambda',\n },\n 'deno-deploy': {\n nitroPreset: 'deno-deploy',\n outputDir: '.output',\n serverBundleDir: 'server',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'deno-deploy',\n },\n 'azure-functions': {\n nitroPreset: 'azure-functions',\n outputDir: '.output',\n serverBundleDir: 'server',\n supportsWaitUntil: false,\n supportsEarlyHints: false,\n runtimeName: 'azure-functions',\n },\n 'node-server': {\n nitroPreset: 'node-server',\n outputDir: '.output',\n serverBundleDir: 'server',\n supportsWaitUntil: true,\n // Disabled by default: most node-server deployments sit behind a\n // reverse proxy (nginx, caddy, traefik) that doesn't support 103\n // Early Hints over HTTP/1.1. Sending 103 causes nginx to intermittently\n // treat the response as an error and retry after proxy_connect_timeout\n // (~5s stalls on ~23% of requests). Link headers on the 200 response\n // are the safe fallback — Cloudflare and other CDNs convert them to\n // 103 automatically at the edge (over HTTP/2+ to the browser).\n supportsEarlyHints: false,\n runtimeName: 'node-server',\n },\n 'bun': {\n nitroPreset: 'bun',\n outputDir: '.output',\n serverBundleDir: 'server',\n supportsWaitUntil: true,\n // Disabled for same reason as node-server — reverse proxies choke on 103.\n // Link headers on the 200 response are converted to 103 by CDNs.\n supportsEarlyHints: false,\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 * Enable application-level gzip compression for HTML and RSC responses.\n *\n * When `true` (default), the origin compresses responses using the Web\n * `CompressionStream` API. This is useful for self-hosted deployments\n * where no reverse proxy or CDN handles compression.\n *\n * Set to `false` when deploying behind a reverse proxy (nginx, caddy)\n * or CDN (Cloudflare, Fastly, Vercel) that compresses at the edge.\n * Disabling origin compression saves CPU on the Node.js event loop —\n * compressing 1MB+ streaming HTML responses takes 10-15ms of main\n * thread time per request, directly reducing throughput under load.\n *\n * @default true\n */\n compress?: boolean;\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 compress = options.compress ?? true;\n const presetConfig = PRESET_CONFIGS[preset];\n return {\n name: `nitro-${preset}`,\n\n async buildOutput(config: TimberConfig, buildDir: string) {\n const outDir = join(buildDir, 'nitro');\n\n // Steps 1–5: shared across all adapters (mkdir, copy client/rsc/ssr,\n // write _headers, write manifest-init).\n await runSharedBuildSteps({\n config,\n buildDir,\n outDir,\n publicDirName: 'public',\n });\n\n // Write runtime helper modules used by the preview server.\n // See design/25-production-deployments.md — self-hosted deployments\n // need application-level compression (Cloudflare handles it at the edge).\n await writeFile(join(outDir, '_compress.mjs'), await generateCompressModule());\n\n // Web→Node response bridge with backpressure. See TIM-1154.\n await writeFile(join(outDir, '_send-response.mjs'), generateSendResponseModule());\n\n // Prepend the manifest assignment directly into the RSC entry so\n // globalThis.__TIMBER_BUILD_MANIFEST__ is set before any module reads it.\n // This must be top-level code, not an import, because rollup tree-shakes\n // side-effect-only globalThis assignments from imported modules.\n if (config.manifestInit) {\n const rscEntry = join(outDir, 'rsc', 'index.js');\n const rscContent = await readFile(rscEntry, 'utf-8');\n await writeFile(rscEntry, `${config.manifestInit}\\n${rscContent}`);\n }\n\n // Generate the Nitro entry point (imports from ./rsc/ within nitro dir)\n const entry = generateNitroEntry(buildDir, outDir, preset, compress);\n await writeFile(join(outDir, 'entry.ts'), entry);\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 // Config is passed programmatically — no nitro.config.ts file needed.\n const { serverDir } = await runNitroBuild(outDir, preset, options.nitroConfig);\n\n // Copy prebuilt flight payloads from the canonical build source\n // (buildDir/prebuilt, cleaned by the prebuilt plugin each build) into\n // the Nitro output. The RSC entry resolves `../prebuilt/` relative to\n // its bundled chunk (<serverDir>/_chunks/), so prebuilt/ must live at\n // <serverDir>/prebuilt/. serverDir is the resolved path from Nitro\n // (honors user overrides via nitroConfig.output.serverDir).\n const prebuiltSrc = join(buildDir, 'prebuilt');\n await cp(prebuiltSrc, join(serverDir, 'prebuilt'), {\n recursive: true,\n }).catch((e: NodeJS.ErrnoException) => {\n if (e.code !== 'ENOENT') throw e;\n });\n\n // Vercel with functionRules clones __server.func to per-route .func\n // directories during the Nitro build. Copy prebuilt into those clones\n // so requests routed to them also resolve ../prebuilt correctly.\n if (preset === 'vercel' || preset === 'vercel-edge') {\n const functionsDir = dirname(serverDir);\n const baseFuncName = basename(serverDir);\n await copyPrebuiltToVercelFuncClones(functionsDir, prebuiltSrc, baseFuncName);\n }\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}\n\n// ─── Entry Generation ────────────────────────────────────────────────────────\n\n/** @internal Exported for testing. */\nexport function generateNitroEntry(\n buildDir: string,\n outDir: string,\n preset: NitroPreset,\n compress = true\n): string {\n // The RSC entry is the main request handler — it exports the fetch handler as default.\n // rsc/ is copied into the nitro dir so the import is local.\n // The manifest init is prepended to rsc/index.js before the nitro build,\n // so globalThis.__TIMBER_BUILD_MANIFEST__ is set before any code reads it.\n const serverEntryRelative = './rsc/index.js';\n const runtimeName = PRESET_CONFIGS[preset].runtimeName;\n const earlyHints = PRESET_CONFIGS[preset].supportsEarlyHints;\n const supportsWaitUntil = PRESET_CONFIGS[preset].supportsWaitUntil;\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 //\n // For presets that support waitUntil, bridge h3's event.waitUntil()\n // to timber's waitUntil() primitive via the ALS bridge.\n // See design/11-platform.md §\"waitUntil()\".\n let handlerCall: string;\n if (earlyHints && supportsWaitUntil) {\n handlerCall = ` 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 waitUntilFn = (typeof event.waitUntil === 'function')\n ? (p) => event.waitUntil(p)\n : undefined\n\n const callHandler = () => handler(webRequest)\n let wrappedHandler = earlyHintsSender\n ? () => runWithEarlyHintsSender(earlyHintsSender, callHandler)\n : callHandler\n const webResponse = waitUntilFn\n ? await runWithWaitUntil(waitUntilFn, wrappedHandler)\n : await wrappedHandler()`;\n } else if (earlyHints) {\n handlerCall = ` 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 } else if (supportsWaitUntil) {\n handlerCall = ` const waitUntilFn = (typeof event.waitUntil === 'function')\n ? (p) => event.waitUntil(p)\n : undefined\n\n const webResponse = waitUntilFn\n ? await runWithWaitUntil(waitUntilFn, () => handler(webRequest))\n : await handler(webRequest)`;\n } else {\n handlerCall = ` const webResponse = await handler(webRequest)`;\n }\n\n // Import runWithWaitUntil only when the preset supports it.\n const waitUntilImport = supportsWaitUntil ? ', runWithWaitUntil' : '';\n\n const compressImport = compress ? \"import { compressResponse } from './_compress.mjs'\\n\" : '';\n const compressCall = compress ? 'compressResponse(webRequest, webResponse)' : 'webResponse';\n\n return `// Generated by @timber-js/app/adapters/nitro\n// Do not edit — this file is regenerated on each build.\n\nimport handler, { runWithEarlyHintsSender${waitUntilImport} } from '${serverEntryRelative}'\n${compressImport}\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\n// Nitro's index.mjs wraps this import with defineLazyEventHandler, which\n// already handles the event handler protocol. We export a plain async\n// function — no defineEventHandler wrapper needed.\n// Nitro bundles h3 into its internal _libs/ directory but doesn't make\n// it available as a bare specifier in the output's node_modules.\nexport default async function timberHandler(event) {\n // h3 v2: event.req is the Web Request\n const webRequest = event.req\n${handlerCall}\n return ${compressCall}\n}\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\n// Import compression helper for self-hosted response compression.\nconst { compressResponse } = await import('./_compress.mjs');\n\n// Web→Node response bridge with backpressure (TIM-1154).\nconst { sendNodeResponse } = await import('./_send-response.mjs');\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}');\n\n// Port resolution (TIM-842):\n// - Default to 3000\n// - If PORT env var is set, honor it strictly (fail loudly on conflict)\n// - Otherwise auto-bump from 3000 until a free port is found\n// Mirrors the dev server behavior so timber dev/preview/production all\n// behave the same way around port selection.\nconst envPort = process.env.PORT ? parseInt(process.env.PORT, 10) : null;\nconst portIsExplicit = envPort != null && Number.isFinite(envPort) && envPort > 0;\nconst startPort = portIsExplicit ? envPort : 3000;\nconst host = process.env.HOST || 'localhost';\n\n// Set after listenWithBump() resolves so request handlers can build\n// absolute URLs from the actual bound port (which may differ from\n// startPort after an auto-bump).\nlet boundPort = startPort;\n\nconst server = createServer(async (req, res) => {\n // Scheme from the socket's TLS state — a hardcoded http:// breaks CSRF\n // origin comparison and absolute-URL construction when TLS is terminated\n // at this server (TIM-1073; mirrors the dev-server fix in TIM-1067).\n // X-Forwarded-Proto is intentionally not consulted here: on a direct\n // connection it would be attacker-controlled. The CSRF layer honors it\n // separately for reverse-proxy deployments.\n const scheme = req.socket?.encrypted ? 'https' : 'http';\n const url = new URL(req.url || '/', \\`\\${scheme}://\\${host}:\\${boundPort}\\`);\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 isImmutable =\n url.pathname.startsWith('/assets/') || url.pathname.startsWith('/_timber/fonts/');\n const cacheControl = isImmutable\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 rawResponse = earlyHintsSender && runWithEarlyHintsSender\n ? await runWithEarlyHintsSender(earlyHintsSender, () => handler(webRequest))\n : await handler(webRequest);\n\n // Compress the response for self-hosted deployments.\n const webResponse = compressResponse(webRequest, rawResponse);\n\n // Write the response to Node's ServerResponse. sendNodeResponse handles\n // status, headers (including Set-Cookie splitting via [...headers]),\n // body streaming with backpressure, and client disconnect cleanup.\n // See TIM-1154.\n await sendNodeResponse(res, webResponse);\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\nfunction listenWithBump(port, attempt = 0) {\n return new Promise((resolveListen, rejectListen) => {\n const onError = (err) => {\n server.removeListener('listening', onListening);\n if (err && err.code === 'EADDRINUSE' && !portIsExplicit && attempt < 100) {\n if (attempt === 0) {\n console.log(\\` [timber] Port \\${port} in use, trying \\${port + 1}...\\`);\n }\n listenWithBump(port + 1, attempt + 1).then(resolveListen, rejectListen);\n return;\n }\n rejectListen(err);\n };\n const onListening = () => {\n server.removeListener('error', onError);\n resolveListen(port);\n };\n server.once('error', onError);\n server.once('listening', onListening);\n server.listen(port, host);\n });\n}\n\ntry {\n boundPort = await listenWithBump(startPort);\n if (boundPort !== startPort) {\n console.log();\n console.log(\\` [timber] Port \\${startPort} in use, using \\${boundPort}\\`);\n }\n console.log();\n console.log(' ⚡ timber preview server running at:');\n console.log();\n console.log(\\` ➜ http://\\${host}:\\${boundPort}\\`);\n console.log();\n} catch (err) {\n if (err && err.code === 'EADDRINUSE') {\n console.error(\\`[timber] Port \\${startPort} is already in use. \\` +\n 'Set PORT to a free port, or unset PORT to auto-pick.');\n } else {\n console.error('[timber] Failed to start preview server:', err);\n }\n process.exit(1);\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 */\ninterface NitroBuildResult {\n /** Resolved output.serverDir — where the server bundle + _chunks/ live. */\n serverDir: string;\n}\n\nasync function runNitroBuild(\n nitroDir: string,\n preset: NitroPreset,\n userConfig?: Record<string, unknown>\n): Promise<NitroBuildResult> {\n const presetConfig = PRESET_CONFIGS[preset];\n const {\n createNitro,\n build: nitroBuild,\n prepare,\n copyPublicAssets,\n } = await import('nitro/builder');\n\n const nitro = await createNitro({\n rootDir: nitroDir,\n preset: presetConfig.nitroPreset,\n // Use renderer.entry so Nitro wraps our handler with its server runtime\n // (HTTP server, static file serving, graceful shutdown, etc.).\n // Using `entry` directly would bypass the Nitro server runtime.\n renderer: { handler: join(nitroDir, 'entry.ts') },\n output: { dir: join(nitroDir, presetConfig.outputDir) },\n routeRules: {\n '/assets/**': { headers: { 'Cache-Control': IMMUTABLE_CACHE } },\n '/_timber/fonts/**': { 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\\//],\n },\n ...presetConfig.extraConfig,\n ...userConfig,\n });\n\n // Read the resolved serverDir AFTER config merging — userConfig may\n // override output.dir or output.serverDir. Nitro appends a trailing\n // slash during resolution; strip it for join() compatibility.\n const raw = nitro.options.output.serverDir;\n const serverDir = raw.endsWith('/') ? raw.slice(0, -1) : raw;\n\n await prepare(nitro);\n await copyPublicAssets(nitro);\n await nitroBuild(nitro);\n await nitro.close();\n\n return { serverDir };\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// ─── Send Response Module ───────────────────────────────────────────────────\n\n/**\n * Generate a standalone ESM module that exports sendNodeResponse.\n *\n * Mirrors srvx/node's sendNodeResponse: status + headers via writeHead,\n * body streaming with backpressure (waits for drain on write() === false),\n * and client disconnect cleanup. Written to `_send-response.mjs` during\n * buildOutput for use by the preview server script. See TIM-1154.\n *\n * @internal Exported for testing.\n */\nexport function generateSendResponseModule(): string {\n return `// Generated by @timber-js/app — Web→Node response bridge.\n// Do not edit — this file is regenerated on each build.\n// Mirrors srvx/node's sendNodeResponse with backpressure support.\n\nexport function sendNodeResponse(nodeRes, webRes) {\n if (!webRes) {\n nodeRes.statusCode = 500;\n return new Promise((resolve) => nodeRes.end(resolve));\n }\n const rawHeaders = [...webRes.headers];\n const writeHeaders = rawHeaders.flat();\n if (!nodeRes.headersSent) {\n if (nodeRes.req?.httpVersion === '2.0') {\n nodeRes.writeHead(webRes.status, writeHeaders);\n } else {\n nodeRes.writeHead(webRes.status, webRes.statusText, writeHeaders);\n }\n }\n if (!webRes.body) {\n return new Promise((resolve) => nodeRes.end(resolve));\n }\n // Stream with backpressure: pause reading when the kernel buffer is\n // full (write() returns false) and resume on 'drain'.\n if (nodeRes.destroyed) {\n webRes.body.cancel();\n return;\n }\n const reader = webRes.body.getReader();\n function streamCancel(error) {\n reader.cancel(error).catch(() => {});\n if (error) nodeRes.destroy(error);\n }\n function streamHandle({ done, value }) {\n try {\n if (done) {\n nodeRes.end();\n } else if (nodeRes.write(value)) {\n reader.read().then(streamHandle, streamCancel);\n } else {\n nodeRes.once('drain', () => reader.read().then(streamHandle, streamCancel));\n }\n } catch (error) {\n streamCancel(error instanceof Error ? error : undefined);\n }\n }\n nodeRes.on('close', streamCancel);\n nodeRes.on('error', streamCancel);\n reader.read().then(streamHandle, streamCancel);\n return reader.closed.catch(streamCancel).finally(() => {\n nodeRes.off('close', streamCancel);\n nodeRes.off('error', streamCancel);\n });\n}\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\n/**\n * Resolve the destination path for prebuilt flight payloads in the Nitro\n * output directory. The RSC entry resolves `../prebuilt/` relative to its\n * bundled chunk location (`<serverBundleDir>/_chunks/`), so prebuilt/ must\n * live at `<serverBundleDir>/prebuilt/`.\n *\n * The serverBundleDir varies by preset — Nitro's Vercel preset uses\n * `functions/__server.func` while most others use `server`.\n *\n * @internal Exported for testing.\n */\nexport function resolvePrebuiltOutputPath(outDir: string, preset: NitroPreset): string {\n const presetConfig = PRESET_CONFIGS[preset];\n return join(outDir, presetConfig.outputDir, presetConfig.serverBundleDir, 'prebuilt');\n}\n\n/**\n * Copy prebuilt payloads into cloned Vercel function directories.\n *\n * When users configure `vercel.functionRules`, Nitro's compiled hook clones\n * the base __server.func to per-route .func directories. These clones are\n * created during `runNitroBuild()` — before our post-build prebuilt copy.\n * Each clone resolves `../prebuilt` independently, so they each need a copy.\n *\n * @internal Exported for testing.\n */\nexport async function copyPrebuiltToVercelFuncClones(\n functionsDir: string,\n prebuiltSrc: string,\n baseFuncName: string\n): Promise<void> {\n const funcDirs: string[] = [];\n try {\n for await (const match of glob('**/*.func', { cwd: functionsDir })) {\n if (!match.endsWith(baseFuncName)) {\n funcDirs.push(join(functionsDir, match));\n }\n }\n } catch {\n return;\n }\n await Promise.all(\n funcDirs.map((dir) =>\n cp(prebuiltSrc, join(dir, 'prebuilt'), { recursive: true }).catch(\n (e: NodeJS.ErrnoException) => {\n if (e.code !== 'ENOENT') throw e;\n }\n )\n )\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"nitro.js","names":[],"sources":["../../src/adapters/compress-module.ts","../../src/adapters/nitro.ts"],"sourcesContent":["// Compiles server/compress.ts into a standalone ESM module (_compress.mjs)\n// for self-hosted deployments. The adapter writes this to the build output\n// during buildOutput(). It's imported by the Nitro entry and preview server\n// at runtime.\n//\n// By compiling the actual TypeScript source rather than maintaining a\n// hand-written string template, any fix to server/compress.ts automatically\n// flows to the production module. See design/18-build-system.md (\"entry\n// modules are real TypeScript files, not codegen strings\").\n\nimport { transformWithOxc } from 'vite';\n// Vite's ?raw import embeds the file as a string at build time — works\n// in both dev (Vite resolves the TS source) and the published package\n// (the string is inlined into the compiled adapter entry).\nimport compressSource from '../server/compress.ts?raw';\n\n/**\n * Compile server/compress.ts into a standalone ESM module.\n *\n * Uses Vite's OXC transform to strip TypeScript types from the\n * embedded source. The result is written to `_compress.mjs` during\n * buildOutput.\n *\n * @internal Exported for testing.\n */\nexport async function generateCompressModule(): Promise<string> {\n const result = await transformWithOxc(compressSource, 'compress.ts', {\n lang: 'ts',\n });\n\n return (\n '// Generated by @timber-js/app — compiled from server/compress.ts.\\n' +\n '// Do not edit — this file is regenerated on each build.\\n' +\n result.code\n );\n}\n","// 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, readFile, cp, glob } from 'node:fs/promises';\nimport { execFile } from 'node:child_process';\nimport { join, relative, dirname, basename } from 'node:path';\nimport type { TimberPlatformAdapter, TimberConfig } from './types';\nimport { generateCompressModule } from './compress-module.js';\nimport { IMMUTABLE_CACHE, generateHeadersFile } from './shared.js';\nimport {\n runSharedBuildSteps,\n buildStaticDirectoryOutput,\n findUnhashedStaticFiles,\n} from './build-output-helper.js';\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 /**\n * Path to the server bundle directory relative to outputDir.\n * Nitro places the bundled server entry + _chunks/ here.\n * Most presets use `server`; Vercel uses `functions/__server.func`.\n */\n serverBundleDir: 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 serverBundleDir: 'functions/__server.func',\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 serverBundleDir: 'functions/__server.func',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'vercel-edge',\n },\n 'netlify': {\n nitroPreset: 'netlify',\n outputDir: '.netlify/functions-internal',\n serverBundleDir: 'server',\n supportsWaitUntil: false,\n supportsEarlyHints: false,\n runtimeName: 'netlify',\n },\n 'netlify-edge': {\n nitroPreset: 'netlify-edge',\n outputDir: '.netlify/edge-functions',\n serverBundleDir: 'server',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'netlify-edge',\n },\n 'aws-lambda': {\n nitroPreset: 'aws-lambda',\n outputDir: '.output',\n serverBundleDir: 'server',\n supportsWaitUntil: false,\n supportsEarlyHints: false,\n runtimeName: 'aws-lambda',\n },\n 'deno-deploy': {\n nitroPreset: 'deno-deploy',\n outputDir: '.output',\n serverBundleDir: 'server',\n supportsWaitUntil: true,\n supportsEarlyHints: false,\n runtimeName: 'deno-deploy',\n },\n 'azure-functions': {\n nitroPreset: 'azure-functions',\n outputDir: '.output',\n serverBundleDir: 'server',\n supportsWaitUntil: false,\n supportsEarlyHints: false,\n runtimeName: 'azure-functions',\n },\n 'node-server': {\n nitroPreset: 'node-server',\n outputDir: '.output',\n serverBundleDir: 'server',\n supportsWaitUntil: true,\n // Disabled by default: most node-server deployments sit behind a\n // reverse proxy (nginx, caddy, traefik) that doesn't support 103\n // Early Hints over HTTP/1.1. Sending 103 causes nginx to intermittently\n // treat the response as an error and retry after proxy_connect_timeout\n // (~5s stalls on ~23% of requests). Link headers on the 200 response\n // are the safe fallback — Cloudflare and other CDNs convert them to\n // 103 automatically at the edge (over HTTP/2+ to the browser).\n supportsEarlyHints: false,\n runtimeName: 'node-server',\n },\n 'bun': {\n nitroPreset: 'bun',\n outputDir: '.output',\n serverBundleDir: 'server',\n supportsWaitUntil: true,\n // Disabled for same reason as node-server — reverse proxies choke on 103.\n // Link headers on the 200 response are converted to 103 by CDNs.\n supportsEarlyHints: false,\n runtimeName: 'bun',\n },\n};\n\n// ─── Options ─────────────────────────────────────────────────────────────────\n\n/**\n * Static deploy target for `output: 'static'` mode.\n *\n * Controls platform-specific files written alongside the static output:\n * - `'github-pages'` — writes `.nojekyll` to skip Jekyll processing\n * - `'generic'` — no extra files (default)\n */\nexport type StaticTarget = 'github-pages' | 'generic';\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 * Enable application-level gzip compression for HTML and RSC responses.\n *\n * When `true` (default), the origin compresses responses using the Web\n * `CompressionStream` API. This is useful for self-hosted deployments\n * where no reverse proxy or CDN handles compression.\n *\n * Set to `false` when deploying behind a reverse proxy (nginx, caddy)\n * or CDN (Cloudflare, Fastly, Vercel) that compresses at the edge.\n * Disabling origin compression saves CPU on the Node.js event loop —\n * compressing 1MB+ streaming HTML responses takes 10-15ms of main\n * thread time per request, directly reducing throughput under load.\n *\n * @default true\n */\n compress?: boolean;\n\n /**\n * Static deploy target for `output: 'static'` mode. Controls\n * platform-specific files (e.g. `.nojekyll` for GitHub Pages).\n * @default 'generic'\n */\n staticTarget?: StaticTarget;\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 compress = options.compress ?? true;\n const presetConfig = PRESET_CONFIGS[preset];\n return {\n name: `nitro-${preset}`,\n\n async buildOutput(config: TimberConfig, buildDir: string) {\n const outDir = join(buildDir, 'nitro');\n\n // ── Static mode: plain directory output ────────────────────────\n // No Nitro server — just pre-rendered HTML + client assets.\n if (config.output === 'static' && config.staticPagesDir) {\n await buildStaticDirectoryOutput(config, buildDir, outDir, 'public');\n const publicDir = join(outDir, 'public');\n\n // Write _headers for cache control + extensionless Content-Type.\n // Netlify supports the _headers convention just like Cloudflare.\n const unhashedPaths = await findUnhashedStaticFiles(publicDir);\n await writeFile(\n join(publicDir, '_headers'),\n generateHeadersFile(unhashedPaths, config.staticContentTypes)\n );\n\n // Platform-specific files for static deploy targets.\n const target = options.staticTarget ?? 'generic';\n if (target === 'github-pages') {\n await writeFile(join(publicDir, '.nojekyll'), '');\n // GitHub Pages doesn't support _redirects files. Warn if any\n // redirects were generated — those routes will 404 on GH Pages.\n try {\n const redirects = await readFile(join(publicDir, '_redirects'), 'utf-8');\n if (redirects.trim().length > 0) {\n console.warn(\n `[timber] Warning: GitHub Pages does not support _redirects files. ` +\n `Redirect rules from middleware/proxy will not work on this platform.`\n );\n }\n } catch {\n // No _redirects file — nothing to warn about\n }\n }\n\n return;\n }\n\n // Steps 1–5: shared across all adapters (mkdir, copy client/rsc/ssr,\n // write _headers, write manifest-init).\n await runSharedBuildSteps({\n config,\n buildDir,\n outDir,\n publicDirName: 'public',\n });\n\n // Write runtime helper modules used by the preview server.\n // See design/25-production-deployments.md — self-hosted deployments\n // need application-level compression (Cloudflare handles it at the edge).\n await writeFile(join(outDir, '_compress.mjs'), await generateCompressModule());\n\n // Web→Node response bridge with backpressure. See TIM-1154.\n await writeFile(join(outDir, '_send-response.mjs'), generateSendResponseModule());\n\n // Prepend the manifest assignment directly into the RSC entry so\n // globalThis.__TIMBER_BUILD_MANIFEST__ is set before any module reads it.\n // This must be top-level code, not an import, because rollup tree-shakes\n // side-effect-only globalThis assignments from imported modules.\n if (config.manifestInit) {\n const rscEntry = join(outDir, 'rsc', 'index.js');\n const rscContent = await readFile(rscEntry, 'utf-8');\n await writeFile(rscEntry, `${config.manifestInit}\\n${rscContent}`);\n }\n\n // Generate the Nitro entry point (imports from ./rsc/ within nitro dir)\n const entry = generateNitroEntry(buildDir, outDir, preset, compress);\n await writeFile(join(outDir, 'entry.ts'), entry);\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 // Config is passed programmatically — no nitro.config.ts file needed.\n const { serverDir } = await runNitroBuild(outDir, preset, options.nitroConfig);\n\n // Copy prebuilt flight payloads from the canonical build source\n // (buildDir/prebuilt, cleaned by the prebuilt plugin each build) into\n // the Nitro output. The RSC entry resolves `../prebuilt/` relative to\n // its bundled chunk (<serverDir>/_chunks/), so prebuilt/ must live at\n // <serverDir>/prebuilt/. serverDir is the resolved path from Nitro\n // (honors user overrides via nitroConfig.output.serverDir).\n const prebuiltSrc = join(buildDir, 'prebuilt');\n await cp(prebuiltSrc, join(serverDir, 'prebuilt'), {\n recursive: true,\n }).catch((e: NodeJS.ErrnoException) => {\n if (e.code !== 'ENOENT') throw e;\n });\n\n // Vercel with functionRules clones __server.func to per-route .func\n // directories during the Nitro build. Copy prebuilt into those clones\n // so requests routed to them also resolve ../prebuilt correctly.\n if (preset === 'vercel' || preset === 'vercel-edge') {\n const functionsDir = dirname(serverDir);\n const baseFuncName = basename(serverDir);\n await copyPrebuiltToVercelFuncClones(functionsDir, prebuiltSrc, baseFuncName);\n }\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}\n\n// ─── Entry Generation ────────────────────────────────────────────────────────\n\n/** @internal Exported for testing. */\nexport function generateNitroEntry(\n buildDir: string,\n outDir: string,\n preset: NitroPreset,\n compress = true\n): string {\n // The RSC entry is the main request handler — it exports the fetch handler as default.\n // rsc/ is copied into the nitro dir so the import is local.\n // The manifest init is prepended to rsc/index.js before the nitro build,\n // so globalThis.__TIMBER_BUILD_MANIFEST__ is set before any code reads it.\n const serverEntryRelative = './rsc/index.js';\n const runtimeName = PRESET_CONFIGS[preset].runtimeName;\n const earlyHints = PRESET_CONFIGS[preset].supportsEarlyHints;\n const supportsWaitUntil = PRESET_CONFIGS[preset].supportsWaitUntil;\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 //\n // For presets that support waitUntil, bridge h3's event.waitUntil()\n // to timber's waitUntil() primitive via the ALS bridge.\n // See design/11-platform.md §\"waitUntil()\".\n let handlerCall: string;\n if (earlyHints && supportsWaitUntil) {\n handlerCall = ` 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 waitUntilFn = (typeof event.waitUntil === 'function')\n ? (p) => event.waitUntil(p)\n : undefined\n\n const callHandler = () => handler(webRequest)\n let wrappedHandler = earlyHintsSender\n ? () => runWithEarlyHintsSender(earlyHintsSender, callHandler)\n : callHandler\n const webResponse = waitUntilFn\n ? await runWithWaitUntil(waitUntilFn, wrappedHandler)\n : await wrappedHandler()`;\n } else if (earlyHints) {\n handlerCall = ` 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 } else if (supportsWaitUntil) {\n handlerCall = ` const waitUntilFn = (typeof event.waitUntil === 'function')\n ? (p) => event.waitUntil(p)\n : undefined\n\n const webResponse = waitUntilFn\n ? await runWithWaitUntil(waitUntilFn, () => handler(webRequest))\n : await handler(webRequest)`;\n } else {\n handlerCall = ` const webResponse = await handler(webRequest)`;\n }\n\n // Import runWithWaitUntil only when the preset supports it.\n const waitUntilImport = supportsWaitUntil ? ', runWithWaitUntil' : '';\n\n const compressImport = compress ? \"import { compressResponse } from './_compress.mjs'\\n\" : '';\n const compressCall = compress ? 'compressResponse(webRequest, webResponse)' : 'webResponse';\n\n return `// Generated by @timber-js/app/adapters/nitro\n// Do not edit — this file is regenerated on each build.\n\nimport handler, { runWithEarlyHintsSender${waitUntilImport} } from '${serverEntryRelative}'\n${compressImport}\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\n// Nitro's index.mjs wraps this import with defineLazyEventHandler, which\n// already handles the event handler protocol. We export a plain async\n// function — no defineEventHandler wrapper needed.\n// Nitro bundles h3 into its internal _libs/ directory but doesn't make\n// it available as a bare specifier in the output's node_modules.\nexport default async function timberHandler(event) {\n // h3 v2: event.req is the Web Request\n const webRequest = event.req\n${handlerCall}\n return ${compressCall}\n}\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\n// Import compression helper for self-hosted response compression.\nconst { compressResponse } = await import('./_compress.mjs');\n\n// Web→Node response bridge with backpressure (TIM-1154).\nconst { sendNodeResponse } = await import('./_send-response.mjs');\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}');\n\n// Port resolution (TIM-842):\n// - Default to 3000\n// - If PORT env var is set, honor it strictly (fail loudly on conflict)\n// - Otherwise auto-bump from 3000 until a free port is found\n// Mirrors the dev server behavior so timber dev/preview/production all\n// behave the same way around port selection.\nconst envPort = process.env.PORT ? parseInt(process.env.PORT, 10) : null;\nconst portIsExplicit = envPort != null && Number.isFinite(envPort) && envPort > 0;\nconst startPort = portIsExplicit ? envPort : 3000;\nconst host = process.env.HOST || 'localhost';\n\n// Set after listenWithBump() resolves so request handlers can build\n// absolute URLs from the actual bound port (which may differ from\n// startPort after an auto-bump).\nlet boundPort = startPort;\n\nconst server = createServer(async (req, res) => {\n // Scheme from the socket's TLS state — a hardcoded http:// breaks CSRF\n // origin comparison and absolute-URL construction when TLS is terminated\n // at this server (TIM-1073; mirrors the dev-server fix in TIM-1067).\n // X-Forwarded-Proto is intentionally not consulted here: on a direct\n // connection it would be attacker-controlled. The CSRF layer honors it\n // separately for reverse-proxy deployments.\n const scheme = req.socket?.encrypted ? 'https' : 'http';\n const url = new URL(req.url || '/', \\`\\${scheme}://\\${host}:\\${boundPort}\\`);\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 isImmutable =\n url.pathname.startsWith('/assets/') || url.pathname.startsWith('/_timber/fonts/');\n const cacheControl = isImmutable\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 rawResponse = earlyHintsSender && runWithEarlyHintsSender\n ? await runWithEarlyHintsSender(earlyHintsSender, () => handler(webRequest))\n : await handler(webRequest);\n\n // Compress the response for self-hosted deployments.\n const webResponse = compressResponse(webRequest, rawResponse);\n\n // Write the response to Node's ServerResponse. sendNodeResponse handles\n // status, headers (including Set-Cookie splitting via [...headers]),\n // body streaming with backpressure, and client disconnect cleanup.\n // See TIM-1154.\n await sendNodeResponse(res, webResponse);\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\nfunction listenWithBump(port, attempt = 0) {\n return new Promise((resolveListen, rejectListen) => {\n const onError = (err) => {\n server.removeListener('listening', onListening);\n if (err && err.code === 'EADDRINUSE' && !portIsExplicit && attempt < 100) {\n if (attempt === 0) {\n console.log(\\` [timber] Port \\${port} in use, trying \\${port + 1}...\\`);\n }\n listenWithBump(port + 1, attempt + 1).then(resolveListen, rejectListen);\n return;\n }\n rejectListen(err);\n };\n const onListening = () => {\n server.removeListener('error', onError);\n resolveListen(port);\n };\n server.once('error', onError);\n server.once('listening', onListening);\n server.listen(port, host);\n });\n}\n\ntry {\n boundPort = await listenWithBump(startPort);\n if (boundPort !== startPort) {\n console.log();\n console.log(\\` [timber] Port \\${startPort} in use, using \\${boundPort}\\`);\n }\n console.log();\n console.log(' ⚡ timber preview server running at:');\n console.log();\n console.log(\\` ➜ http://\\${host}:\\${boundPort}\\`);\n console.log();\n} catch (err) {\n if (err && err.code === 'EADDRINUSE') {\n console.error(\\`[timber] Port \\${startPort} is already in use. \\` +\n 'Set PORT to a free port, or unset PORT to auto-pick.');\n } else {\n console.error('[timber] Failed to start preview server:', err);\n }\n process.exit(1);\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 */\ninterface NitroBuildResult {\n /** Resolved output.serverDir — where the server bundle + _chunks/ live. */\n serverDir: string;\n}\n\nasync function runNitroBuild(\n nitroDir: string,\n preset: NitroPreset,\n userConfig?: Record<string, unknown>\n): Promise<NitroBuildResult> {\n const presetConfig = PRESET_CONFIGS[preset];\n const {\n createNitro,\n build: nitroBuild,\n prepare,\n copyPublicAssets,\n } = await import('nitro/builder');\n\n const nitro = await createNitro({\n rootDir: nitroDir,\n preset: presetConfig.nitroPreset,\n // Use renderer.entry so Nitro wraps our handler with its server runtime\n // (HTTP server, static file serving, graceful shutdown, etc.).\n // Using `entry` directly would bypass the Nitro server runtime.\n renderer: { handler: join(nitroDir, 'entry.ts') },\n output: { dir: join(nitroDir, presetConfig.outputDir) },\n routeRules: {\n '/assets/**': { headers: { 'Cache-Control': IMMUTABLE_CACHE } },\n '/_timber/fonts/**': { 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\\//],\n },\n ...presetConfig.extraConfig,\n ...userConfig,\n });\n\n // Read the resolved serverDir AFTER config merging — userConfig may\n // override output.dir or output.serverDir. Nitro appends a trailing\n // slash during resolution; strip it for join() compatibility.\n const raw = nitro.options.output.serverDir;\n const serverDir = raw.endsWith('/') ? raw.slice(0, -1) : raw;\n\n await prepare(nitro);\n await copyPublicAssets(nitro);\n await nitroBuild(nitro);\n await nitro.close();\n\n return { serverDir };\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// ─── Send Response Module ───────────────────────────────────────────────────\n\n/**\n * Generate a standalone ESM module that exports sendNodeResponse.\n *\n * Mirrors srvx/node's sendNodeResponse: status + headers via writeHead,\n * body streaming with backpressure (waits for drain on write() === false),\n * and client disconnect cleanup. Written to `_send-response.mjs` during\n * buildOutput for use by the preview server script. See TIM-1154.\n *\n * @internal Exported for testing.\n */\nexport function generateSendResponseModule(): string {\n return `// Generated by @timber-js/app — Web→Node response bridge.\n// Do not edit — this file is regenerated on each build.\n// Mirrors srvx/node's sendNodeResponse with backpressure support.\n\nexport function sendNodeResponse(nodeRes, webRes) {\n if (!webRes) {\n nodeRes.statusCode = 500;\n return new Promise((resolve) => nodeRes.end(resolve));\n }\n const rawHeaders = [...webRes.headers];\n const writeHeaders = rawHeaders.flat();\n if (!nodeRes.headersSent) {\n if (nodeRes.req?.httpVersion === '2.0') {\n nodeRes.writeHead(webRes.status, writeHeaders);\n } else {\n nodeRes.writeHead(webRes.status, webRes.statusText, writeHeaders);\n }\n }\n if (!webRes.body) {\n return new Promise((resolve) => nodeRes.end(resolve));\n }\n // Stream with backpressure: pause reading when the kernel buffer is\n // full (write() returns false) and resume on 'drain'.\n if (nodeRes.destroyed) {\n webRes.body.cancel();\n return;\n }\n const reader = webRes.body.getReader();\n function streamCancel(error) {\n reader.cancel(error).catch(() => {});\n if (error) nodeRes.destroy(error);\n }\n function streamHandle({ done, value }) {\n try {\n if (done) {\n nodeRes.end();\n } else if (nodeRes.write(value)) {\n reader.read().then(streamHandle, streamCancel);\n } else {\n nodeRes.once('drain', () => reader.read().then(streamHandle, streamCancel));\n }\n } catch (error) {\n streamCancel(error instanceof Error ? error : undefined);\n }\n }\n nodeRes.on('close', streamCancel);\n nodeRes.on('error', streamCancel);\n reader.read().then(streamHandle, streamCancel);\n return reader.closed.catch(streamCancel).finally(() => {\n nodeRes.off('close', streamCancel);\n nodeRes.off('error', streamCancel);\n });\n}\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\n/**\n * Resolve the destination path for prebuilt flight payloads in the Nitro\n * output directory. The RSC entry resolves `../prebuilt/` relative to its\n * bundled chunk location (`<serverBundleDir>/_chunks/`), so prebuilt/ must\n * live at `<serverBundleDir>/prebuilt/`.\n *\n * The serverBundleDir varies by preset — Nitro's Vercel preset uses\n * `functions/__server.func` while most others use `server`.\n *\n * @internal Exported for testing.\n */\nexport function resolvePrebuiltOutputPath(outDir: string, preset: NitroPreset): string {\n const presetConfig = PRESET_CONFIGS[preset];\n return join(outDir, presetConfig.outputDir, presetConfig.serverBundleDir, 'prebuilt');\n}\n\n/**\n * Copy prebuilt payloads into cloned Vercel function directories.\n *\n * When users configure `vercel.functionRules`, Nitro's compiled hook clones\n * the base __server.func to per-route .func directories. These clones are\n * created during `runNitroBuild()` — before our post-build prebuilt copy.\n * Each clone resolves `../prebuilt` independently, so they each need a copy.\n *\n * @internal Exported for testing.\n */\nexport async function copyPrebuiltToVercelFuncClones(\n functionsDir: string,\n prebuiltSrc: string,\n baseFuncName: string\n): Promise<void> {\n const funcDirs: string[] = [];\n try {\n for await (const match of glob('**/*.func', { cwd: functionsDir })) {\n if (!match.endsWith(baseFuncName)) {\n funcDirs.push(join(functionsDir, match));\n }\n }\n } catch {\n return;\n }\n await Promise.all(\n funcDirs.map((dir) =>\n cp(prebuiltSrc, join(dir, 'prebuilt'), { recursive: true }).catch(\n (e: NodeJS.ErrnoException) => {\n if (e.code !== 'ENOENT') throw e;\n }\n )\n )\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAyBA,eAAsB,yBAA0C;CAK9D,OACE,oIAEA,MAPmB,iBAAiB,kBAAgB,eAAe,EACnE,MAAM,KACR,CAAC,EAAA,CAKQ;AAEX;;;ACyBA,IAAM,iBAAoD;CACxD,UAAU;EACR,aAAa;EACb,WAAW;EACX,iBAAiB;EACjB,mBAAmB;EACnB,oBAAoB;EACpB,aAAa;EACb,aAAa,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,GAAG,EAAE,EAAE;CAC5D;CACA,eAAe;EACb,aAAa;EACb,WAAW;EACX,iBAAiB;EACjB,mBAAmB;EACnB,oBAAoB;EACpB,aAAa;CACf;CACA,WAAW;EACT,aAAa;EACb,WAAW;EACX,iBAAiB;EACjB,mBAAmB;EACnB,oBAAoB;EACpB,aAAa;CACf;CACA,gBAAgB;EACd,aAAa;EACb,WAAW;EACX,iBAAiB;EACjB,mBAAmB;EACnB,oBAAoB;EACpB,aAAa;CACf;CACA,cAAc;EACZ,aAAa;EACb,WAAW;EACX,iBAAiB;EACjB,mBAAmB;EACnB,oBAAoB;EACpB,aAAa;CACf;CACA,eAAe;EACb,aAAa;EACb,WAAW;EACX,iBAAiB;EACjB,mBAAmB;EACnB,oBAAoB;EACpB,aAAa;CACf;CACA,mBAAmB;EACjB,aAAa;EACb,WAAW;EACX,iBAAiB;EACjB,mBAAmB;EACnB,oBAAoB;EACpB,aAAa;CACf;CACA,eAAe;EACb,aAAa;EACb,WAAW;EACX,iBAAiB;EACjB,mBAAmB;EAQnB,oBAAoB;EACpB,aAAa;CACf;CACA,OAAO;EACL,aAAa;EACb,WAAW;EACX,iBAAiB;EACjB,mBAAmB;EAGnB,oBAAoB;EACpB,aAAa;CACf;AACF;;;;;;;;;;;;;;;;;AAsEA,SAAgB,MAAM,UAA+B,CAAC,GAA0B;CAC9E,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,WAAW,QAAQ,YAAY;CAChB,eAAe;CACpC,OAAO;EACL,MAAM,SAAS;EAEf,MAAM,YAAY,QAAsB,UAAkB;GACxD,MAAM,SAAS,KAAK,UAAU,OAAO;GAIrC,IAAI,OAAO,WAAW,YAAY,OAAO,gBAAgB;IACvD,MAAM,2BAA2B,QAAQ,UAAU,QAAQ,QAAQ;IACnE,MAAM,YAAY,KAAK,QAAQ,QAAQ;IAIvC,MAAM,gBAAgB,MAAM,wBAAwB,SAAS;IAC7D,MAAM,UACJ,KAAK,WAAW,UAAU,GAC1B,oBAAoB,eAAe,OAAO,kBAAkB,CAC9D;IAIA,KADe,QAAQ,gBAAgB,eACxB,gBAAgB;KAC7B,MAAM,UAAU,KAAK,WAAW,WAAW,GAAG,EAAE;KAGhD,IAAI;MAEF,KAAI,MADoB,SAAS,KAAK,WAAW,YAAY,GAAG,OAAO,EAAA,CACzD,KAAK,CAAC,CAAC,SAAS,GAC5B,QAAQ,KACN,wIAEF;KAEJ,QAAQ,CAER;IACF;IAEA;GACF;GAIA,MAAM,oBAAoB;IACxB;IACA;IACA;IACA,eAAe;GACjB,CAAC;GAKD,MAAM,UAAU,KAAK,QAAQ,eAAe,GAAG,MAAM,uBAAuB,CAAC;GAG7E,MAAM,UAAU,KAAK,QAAQ,oBAAoB,GAAG,2BAA2B,CAAC;GAMhF,IAAI,OAAO,cAAc;IACvB,MAAM,WAAW,KAAK,QAAQ,OAAO,UAAU;IAC/C,MAAM,aAAa,MAAM,SAAS,UAAU,OAAO;IACnD,MAAM,UAAU,UAAU,GAAG,OAAO,aAAa,IAAI,YAAY;GACnE;GAGA,MAAM,QAAQ,mBAAmB,UAAU,QAAQ,QAAQ,QAAQ;GACnE,MAAM,UAAU,KAAK,QAAQ,UAAU,GAAG,KAAK;GAK/C,MAAM,EAAE,cAAc,MAAM,cAAc,QAAQ,QAAQ,QAAQ,WAAW;GAQ7E,MAAM,cAAc,KAAK,UAAU,UAAU;GAC7C,MAAM,GAAG,aAAa,KAAK,WAAW,UAAU,GAAG,EACjD,WAAW,KACb,CAAC,CAAC,CAAC,OAAO,MAA6B;IACrC,IAAI,EAAE,SAAS,UAAU,MAAM;GACjC,CAAC;GAKD,IAAI,WAAW,YAAY,WAAW,eAGpC,MAAM,+BAFe,QAAQ,SAEQ,GAAc,aAD9B,SAAS,SACkC,CAAY;EAEhF;EAKA,SAAS,oBAAoB,IAAI,MAAM,IACnC,OAAO,SAAuB,aAAqB;GAIjD,MAAM,gBAAgB,sBAAsB,UAAU,MAAM;GAC5D,MAAM,aAAa,KAAK,UAAU,SAAS,qBAAqB;GAChE,MAAM,UAAU,YAAY,aAAa;GAGzC,MAAM,kBADU,WAAW,QAAQ,QAAQ,QACV,CAAC,UAAU,GAAG,KAAK,UAAU,OAAO,CAAC;EACxE,IACA,KAAA;CACN;AACF;;AAKA,SAAgB,mBACd,UACA,QACA,QACA,WAAW,MACH;CAKR,MAAM,sBAAsB;CAC5B,MAAM,cAAc,eAAe,OAAO,CAAC;CAC3C,MAAM,aAAa,eAAe,OAAO,CAAC;CAC1C,MAAM,oBAAoB,eAAe,OAAO,CAAC;CASjD,IAAI;CACJ,IAAI,cAAc,mBAChB,cAAc;;;;;;;;;;;;;;;;MAgBT,IAAI,YACT,cAAc;;;;;;;;MAQT,IAAI,mBACT,cAAc;;;;;;;MAQd,cAAc;CAShB,OAAO;;;2CALiB,oBAAoB,uBAAuB,GAQV,WAAW,oBAAoB;EANjE,WAAW,yDAAyD,GAO5E;;;gCAGe,YAAY;;;;;;;;;;EAU1C,YAAY;WAnBS,WAAW,8CAA8C,cAoBxD;;;AAGxB;;AAKA,IAAM,sCAAsB,IAAI,IAAiB,CAAC,eAAe,KAAK,CAAC;;;;;;;;;AAUvE,SAAgB,sBAAsB,UAAkB,QAA6B;CACnF,MAAM,mBAAmB,SAAS,KAAK,UAAU,OAAO,GAAG,KAAK,UAAU,OAAO,UAAU,CAAC;CAC5F,MAAM,WAAW,iBAAiB,WAAW,GAAG,IAAI,mBAAmB,OAAO;CAC9E,MAAM,YAAY;CAClB,MAAM,mBAAmB;CAGzB,OAAO;;;;;;;;;;;gCAFa,eAAe,OAAO,CAAC,YAaD;;;;wCAIJ,iBAAiB;;kBAEvC,iBAAiB;;;;sEAImC,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qCAiC1C,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4J/C;;AAUA,SAAgB,4BACd,UACA,QAC4B;CAC5B,IAAI,CAAC,oBAAoB,IAAI,MAAM,GAAG,OAAO;CAE7C,MAAM,WAAW,KAAK,UAAU,OAAO;CACvC,MAAM,YAAY,KAAK,UAAU,UAAU;CAG3C,OAAO;EACL,SAFc,WAAW,QAAQ,QAAQ;EAGzC,MAAM,CAAC,SAAS;EAChB,KAAK;CACP;AACF;AAaA,eAAe,cACb,UACA,QACA,YAC2B;CAC3B,MAAM,eAAe,eAAe;CACpC,MAAM,EACJ,aACA,OAAO,YACP,SACA,qBACE,MAAM,OAAO;CAEjB,MAAM,QAAQ,MAAM,YAAY;EAC9B,SAAS;EACT,QAAQ,aAAa;EAIrB,UAAU,EAAE,SAAS,KAAK,UAAU,UAAU,EAAE;EAChD,QAAQ,EAAE,KAAK,KAAK,UAAU,aAAa,SAAS,EAAE;EACtD,YAAY;GACV,cAAc,EAAE,SAAS,EAAE,iBAAiB,gBAAgB,EAAE;GAC9D,qBAAqB,EAAE,SAAS,EAAE,iBAAiB,gBAAgB,EAAE;EACvE;EAIA,cAAc,EACZ,UAAU,CAAC,aAAa,EAC1B;EACA,GAAG,aAAa;EAChB,GAAG;CACL,CAAC;CAKD,MAAM,MAAM,MAAM,QAAQ,OAAO;CACjC,MAAM,YAAY,IAAI,SAAS,GAAG,IAAI,IAAI,MAAM,GAAG,EAAE,IAAI;CAEzD,MAAM,QAAQ,KAAK;CACnB,MAAM,iBAAiB,KAAK;CAC5B,MAAM,WAAW,KAAK;CACtB,MAAM,MAAM,MAAM;CAElB,OAAO,EAAE,UAAU;AACrB;;AAGA,SAAS,kBAAkB,SAAiB,MAAgB,KAA4B;CACtF,OAAO,IAAI,SAAe,SAAS,WAAW;EAC5C,MAAM,QAAQ,SAAS,SAAS,MAAM,EAAE,IAAI,IAAI,QAAQ;GACtD,IAAI,KAAK,OAAO,GAAG;QACd,QAAQ;EACf,CAAC;EACD,MAAM,QAAQ,KAAK,QAAQ,MAAM;EACjC,MAAM,QAAQ,KAAK,QAAQ,MAAM;CACnC,CAAC;AACH;;;;;;;;;;;AAcA,SAAgB,6BAAqC;CACnD,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDT;;;;;AAQA,SAAgB,gBAAgB,QAAmC;CACjE,OAAO,eAAe;AACxB;;;;;;;;;;;;AAaA,SAAgB,0BAA0B,QAAgB,QAA6B;CACrF,MAAM,eAAe,eAAe;CACpC,OAAO,KAAK,QAAQ,aAAa,WAAW,aAAa,iBAAiB,UAAU;AACtF;;;;;;;;;;;AAYA,eAAsB,+BACpB,cACA,aACA,cACe;CACf,MAAM,WAAqB,CAAC;CAC5B,IAAI;EACF,WAAW,MAAM,SAAS,KAAK,aAAa,EAAE,KAAK,aAAa,CAAC,GAC/D,IAAI,CAAC,MAAM,SAAS,YAAY,GAC9B,SAAS,KAAK,KAAK,cAAc,KAAK,CAAC;CAG7C,QAAQ;EACN;CACF;CACA,MAAM,QAAQ,IACZ,SAAS,KAAK,QACZ,GAAG,aAAa,KAAK,KAAK,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC,CAAC,CAAC,OACzD,MAA6B;EAC5B,IAAI,EAAE,SAAS,UAAU,MAAM;CACjC,CACF,CACF,CACF;AACF"}
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
export declare const IMMUTABLE_CACHE = "public, max-age=31536000, immutable";
|
|
3
3
|
/** Cache-Control value for unhashed static assets. */
|
|
4
4
|
export declare const STATIC_CACHE = "public, max-age=3600, must-revalidate";
|
|
5
|
-
export declare function generateHeadersFile(unhashedPaths?: string[]): string;
|
|
5
|
+
export declare function generateHeadersFile(unhashedPaths?: string[], contentTypes?: Record<string, string>): string;
|
|
6
6
|
//# sourceMappingURL=shared.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/adapters/shared.ts"],"names":[],"mappings":"AAaA,yDAAyD;AACzD,eAAO,MAAM,eAAe,wCAAwC,CAAC;AAErE,sDAAsD;AACtD,eAAO,MAAM,YAAY,0CAA0C,CAAC;AAuBpE,wBAAgB,mBAAmB,
|
|
1
|
+
{"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/adapters/shared.ts"],"names":[],"mappings":"AAaA,yDAAyD;AACzD,eAAO,MAAM,eAAe,wCAAwC,CAAC;AAErE,sDAAsD;AACtD,eAAO,MAAM,YAAY,0CAA0C,CAAC;AAuBpE,wBAAgB,mBAAmB,CACjC,aAAa,CAAC,EAAE,MAAM,EAAE,EACxB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACpC,MAAM,CAuDR"}
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -13,6 +13,20 @@ export interface TimberConfig {
|
|
|
13
13
|
* Undefined when no build manifest was produced (e.g., dev mode or no client assets).
|
|
14
14
|
*/
|
|
15
15
|
manifestInit?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Absolute path to the directory containing pre-rendered static HTML,
|
|
18
|
+
* RSC flight files, and API route outputs. Present only when
|
|
19
|
+
* output: 'static' and the static generation pass completed.
|
|
20
|
+
* Layout: path/index.html, _rsc/path.rsc, api-route-path
|
|
21
|
+
*/
|
|
22
|
+
staticPagesDir?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Content-Type mappings for extensionless API route outputs.
|
|
25
|
+
* Map from URL path (e.g. "/api/data") to Content-Type value.
|
|
26
|
+
* Written during static generation, consumed by adapters when
|
|
27
|
+
* writing _headers. See TIM-1241.
|
|
28
|
+
*/
|
|
29
|
+
staticContentTypes?: Record<string, string>;
|
|
16
30
|
}
|
|
17
31
|
/**
|
|
18
32
|
* The formal adapter interface. An adapter transforms the build output
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/adapters/types.ts"],"names":[],"mappings":"AAMA;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,qDAAqD;IACrD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC5B,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/adapters/types.ts"],"names":[],"mappings":"AAMA;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,qDAAqD;IACrD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC5B,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7C;AAED;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,WAAW,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnE;;;OAGG;IACH,OAAO,CAAC,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEhE;;;;;;;;;OASG;IACH,cAAc,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CACxC"}
|