@timber-js/app 0.2.0-alpha.194 → 0.2.0-alpha.196

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LICENSE +8 -0
  2. package/dist/_chunks/{cli-schema-sync-ZwM9u_ob.js → cli-schema-sync-3Wutm8pH.js} +2 -2
  3. package/dist/_chunks/{cli-schema-sync-ZwM9u_ob.js.map → cli-schema-sync-3Wutm8pH.js.map} +1 -1
  4. package/dist/_chunks/{error-boundary-D-ODYX41.js → error-boundary-D-lkwyaD.js} +3 -3
  5. package/dist/_chunks/{error-boundary-D-ODYX41.js.map → error-boundary-D-lkwyaD.js.map} +1 -1
  6. package/dist/_chunks/{router-ref-DuYuV_0Q.js → router-ref-BzqbPwYC.js} +2 -2
  7. package/dist/_chunks/{router-ref-DuYuV_0Q.js.map → router-ref-BzqbPwYC.js.map} +1 -1
  8. package/dist/_chunks/{ssr-data-14MXm7Pj.js → ssr-data-Ya2HJPFp.js} +1 -7
  9. package/dist/_chunks/{ssr-data-14MXm7Pj.js.map → ssr-data-Ya2HJPFp.js.map} +1 -1
  10. package/dist/_chunks/{use-segment-params-C4r4BD9T.js → use-segment-params-DzTBpkvj.js} +3 -3
  11. package/dist/_chunks/{use-segment-params-C4r4BD9T.js.map → use-segment-params-DzTBpkvj.js.map} +1 -1
  12. package/dist/_chunks/{walkers-uCu3WW6_.js → walkers-BU6z9xRV.js} +2 -2
  13. package/dist/_chunks/{walkers-uCu3WW6_.js.map → walkers-BU6z9xRV.js.map} +1 -1
  14. package/dist/cli.js +1 -1
  15. package/dist/client/error-boundary.js +1 -1
  16. package/dist/client/index.js +5 -5
  17. package/dist/client/index.js.map +1 -1
  18. package/dist/client/internal.js +4 -4
  19. package/dist/client/link.d.ts.map +1 -1
  20. package/dist/config-types.d.ts +12 -9
  21. package/dist/config-types.d.ts.map +1 -1
  22. package/dist/cookies/index.js +1 -1
  23. package/dist/index.d.ts +0 -15
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +79 -35
  26. package/dist/index.js.map +1 -1
  27. package/dist/routing/index.js +1 -1
  28. package/dist/server/index.js +1 -6
  29. package/dist/server/index.js.map +1 -1
  30. package/dist/server/internal.js +1 -1
  31. package/docs/api/34-api-config.mdx +7 -4
  32. package/docs/learn/13-configuration.mdx +1 -1
  33. package/package.json +12 -12
  34. package/src/cli.ts +0 -0
  35. package/src/client/link.tsx +8 -2
  36. package/src/config-types.ts +12 -9
  37. package/src/index.ts +44 -58
package/LICENSE ADDED
@@ -0,0 +1,8 @@
1
+ DONTFUCKINGUSE LICENSE
2
+
3
+ Copyright (c) 2025 Daniel Saewitz
4
+
5
+ This software may not be used, copied, modified, merged, published,
6
+ distributed, sublicensed, or sold by anyone other than the copyright holder.
7
+
8
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
@@ -187,7 +187,7 @@ function ensureCodecImport(content) {
187
187
  const lines = content.split("\n");
188
188
  let lastImportIndex = -1;
189
189
  for (let i = 0; i < lines.length; i++) if (/^\s*import\s/.test(lines[i])) lastImportIndex = i;
190
- if (lastImportIndex === -1) return "import { codec } from '@timber-js/app/codec';\n" + content;
190
+ if (lastImportIndex === -1) return CODEC_IMPORT + "\n" + content;
191
191
  lines.splice(lastImportIndex + 1, 0, CODEC_IMPORT);
192
192
  return lines.join("\n");
193
193
  }
@@ -296,4 +296,4 @@ function runSchemaSync(projectRoot) {
296
296
  //#endregion
297
297
  export { findSchemaFile as n, parseExistingSchemaKeys as r, cli_schema_sync_exports as t };
298
298
 
299
- //# sourceMappingURL=cli-schema-sync-ZwM9u_ob.js.map
299
+ //# sourceMappingURL=cli-schema-sync-3Wutm8pH.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"cli-schema-sync-ZwM9u_ob.js","names":[],"sources":["../../src/cli-schema-sync.ts"],"sourcesContent":["/**\n * `timber schema sync` — scan filesystem for dynamic segments, append\n * missing entries with `codec.string` defaults, and remove stale entries\n * whose segments no longer exist in the filesystem.\n *\n * Design doc: design/41-global-params.md §Auto-Scaffolding\n */\n\nimport { readFileSync, writeFileSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { scanRoutes } from './routing/scanner.ts';\nimport type { SegmentNode } from './routing/types.ts';\nimport { resolveAppDir } from './plugin-context.ts';\n\nfunction toBracketKey(node: SegmentNode): string {\n if (node.paramPrefix || node.paramSuffix) return `[${node.paramName}]`;\n switch (node.segmentType) {\n case 'catch-all':\n return `[...${node.paramName}]`;\n case 'optional-catch-all':\n return `[[...${node.paramName}]]`;\n default:\n return `[${node.paramName}]`;\n }\n}\n\n/**\n * Collect all unique bracket-keyed dynamic segment names from the route tree.\n * Returns entries like '[artistSlug]', '[...slug]', '[[...topic]]'.\n */\nexport function collectDynamicSegments(appDir: string): string[] {\n const tree = scanRoutes(appDir);\n const segments = new Set<string>();\n walkSegments(tree.root, segments);\n return [...segments].sort();\n}\n\nfunction walkSegments(node: SegmentNode, segments: Set<string>): void {\n if (node.paramName) {\n // Use bare bracket form for schema keys — affixed segments like\n // `img-[id].png` register as `[id]`, matching runtime codec lookup.\n segments.add(toBracketKey(node));\n }\n for (const child of node.children) {\n walkSegments(child, segments);\n }\n for (const slot of Object.values(node.slots)) {\n walkSegments(slot, segments);\n }\n}\n\n/**\n * Replace comment bodies (and optionally string contents) with spaces,\n * preserving length and newlines so every index maps 1:1 to the original.\n * Lets the structural scanning below ignore `segmentParams` mentions and\n * braces inside comments/strings without a TS parser (fine pre-alpha,\n * see TIM-1066).\n *\n * With `maskStrings`, string contents are blanked too — except literal\n * 'segmentParams' strings, kept so a quoted property key stays findable.\n */\nfunction maskSource(content: string, maskStrings: boolean): string {\n const out = content.split('');\n const n = content.length;\n let i = 0;\n\n const blank = (from: number, to: number): void => {\n for (let j = from; j < to; j++) {\n if (content[j] !== '\\n') out[j] = ' ';\n }\n };\n\n while (i < n) {\n const ch = content[i];\n const next = content[i + 1];\n if (ch === '/' && next === '/') {\n const start = i;\n while (i < n && content[i] !== '\\n') i++;\n blank(start, i);\n } else if (ch === '/' && next === '*') {\n const start = i;\n i += 2;\n while (i < n && !(content[i] === '*' && content[i + 1] === '/')) i++;\n i = Math.min(i + 2, n);\n blank(start, i);\n } else if (ch === '\"' || ch === \"'\" || ch === '`') {\n const start = i;\n i++;\n while (i < n && content[i] !== ch) {\n if (content[i] === '\\\\') i++;\n i++;\n }\n const end = i; // at closing quote (or n if unterminated)\n if (maskStrings && content.slice(start + 1, end) !== 'segmentParams') {\n blank(start + 1, end);\n }\n i++;\n } else {\n i++;\n }\n }\n return out.join('');\n}\n\n/**\n * Locate the `segmentParams: { ... }` block in a schema file.\n * Returns the indices of the opening and matching closing brace, or null\n * if no segmentParams property is found. Comments and strings are masked\n * first, so a JSDoc example mentioning `segmentParams: { ... }` above the\n * real defineSchema call is not mistaken for the block, and braces inside\n * comments/strings don't skew the depth count. The search is anchored to\n * the `defineSchema(` call when present, so helper objects with their own\n * segmentParams property earlier in the file are skipped.\n */\nexport function findSegmentParamsBlock(\n content: string\n): { openBrace: number; closeBrace: number } | null {\n const masked = maskSource(content, true);\n\n const defineCall = /\\bdefineSchema\\s*\\(/.exec(masked);\n const keyRe = /(['\"]?)segmentParams\\1\\s*:\\s*\\{/g;\n keyRe.lastIndex = defineCall ? defineCall.index + defineCall[0].length : 0;\n const keyMatch = keyRe.exec(masked);\n if (!keyMatch) return null;\n\n const openBrace = keyMatch.index + keyMatch[0].length - 1;\n let depth = 1;\n let i = openBrace + 1;\n while (i < masked.length && depth > 0) {\n if (masked[i] === '{') depth++;\n else if (masked[i] === '}') depth--;\n i++;\n }\n if (depth > 0) return null;\n // i now points to just after the closing brace\n return { openBrace, closeBrace: i - 1 };\n}\n\n/**\n * Parse existing bracket keys from an app/schema.ts file.\n * Scoped to the segmentParams: { ... } block — bracket keys elsewhere in\n * the file (unrelated objects, comments above defineSchema) are ignored.\n * Comments inside the block are masked, so a commented-out entry does not\n * count as registered.\n *\n * The key charset matches the route classifier (segment-classify.ts):\n * any character except `]`, so hyphenated params like '[my-param]' parse.\n */\nexport function parseExistingSchemaKeys(content: string): string[] {\n const block = findSegmentParamsBlock(content);\n if (!block) return [];\n\n const blockContent = maskSource(content, false).slice(block.openBrace, block.closeBrace);\n const keys: string[] = [];\n // Match quoted bracket keys like '[artistSlug]', '[...slug]', '[[...topic]]'\n const re = /['\"](\\[{1,2}(?:\\.{3})?[^\\]]+\\]{1,2})['\"]\\s*:/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(blockContent)) !== null) {\n keys.push(m[1]);\n }\n return keys;\n}\n\n/** Schema file extensions to check. */\nconst SCHEMA_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];\n\n/**\n * Find the schema file path if it exists.\n */\nexport function findSchemaFile(appDir: string): string | undefined {\n for (const ext of SCHEMA_EXTENSIONS) {\n const p = join(appDir, `schema${ext}`);\n if (existsSync(p)) return p;\n }\n return undefined;\n}\n\nexport function defaultCodecForSegment(seg: string): string {\n if (seg.startsWith('[[...')) return 'codec.optionalCatchAll(codec.string)';\n if (seg.startsWith('[...')) return 'codec.stringArray';\n return 'codec.string';\n}\n\n/**\n * Generate the content for a new app/schema.ts with the given segment keys.\n */\nfunction generateNewSchema(segments: string[]): string {\n const lines: string[] = [];\n lines.push(\"import { defineSchema } from '@timber-js/app/params';\");\n lines.push(\"import { codec } from '@timber-js/app/codec';\");\n lines.push('');\n lines.push('export default defineSchema({');\n lines.push(' segmentParams: {');\n for (const seg of segments) {\n lines.push(` '${seg}': ${defaultCodecForSegment(seg)},`);\n }\n lines.push(' },');\n lines.push('});');\n lines.push('');\n return lines.join('\\n');\n}\n\n/**\n * Append missing segment entries to an existing schema file.\n * Inserts new entries before the closing `}` of segmentParams.\n */\nconst CODEC_IMPORT = \"import { codec } from '@timber-js/app/codec';\";\n\nfunction hasCodecImport(content: string): boolean {\n return /import\\s+\\{[^}]*\\bcodec\\b[^}]*\\}\\s+from\\s+['\"]@timber-js\\/app\\/codec['\"]/.test(content);\n}\n\nfunction ensureCodecImport(content: string): string {\n if (hasCodecImport(content)) return content;\n\n // Insert after the last existing import statement\n const lines = content.split('\\n');\n let lastImportIndex = -1;\n for (let i = 0; i < lines.length; i++) {\n if (/^\\s*import\\s/.test(lines[i])) lastImportIndex = i;\n }\n\n if (lastImportIndex === -1) {\n return CODEC_IMPORT + '\\n' + content;\n }\n\n lines.splice(lastImportIndex + 1, 0, CODEC_IMPORT);\n return lines.join('\\n');\n}\n\nfunction appendToSchema(content: string, newSegments: string[]): string {\n content = ensureCodecImport(content);\n\n const block = findSegmentParamsBlock(content);\n if (!block) {\n throw new Error(\n '[timber] Could not find segmentParams in schema file. ' +\n 'Expected a defineSchema({ segmentParams: { ... } }) call.'\n );\n }\n const { openBrace, closeBrace } = block;\n\n // Detect indentation from existing entries\n const existingLine = content.slice(openBrace, closeBrace).match(/^(\\s+)['\"]\\[/m);\n const indent = existingLine ? existingLine[1] : ' ';\n\n // Build insertion text\n const insertion = newSegments\n .map((seg) => `${indent}'${seg}': ${defaultCodecForSegment(seg)},`)\n .join('\\n');\n\n // Insert before the closing brace\n const before = content.slice(0, closeBrace);\n const after = content.slice(closeBrace);\n\n // Add newline if the content before doesn't end with one\n const separator = before.trimEnd().endsWith(',') ? '\\n' : '\\n';\n\n return before + separator + insertion + '\\n' + after;\n}\n\n/**\n * Remove entries from the segmentParams block whose keys match `staleSegments`.\n * Removes the entire entry including trailing comma/whitespace. Scoped to\n * the segmentParams block — identical keys in unrelated objects elsewhere\n * in the file are left untouched.\n */\nfunction removeFromSchema(content: string, staleSegments: string[]): string {\n const block = findSegmentParamsBlock(content);\n if (!block) return content;\n\n let blockContent = content.slice(block.openBrace, block.closeBrace);\n // Key matching and the entry walk run on a comment-masked copy (same\n // length, indices align) so commented-out entries are never removed.\n // Both copies are spliced in sync to keep indices valid.\n let blockMasked = maskSource(blockContent, false);\n\n for (const seg of staleSegments) {\n const escaped = seg.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const keyRe = new RegExp(`^[ \\t]*['\"]${escaped}['\"][ \\t]*:`, 'gm');\n let m: RegExpExecArray | null;\n while ((m = keyRe.exec(blockMasked)) !== null) {\n const start = m.index;\n let i = m.index + m[0].length;\n let depth = 0;\n let inString: string | null = null;\n\n while (i < blockMasked.length) {\n const ch = blockMasked[i];\n if (inString) {\n if (ch === '\\\\') {\n i++;\n } else if (ch === inString) {\n inString = null;\n }\n } else if (ch === '\"' || ch === \"'\" || ch === '`') {\n inString = ch;\n } else if (ch === '(' || ch === '{' || ch === '[') {\n depth++;\n } else if (ch === ')' || ch === '}' || ch === ']') {\n depth--;\n } else if (depth === 0 && ch === ',') {\n i++;\n break;\n } else if (depth === 0 && (ch === '\\n' || ch === '}')) {\n break;\n }\n i++;\n }\n\n // Consume trailing newline\n if (i < blockMasked.length && blockMasked[i] === '\\n') i++;\n\n blockContent = blockContent.slice(0, start) + blockContent.slice(i);\n blockMasked = blockMasked.slice(0, start) + blockMasked.slice(i);\n keyRe.lastIndex = start;\n }\n }\n\n return content.slice(0, block.openBrace) + blockContent + content.slice(block.closeBrace);\n}\n\nexport interface SchemaSyncResult {\n /** Segments found in the filesystem. */\n filesystemSegments: string[];\n /** Segments already defined in app/schema.ts. */\n existingSegments: string[];\n /** Segments that were added. */\n addedSegments: string[];\n /** Segments that were removed (no longer in filesystem). */\n removedSegments: string[];\n /** Whether a new schema file was created. */\n created: boolean;\n}\n\n/**\n * Run the schema sync: scan filesystem, diff against schema, append missing.\n */\nexport function runSchemaSync(projectRoot: string): SchemaSyncResult {\n const appDir = resolveAppDir(projectRoot);\n\n const filesystemSegments = collectDynamicSegments(appDir);\n\n const schemaPath = findSchemaFile(appDir);\n\n if (filesystemSegments.length === 0 && !schemaPath) {\n return {\n filesystemSegments: [],\n existingSegments: [],\n addedSegments: [],\n removedSegments: [],\n created: false,\n };\n }\n\n if (!schemaPath) {\n // Create new schema file\n const newPath = join(appDir, 'schema.ts');\n writeFileSync(newPath, generateNewSchema(filesystemSegments), 'utf-8');\n return {\n filesystemSegments,\n existingSegments: [],\n addedSegments: filesystemSegments,\n removedSegments: [],\n created: true,\n };\n }\n\n // Read existing schema and diff\n const content = readFileSync(schemaPath, 'utf-8');\n const existingSegments = parseExistingSchemaKeys(content);\n const missing = filesystemSegments.filter((seg) => !existingSegments.includes(seg));\n const stale = existingSegments.filter((seg) => !filesystemSegments.includes(seg));\n\n if (missing.length === 0 && stale.length === 0) {\n return {\n filesystemSegments,\n existingSegments,\n addedSegments: [],\n removedSegments: [],\n created: false,\n };\n }\n\n let updated = content;\n if (stale.length > 0) {\n updated = removeFromSchema(updated, stale);\n }\n if (missing.length > 0) {\n updated = appendToSchema(updated, missing);\n }\n writeFileSync(schemaPath, updated, 'utf-8');\n\n return {\n filesystemSegments,\n existingSegments,\n addedSegments: missing,\n removedSegments: stale,\n created: false,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli-schema-sync-3Wutm8pH.js","names":[],"sources":["../../src/cli-schema-sync.ts"],"sourcesContent":["/**\n * `timber schema sync` — scan filesystem for dynamic segments, append\n * missing entries with `codec.string` defaults, and remove stale entries\n * whose segments no longer exist in the filesystem.\n *\n * Design doc: design/41-global-params.md §Auto-Scaffolding\n */\n\nimport { readFileSync, writeFileSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { scanRoutes } from './routing/scanner.ts';\nimport type { SegmentNode } from './routing/types.ts';\nimport { resolveAppDir } from './plugin-context.ts';\n\nfunction toBracketKey(node: SegmentNode): string {\n if (node.paramPrefix || node.paramSuffix) return `[${node.paramName}]`;\n switch (node.segmentType) {\n case 'catch-all':\n return `[...${node.paramName}]`;\n case 'optional-catch-all':\n return `[[...${node.paramName}]]`;\n default:\n return `[${node.paramName}]`;\n }\n}\n\n/**\n * Collect all unique bracket-keyed dynamic segment names from the route tree.\n * Returns entries like '[artistSlug]', '[...slug]', '[[...topic]]'.\n */\nexport function collectDynamicSegments(appDir: string): string[] {\n const tree = scanRoutes(appDir);\n const segments = new Set<string>();\n walkSegments(tree.root, segments);\n return [...segments].sort();\n}\n\nfunction walkSegments(node: SegmentNode, segments: Set<string>): void {\n if (node.paramName) {\n // Use bare bracket form for schema keys — affixed segments like\n // `img-[id].png` register as `[id]`, matching runtime codec lookup.\n segments.add(toBracketKey(node));\n }\n for (const child of node.children) {\n walkSegments(child, segments);\n }\n for (const slot of Object.values(node.slots)) {\n walkSegments(slot, segments);\n }\n}\n\n/**\n * Replace comment bodies (and optionally string contents) with spaces,\n * preserving length and newlines so every index maps 1:1 to the original.\n * Lets the structural scanning below ignore `segmentParams` mentions and\n * braces inside comments/strings without a TS parser (fine pre-alpha,\n * see TIM-1066).\n *\n * With `maskStrings`, string contents are blanked too — except literal\n * 'segmentParams' strings, kept so a quoted property key stays findable.\n */\nfunction maskSource(content: string, maskStrings: boolean): string {\n const out = content.split('');\n const n = content.length;\n let i = 0;\n\n const blank = (from: number, to: number): void => {\n for (let j = from; j < to; j++) {\n if (content[j] !== '\\n') out[j] = ' ';\n }\n };\n\n while (i < n) {\n const ch = content[i];\n const next = content[i + 1];\n if (ch === '/' && next === '/') {\n const start = i;\n while (i < n && content[i] !== '\\n') i++;\n blank(start, i);\n } else if (ch === '/' && next === '*') {\n const start = i;\n i += 2;\n while (i < n && !(content[i] === '*' && content[i + 1] === '/')) i++;\n i = Math.min(i + 2, n);\n blank(start, i);\n } else if (ch === '\"' || ch === \"'\" || ch === '`') {\n const start = i;\n i++;\n while (i < n && content[i] !== ch) {\n if (content[i] === '\\\\') i++;\n i++;\n }\n const end = i; // at closing quote (or n if unterminated)\n if (maskStrings && content.slice(start + 1, end) !== 'segmentParams') {\n blank(start + 1, end);\n }\n i++;\n } else {\n i++;\n }\n }\n return out.join('');\n}\n\n/**\n * Locate the `segmentParams: { ... }` block in a schema file.\n * Returns the indices of the opening and matching closing brace, or null\n * if no segmentParams property is found. Comments and strings are masked\n * first, so a JSDoc example mentioning `segmentParams: { ... }` above the\n * real defineSchema call is not mistaken for the block, and braces inside\n * comments/strings don't skew the depth count. The search is anchored to\n * the `defineSchema(` call when present, so helper objects with their own\n * segmentParams property earlier in the file are skipped.\n */\nexport function findSegmentParamsBlock(\n content: string\n): { openBrace: number; closeBrace: number } | null {\n const masked = maskSource(content, true);\n\n const defineCall = /\\bdefineSchema\\s*\\(/.exec(masked);\n const keyRe = /(['\"]?)segmentParams\\1\\s*:\\s*\\{/g;\n keyRe.lastIndex = defineCall ? defineCall.index + defineCall[0].length : 0;\n const keyMatch = keyRe.exec(masked);\n if (!keyMatch) return null;\n\n const openBrace = keyMatch.index + keyMatch[0].length - 1;\n let depth = 1;\n let i = openBrace + 1;\n while (i < masked.length && depth > 0) {\n if (masked[i] === '{') depth++;\n else if (masked[i] === '}') depth--;\n i++;\n }\n if (depth > 0) return null;\n // i now points to just after the closing brace\n return { openBrace, closeBrace: i - 1 };\n}\n\n/**\n * Parse existing bracket keys from an app/schema.ts file.\n * Scoped to the segmentParams: { ... } block — bracket keys elsewhere in\n * the file (unrelated objects, comments above defineSchema) are ignored.\n * Comments inside the block are masked, so a commented-out entry does not\n * count as registered.\n *\n * The key charset matches the route classifier (segment-classify.ts):\n * any character except `]`, so hyphenated params like '[my-param]' parse.\n */\nexport function parseExistingSchemaKeys(content: string): string[] {\n const block = findSegmentParamsBlock(content);\n if (!block) return [];\n\n const blockContent = maskSource(content, false).slice(block.openBrace, block.closeBrace);\n const keys: string[] = [];\n // Match quoted bracket keys like '[artistSlug]', '[...slug]', '[[...topic]]'\n const re = /['\"](\\[{1,2}(?:\\.{3})?[^\\]]+\\]{1,2})['\"]\\s*:/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(blockContent)) !== null) {\n keys.push(m[1]);\n }\n return keys;\n}\n\n/** Schema file extensions to check. */\nconst SCHEMA_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];\n\n/**\n * Find the schema file path if it exists.\n */\nexport function findSchemaFile(appDir: string): string | undefined {\n for (const ext of SCHEMA_EXTENSIONS) {\n const p = join(appDir, `schema${ext}`);\n if (existsSync(p)) return p;\n }\n return undefined;\n}\n\nexport function defaultCodecForSegment(seg: string): string {\n if (seg.startsWith('[[...')) return 'codec.optionalCatchAll(codec.string)';\n if (seg.startsWith('[...')) return 'codec.stringArray';\n return 'codec.string';\n}\n\n/**\n * Generate the content for a new app/schema.ts with the given segment keys.\n */\nfunction generateNewSchema(segments: string[]): string {\n const lines: string[] = [];\n lines.push(\"import { defineSchema } from '@timber-js/app/params';\");\n lines.push(\"import { codec } from '@timber-js/app/codec';\");\n lines.push('');\n lines.push('export default defineSchema({');\n lines.push(' segmentParams: {');\n for (const seg of segments) {\n lines.push(` '${seg}': ${defaultCodecForSegment(seg)},`);\n }\n lines.push(' },');\n lines.push('});');\n lines.push('');\n return lines.join('\\n');\n}\n\n/**\n * Append missing segment entries to an existing schema file.\n * Inserts new entries before the closing `}` of segmentParams.\n */\nconst CODEC_IMPORT = \"import { codec } from '@timber-js/app/codec';\";\n\nfunction hasCodecImport(content: string): boolean {\n return /import\\s+\\{[^}]*\\bcodec\\b[^}]*\\}\\s+from\\s+['\"]@timber-js\\/app\\/codec['\"]/.test(content);\n}\n\nfunction ensureCodecImport(content: string): string {\n if (hasCodecImport(content)) return content;\n\n // Insert after the last existing import statement\n const lines = content.split('\\n');\n let lastImportIndex = -1;\n for (let i = 0; i < lines.length; i++) {\n if (/^\\s*import\\s/.test(lines[i])) lastImportIndex = i;\n }\n\n if (lastImportIndex === -1) {\n return CODEC_IMPORT + '\\n' + content;\n }\n\n lines.splice(lastImportIndex + 1, 0, CODEC_IMPORT);\n return lines.join('\\n');\n}\n\nfunction appendToSchema(content: string, newSegments: string[]): string {\n content = ensureCodecImport(content);\n\n const block = findSegmentParamsBlock(content);\n if (!block) {\n throw new Error(\n '[timber] Could not find segmentParams in schema file. ' +\n 'Expected a defineSchema({ segmentParams: { ... } }) call.'\n );\n }\n const { openBrace, closeBrace } = block;\n\n // Detect indentation from existing entries\n const existingLine = content.slice(openBrace, closeBrace).match(/^(\\s+)['\"]\\[/m);\n const indent = existingLine ? existingLine[1] : ' ';\n\n // Build insertion text\n const insertion = newSegments\n .map((seg) => `${indent}'${seg}': ${defaultCodecForSegment(seg)},`)\n .join('\\n');\n\n // Insert before the closing brace\n const before = content.slice(0, closeBrace);\n const after = content.slice(closeBrace);\n\n // Add newline if the content before doesn't end with one\n const separator = before.trimEnd().endsWith(',') ? '\\n' : '\\n';\n\n return before + separator + insertion + '\\n' + after;\n}\n\n/**\n * Remove entries from the segmentParams block whose keys match `staleSegments`.\n * Removes the entire entry including trailing comma/whitespace. Scoped to\n * the segmentParams block — identical keys in unrelated objects elsewhere\n * in the file are left untouched.\n */\nfunction removeFromSchema(content: string, staleSegments: string[]): string {\n const block = findSegmentParamsBlock(content);\n if (!block) return content;\n\n let blockContent = content.slice(block.openBrace, block.closeBrace);\n // Key matching and the entry walk run on a comment-masked copy (same\n // length, indices align) so commented-out entries are never removed.\n // Both copies are spliced in sync to keep indices valid.\n let blockMasked = maskSource(blockContent, false);\n\n for (const seg of staleSegments) {\n const escaped = seg.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const keyRe = new RegExp(`^[ \\t]*['\"]${escaped}['\"][ \\t]*:`, 'gm');\n let m: RegExpExecArray | null;\n while ((m = keyRe.exec(blockMasked)) !== null) {\n const start = m.index;\n let i = m.index + m[0].length;\n let depth = 0;\n let inString: string | null = null;\n\n while (i < blockMasked.length) {\n const ch = blockMasked[i];\n if (inString) {\n if (ch === '\\\\') {\n i++;\n } else if (ch === inString) {\n inString = null;\n }\n } else if (ch === '\"' || ch === \"'\" || ch === '`') {\n inString = ch;\n } else if (ch === '(' || ch === '{' || ch === '[') {\n depth++;\n } else if (ch === ')' || ch === '}' || ch === ']') {\n depth--;\n } else if (depth === 0 && ch === ',') {\n i++;\n break;\n } else if (depth === 0 && (ch === '\\n' || ch === '}')) {\n break;\n }\n i++;\n }\n\n // Consume trailing newline\n if (i < blockMasked.length && blockMasked[i] === '\\n') i++;\n\n blockContent = blockContent.slice(0, start) + blockContent.slice(i);\n blockMasked = blockMasked.slice(0, start) + blockMasked.slice(i);\n keyRe.lastIndex = start;\n }\n }\n\n return content.slice(0, block.openBrace) + blockContent + content.slice(block.closeBrace);\n}\n\nexport interface SchemaSyncResult {\n /** Segments found in the filesystem. */\n filesystemSegments: string[];\n /** Segments already defined in app/schema.ts. */\n existingSegments: string[];\n /** Segments that were added. */\n addedSegments: string[];\n /** Segments that were removed (no longer in filesystem). */\n removedSegments: string[];\n /** Whether a new schema file was created. */\n created: boolean;\n}\n\n/**\n * Run the schema sync: scan filesystem, diff against schema, append missing.\n */\nexport function runSchemaSync(projectRoot: string): SchemaSyncResult {\n const appDir = resolveAppDir(projectRoot);\n\n const filesystemSegments = collectDynamicSegments(appDir);\n\n const schemaPath = findSchemaFile(appDir);\n\n if (filesystemSegments.length === 0 && !schemaPath) {\n return {\n filesystemSegments: [],\n existingSegments: [],\n addedSegments: [],\n removedSegments: [],\n created: false,\n };\n }\n\n if (!schemaPath) {\n // Create new schema file\n const newPath = join(appDir, 'schema.ts');\n writeFileSync(newPath, generateNewSchema(filesystemSegments), 'utf-8');\n return {\n filesystemSegments,\n existingSegments: [],\n addedSegments: filesystemSegments,\n removedSegments: [],\n created: true,\n };\n }\n\n // Read existing schema and diff\n const content = readFileSync(schemaPath, 'utf-8');\n const existingSegments = parseExistingSchemaKeys(content);\n const missing = filesystemSegments.filter((seg) => !existingSegments.includes(seg));\n const stale = existingSegments.filter((seg) => !filesystemSegments.includes(seg));\n\n if (missing.length === 0 && stale.length === 0) {\n return {\n filesystemSegments,\n existingSegments,\n addedSegments: [],\n removedSegments: [],\n created: false,\n };\n }\n\n let updated = content;\n if (stale.length > 0) {\n updated = removeFromSchema(updated, stale);\n }\n if (missing.length > 0) {\n updated = appendToSchema(updated, missing);\n }\n writeFileSync(schemaPath, updated, 'utf-8');\n\n return {\n filesystemSegments,\n existingSegments,\n addedSegments: missing,\n removedSegments: stale,\n created: false,\n };\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { c as _setUnloading, m as unloading, n as getSsrData } from "./ssr-data-14MXm7Pj.js";
2
- import { n as getRouterOrNull } from "./router-ref-DuYuV_0Q.js";
1
+ import { c as _setUnloading, m as unloading, n as getSsrData } from "./ssr-data-Ya2HJPFp.js";
2
+ import { n as getRouterOrNull } from "./router-ref-BzqbPwYC.js";
3
3
  import { Component, createElement } from "react";
4
4
  //#region src/client/unload-guard.ts
5
5
  /**
@@ -350,4 +350,4 @@ function statusMatches(boundaryStatus, errorStatus) {
350
350
  //#endregion
351
351
  export { markStaleFromError as i, isClientStale as n, markClientStale as r, TimberErrorBoundary as t };
352
352
 
353
- //# sourceMappingURL=error-boundary-D-ODYX41.js.map
353
+ //# sourceMappingURL=error-boundary-D-lkwyaD.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"error-boundary-D-ODYX41.js","names":[],"sources":["../../src/client/unload-guard.ts","../../src/client/stale-client.ts","../../src/client/error-boundary.tsx"],"sourcesContent":["/**\n * Page unload detection — suppresses spurious errors during page refresh/navigation.\n *\n * When the user refreshes the page or navigates away while React is still\n * streaming Suspense content, the aborted connection causes streaming errors.\n * These are not application errors — they're a side effect of the browser\n * tearing down the connection. This module tracks whether the page is being\n * unloaded so error boundaries and error handlers can suppress abort-related\n * errors during the unload window.\n *\n * This is the single owner of \"is this document going away\". Anything that\n * needs the answer imports `isPageUnloading()` — an unload flag maintained a\n * second time somewhere else is a flag that will drift out of agreement with\n * this one (TIM-1260).\n *\n * Mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n *\n * See design/10-error-handling.md §\"Known limitation: deny() inside Suspense and hydration\"\n */\n\nimport { unloading, _setUnloading } from './state.ts';\n\n/**\n * Departure is *committed*: `pagehide` has fired, or this document has called\n * reload() on itself. `beforeunload` alone doesn't qualify — it is\n * speculative, and the user can still cancel out of it.\n */\nlet committed = false;\n\nif (typeof window !== 'undefined') {\n window.addEventListener('beforeunload', () => {\n _setUnloading(true);\n\n // An app with an unsaved-changes prompt can have this cancelled. Nothing\n // fires to say so, so the only signal is that the page is still running\n // afterwards — and timers are blocked while the modal is up, so this task\n // cannot run until the user has chosen. If they left, `pagehide` has\n // already committed and this is a no-op.\n setTimeout(() => {\n if (!committed) _setUnloading(false);\n }, 0);\n });\n\n // pagehide is the committed-departure event, and fires for both navigations\n // and bfcache entry. Safari in particular may skip beforeunload entirely.\n window.addEventListener('pagehide', () => {\n committed = true;\n _setUnloading(true);\n });\n\n window.addEventListener('pageshow', (event) => {\n // A bfcache restore un-does the teardown `pagehide` announced: the same\n // document is alive again, with this module's state intact. Without the\n // reset, every consumer would go on suppressing errors on a live page.\n if (event.persisted) {\n committed = false;\n _setUnloading(false);\n }\n });\n}\n\n/**\n * Returns true if the page is currently being unloaded (user refreshed\n * or navigated away). Error boundaries should suppress errors in this state.\n */\nexport function isPageUnloading(): boolean {\n return unloading;\n}\n","/**\n * Stale Client — knowing when this bundle has been superseded.\n *\n * Layer 1 of version skew handling (see design/33-version-skew.md) is\n * proactive: the server compares `X-Timber-Deployment-Id` against the current\n * build and answers a stale client with `X-Timber-Reload: 1`. That covers\n * every RSC navigation and every server action — as long as there is a server\n * in the request path.\n *\n * This module is the reactive half, plus the state both halves feed. It\n * recognizes the two error shapes a stale client produces on its own:\n *\n * 1. **Stale client references** — React's Flight client throws\n * \"Could not find the module …\" when an RSC payload names a client module\n * ID that isn't in this bundle's manifest.\n * 2. **Chunk load failures** — a content-hashed chunk that no longer exists on\n * the CDN fails to `import()`. Every engine words this differently.\n *\n * It matters most for `output: 'static'`, where there is no server to send the\n * reload header and this is the *only* skew detection that can run.\n *\n * **Nothing here reloads the page.** A stale client is recorded, not acted on;\n * the router turns the next user-initiated navigation into a full document\n * load. See design/33-version-skew.md §\"Recovery on the next navigation\" for\n * why — a self-issued `location.reload()` cannot tell a recovery from a bounce\n * between two live deployments, and mid-rollout it produces exactly that loop.\n *\n * See TIM-1260, TIM-1275.\n */\n\n// The only import: the shared unload state. Kept minimal because this module\n// is reached from the error boundary, a `'use client'` component evaluated in\n// the SSR graph too — a dependency on the client fetch layer would drag that\n// graph along with it.\nimport { isPageUnloading } from './unload-guard.ts';\n\n// ─── Error Shapes ────────────────────────────────────────────────\n\n/**\n * Substrings that identify a failed dynamic import, lowercased for\n * case-insensitive matching. Engines and bundlers each word this\n * differently and none of them expose a stable error code.\n */\nconst CHUNK_LOAD_PATTERNS = [\n // Chromium\n 'failed to fetch dynamically imported module',\n // Firefox\n 'error loading dynamically imported module',\n // Safari\n 'importing a module script failed',\n // Vite's preload helper, for the stylesheet of a lazily-imported chunk\n 'unable to preload css for',\n];\n\n/** Substring React's Flight client uses when a client module ID is unknown. */\nconst STALE_REFERENCE_PATTERN = 'could not find the module';\n\n/**\n * Pull a comparable message out of an unknown thrown value. Rejections cross\n * enough boundaries (event handlers, `unhandledrejection`, Vite's preload\n * event) that the value is not reliably an `Error`.\n */\nfunction messageOf(error: unknown): string {\n if (typeof error === 'string') return error.toLowerCase();\n if (error instanceof Error) return error.message.toLowerCase();\n if (error && typeof error === 'object' && 'message' in error) {\n const message = (error as { message: unknown }).message;\n if (typeof message === 'string') return message.toLowerCase();\n }\n return '';\n}\n\n/**\n * True when the error is React's \"client reference not in this manifest\"\n * failure — the payload was produced by a build this bundle doesn't match.\n */\nexport function isStaleClientReference(error: unknown): boolean {\n return messageOf(error).includes(STALE_REFERENCE_PATTERN);\n}\n\n/**\n * True when the error is a failed dynamic import or CSS preload — the\n * content-hashed chunk this bundle asks for is gone from the CDN.\n */\nexport function isChunkLoadError(error: unknown): boolean {\n if (error instanceof Error && error.name === 'ChunkLoadError') return true;\n const message = messageOf(error);\n return CHUNK_LOAD_PATTERNS.some((pattern) => message.includes(pattern));\n}\n\n// ─── Staleness ───────────────────────────────────────────────────\n\n/**\n * True once this client is known to be running against a superseded\n * deployment. One-way: a document that has gone stale cannot come back, since\n * the only way to get fresh code is to load a new document.\n *\n * In-memory on purpose. Persisting it would be wrong — the next document gets\n * whatever build the server hands it, and if that one is stale too it will\n * find out the same way.\n */\nlet stale = false;\n\n/**\n * True once {@link initStaleClient} has seen a deployment ID — i.e. this is a\n * production build. False in dev, where nothing is ever stale: HMR owns code\n * updates, and a failed dynamic import is a real error the developer needs to\n * see rather than something a page load fixes.\n */\nlet deployed = false;\n\n/** Whether this client has been superseded by a newer deployment. */\nexport function isClientStale(): boolean {\n return stale;\n}\n\n/**\n * Record that the origin has told us this client is stale — the `X-Timber-Reload`\n * header on an RSC or action response. Authoritative, so it is not gated on\n * anything: a dev server never sends the header in the first place.\n */\nexport function markClientStale(): void {\n stale = true;\n}\n\n/**\n * Record staleness if `error` is one of the shapes a superseded bundle\n * produces. Returns whether it did, so a caller mid-navigation can decide to\n * hard-navigate instead of surfacing the error.\n *\n * Inference, unlike {@link markClientStale}, so it is gated on being a\n * production build. This is the single place that gate lives — call sites\n * cannot see each other, and \"remember to gate the next one too\" is not a\n * guarantee.\n */\nexport function markStaleFromError(error: unknown): boolean {\n if (!deployed) return false;\n\n // A document being torn down aborts its in-flight dynamic imports, and an\n // aborted import reads exactly like a chunk the CDN has dropped. That is not\n // evidence of staleness, and the bit outlives the teardown that produced it:\n // a document entering the back-forward cache is frozen, not destroyed, so a\n // false positive recorded on the way out would cost the *restored* page its\n // client-side navigation for good. `unload-guard.ts` clears its own flag on\n // a persisted `pageshow`; this one is one-way by design, so it has to not be\n // set in the first place.\n if (isPageUnloading()) return false;\n\n if (!isStaleClientReference(error) && !isChunkLoadError(error)) return false;\n stale = true;\n return true;\n}\n\n// ─── Initialization ──────────────────────────────────────────────\n\n/**\n * The underlying error Vite attaches to a `vite:preloadError` event.\n *\n * Narrowed at runtime rather than cast: the property is Vite's, not ours, and\n * a shape change should fall back to matching the event itself rather than\n * silently reading `undefined` forever.\n */\nfunction preloadErrorPayload(event: Event): unknown {\n return 'payload' in event ? (event as Event & { payload: unknown }).payload : event;\n}\n\n/**\n * Arm the module, and register the listeners that catch stale-client errors\n * never passing through the router: a lazily imported client component, a\n * preloaded stylesheet, anything that rejects outside a navigation.\n *\n * No-op when `deploymentId` is null — that is dev. Called from the browser\n * entry before hydration, so it precedes any render or navigation that could\n * throw.\n */\nexport function initStaleClient(deploymentId: string | null): void {\n if (!deploymentId) return;\n deployed = true;\n\n // Vite dispatches this for failed chunk/CSS preloads. Note what is *not*\n // here: `preventDefault()`. Recording staleness is not recovering from this\n // error — the page is still broken until the user navigates — so Vite must\n // go on rethrowing it rather than have it silently swallowed.\n window.addEventListener('vite:preloadError', (event) => {\n markStaleFromError(preloadErrorPayload(event));\n });\n\n window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => {\n markStaleFromError(event.reason);\n });\n}\n","'use client';\n\n/**\n * Framework-injected React error boundary.\n *\n * Catches errors thrown by children and renders a fallback component\n * with the appropriate props based on error type:\n * - DenySignal (4xx) → { status, dangerouslyPassData }\n * - RenderError (5xx) → { error, digest, reset }\n * - Unhandled error → { error, digest: null, reset }\n *\n * The `status` prop controls which errors this boundary catches:\n * - Specific code (e.g. 403) → only that status\n * - Category (400) → any 4xx\n * - Category (500) → any 5xx\n * - Omitted → catches everything (error.tsx behavior)\n *\n * See design/10-error-handling.md §\"Status-Code Files\"\n */\n\nimport { Component, createElement, type ReactNode } from 'react';\nimport { getSsrData } from './ssr-data.ts';\nimport { getRouterOrNull } from './router-ref.ts';\nimport { markStaleFromError } from './stale-client.ts';\n// Page unload detection: when the page is being torn down, error boundaries\n// suppress activation — the error is from the aborted connection, not the\n// application. unload-guard.ts is the single owner of that state; this module\n// used to keep its own copy, which drifted (TIM-1260).\nimport { isPageUnloading } from './unload-guard.ts';\n\n// ─── Digest Types ────────────────────────────────────────────────────────────\n\n/** Structured digest returned by RSC onError for DenySignal. */\ninterface DenyDigest {\n type: 'deny';\n status: number;\n data: unknown;\n /**\n * Segment key of the segment that owns the matched deny page, present when\n * an in-tree catch site matched a file it may not render at its own\n * position and re-threw to reach the owner. See TIM-1356.\n */\n ownerKey?: string;\n}\n\n/** Structured digest returned by RSC onError for RenderError. */\ninterface RenderErrorDigest {\n type: 'render-error';\n code: string;\n data: unknown;\n status: number;\n}\n\n/** Structured digest returned by RSC onError for RedirectSignal. */\ninterface RedirectDigest {\n type: 'redirect';\n location: string;\n status: number;\n}\n\ntype ParsedDigest = DenyDigest | RenderErrorDigest | RedirectDigest;\n\n// ─── Props & State ───────────────────────────────────────────────────────────\n\nexport interface TimberErrorBoundaryProps {\n /** The component to render when an error is caught. */\n fallbackComponent?: (...args: unknown[]) => ReactNode;\n /**\n * Pre-rendered fallback element. Used for MDX status files which are server\n * components and cannot be passed as function props across the RSC→client\n * boundary. When set, rendered directly instead of calling fallbackComponent.\n *\n * See design/10-error-handling.md §\"Status-Code File Variants\" — MDX status\n * files are server components by default (zero client JS).\n */\n fallbackElement?: ReactNode;\n /**\n * Status code filter. If set, only catches errors matching this status.\n * 400 = any 4xx, 500 = any 5xx, specific number = exact match.\n */\n status?: number;\n /**\n * When true, this boundary wraps a parallel slot. Slot denials are\n * graceful degradation — they must NOT change the HTTP status code.\n */\n isSlotBoundary?: boolean;\n /**\n * Segment key of the segment that declared this boundary. Only meaningful\n * against a deny digest carrying an `ownerKey`. See the decline rule in\n * `render()` and TIM-1356.\n */\n segmentKey?: string;\n children: ReactNode;\n}\n\ninterface TimberErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n}\n\n// Module-level guard: the first boundary to catch a given redirect digest\n// schedules the navigation and marks it handled here. Subsequent renders\n// (React retries the error-boundary render path multiple times in dev, and\n// outer boundaries that also see the error) become no-ops, preventing\n// duplicate router.navigate() calls.\nconst _handledRedirects = new WeakSet<object>();\n\n// Companion guard keyed by location: client-render redirect() (the\n// next/navigation shim) throws a FRESH error instance on every render\n// attempt, and React replays the errored render — so the WeakSet alone\n// would schedule the navigation twice. While a navigation to a location\n// is pending, further redirect errors for the same location are no-ops.\nlet _pendingRedirectLocation: string | null = null;\n\n// ─── Component ───────────────────────────────────────────────────────────────\n\nexport class TimberErrorBoundary extends Component<\n TimberErrorBoundaryProps,\n TimberErrorBoundaryState\n> {\n constructor(props: TimberErrorBoundaryProps) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n\n static getDerivedStateFromError(error: Error): TimberErrorBoundaryState {\n // Suppress error boundaries during page unload (refresh/navigate away).\n // The aborted connection causes React's streaming hydration to error,\n // but the page is about to be replaced — showing an error boundary\n // would be a jarring flash for the user.\n if (isPageUnloading()) {\n return { hasError: false, error: null };\n }\n\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error): void {\n // A stale bundle most often surfaces here rather than as a rejected\n // navigation: a client reference missing from this build's manifest, or\n // a lazily-imported chunk the CDN no longer has, thrown during render.\n //\n // Recording it is all that happens — this boundary still renders its\n // fallback, and the recovery is the user's next navigation, which the\n // router turns into a full document load. Reloading from here would be a\n // page load the user did not ask for, on a page that may be mid-rollout\n // and would come back just as stale (TIM-1275).\n markStaleFromError(error);\n }\n\n componentDidUpdate(prevProps: TimberErrorBoundaryProps): void {\n // Reset error state when children change (e.g. client-side navigation).\n // Without this, navigating from one error page to another keeps the\n // stale error — getDerivedStateFromError doesn't re-fire for new children.\n if (this.state.hasError && prevProps.children !== this.props.children) {\n this.setState({ hasError: false, error: null });\n }\n }\n\n /** Reset the error state so children re-render. */\n private reset = () => {\n this.setState({ hasError: false, error: null });\n };\n\n render(): ReactNode {\n if (!this.state.hasError || !this.state.error) {\n return this.props.children;\n }\n\n const error = this.state.error;\n const parsed = parseDigest(error);\n\n // RedirectSignal handling splits by environment:\n //\n // - SSR (no window): re-throw so the Fizz shell fails and the server\n // pipeline catch block can produce a proper HTTP 3xx response. See\n // design/04-authorization.md.\n //\n // - Client (window exists): a RedirectSignal that leaked into the RSC\n // Flight payload during client navigation would otherwise bubble past\n // every error boundary to `window.onerror`, which in dev triggers the\n // Vite error overlay (false positive — navigation itself works).\n // Instead, ask the router to perform an SPA navigation to the target\n // and render nothing while it takes over. The router's own catch path\n // handles cross-origin / hard navigations. See TIM-838.\n if (parsed?.type === 'redirect') {\n if (typeof window === 'undefined') {\n throw error;\n }\n // Dedupe: React may render the error-boundary fallback path more\n // than once for a single caught error (dev-mode double render, and\n // additional boundaries above us in the tree). Only schedule the\n // navigation on the first render that sees this error instance.\n const alreadyHandled =\n _handledRedirects.has(error) || _pendingRedirectLocation === parsed.location;\n if (!alreadyHandled) {\n _handledRedirects.add(error);\n const router = getRouterOrNull();\n if (router) {\n _pendingRedirectLocation = parsed.location;\n // Schedule the navigation outside the render phase to avoid\n // setState-during-render warnings from router state updates.\n queueMicrotask(() => {\n router\n .navigate(parsed.location, { replace: true })\n .catch(() => {\n // Fall back to a hard navigation if the soft navigation fails.\n window.location.href = parsed.location;\n })\n .finally(() => {\n _pendingRedirectLocation = null;\n });\n });\n } else {\n // No router available (shouldn't happen post-bootstrap) — fall\n // back to a hard navigation so the user still ends up at the\n // target.\n window.location.href = parsed.location;\n }\n }\n return null;\n }\n\n // Owner-addressed denies: an in-tree catch site already resolved the\n // fallback chain and matched a file it may not render at its own\n // position, so it re-threw to reach the segment that owns that file.\n // Every boundary but that one declines — otherwise the nearest boundary\n // whose status filter happens to match catches first and renders the\n // deny page at its own depth, wrapped in the layouts the hoist existed\n // to skip, and from a different file than the chain chose. (Boundary\n // nesting is per-segment `{status} → category → error`; the 4xx chain\n // runs status files across ALL segments before any error.tsx, so a\n // leaf error.tsx would otherwise beat a root 404.tsx.)\n //\n // Declining is the default: an unkeyed boundary — a slot boundary, or\n // one from a construction site that builds no deny chains — never\n // claims a deny addressed to a segment. See TIM-1356.\n // `typeof === 'string'`, not a null check: the digest is untrusted JSON —\n // `parseDigest` validates only `type`, and any component can set\n // `error.digest` itself. A non-string `ownerKey` is unequal to every\n // boundary's key, so a truthiness test would let one error decline its\n // way past the entire tree and blank the page, where it would otherwise\n // have hit the nearest matching boundary.\n if (\n parsed?.type === 'deny' &&\n typeof parsed.ownerKey === 'string' &&\n parsed.ownerKey !== this.props.segmentKey\n ) {\n throw error;\n }\n\n // If this boundary has a status filter, check whether the error matches.\n // Non-matching errors re-throw so an outer boundary can catch them.\n if (this.props.status != null) {\n const errorStatus = getErrorStatus(parsed, error);\n if (errorStatus == null || !statusMatches(this.props.status, errorStatus)) {\n // Re-throw: this boundary doesn't handle this error.\n throw error;\n }\n }\n\n // Report DenySignal handling so the pipeline skips the redundant\n // renderDenyPage() re-render — but ONLY when this boundary can fully\n // handle the deny in-tree:\n //\n // ✅ TSX boundaries (fallbackComponent) — can receive runtime props\n // (status, dangerouslyPassData) dynamically in render()\n // ❌ MDX boundaries (fallbackElement) — pre-rendered at tree-build time,\n // cannot receive runtime deny props. Must fall through to re-render.\n //\n // For qualifying segment boundaries: also set the HTTP status code from\n // the DenySignal so the Response has the correct 4xx status. This runs\n // synchronously during Fizz rendering, BEFORE onShellReady.\n //\n // For slot boundaries: set _denyHandledByBoundary but do NOT change\n // statusCode — slot denials are graceful degradation.\n //\n // See TIM-664, LOCAL-298.\n if (parsed?.type === 'deny') {\n const canHandleInTree = this.props.fallbackElement == null;\n\n if (canHandleInTree || this.props.isSlotBoundary) {\n const ssrData = getSsrData();\n if (ssrData?._navContext) {\n ssrData._navContext._denyHandledByBoundary = true;\n if (!this.props.isSlotBoundary) {\n ssrData._navContext.statusCode = parsed.status;\n }\n }\n }\n }\n\n // Pre-rendered fallback element (MDX status files) — render directly.\n // MDX components are server components that cannot be passed as function\n // props across the RSC→client boundary. Instead, they are pre-rendered\n // as elements in the RSC environment and passed here as fallbackElement.\n if (this.props.fallbackElement != null) {\n return this.props.fallbackElement;\n }\n\n // Render the fallback component with the right props shape.\n if (parsed?.type === 'deny') {\n // Status-code file boundaries (404.tsx, 4xx.tsx, …) get the documented\n // 4xx props. error.tsx boundaries (no status filter) get their\n // documented { error, digest, reset } contract — plus the deny extras\n // for dual-shape implementations. Without the error prop, an error.tsx\n // reading error.message crashes on deny fall-through. See TIM-1081.\n if (this.props.status != null) {\n return createElement(this.props.fallbackComponent as never, {\n status: parsed.status,\n dangerouslyPassData: parsed.data,\n });\n }\n return createElement(this.props.fallbackComponent as never, {\n error: denyError(parsed.status, error),\n digest: null,\n reset: this.reset,\n status: parsed.status,\n dangerouslyPassData: parsed.data,\n });\n }\n\n // 5xx / RenderError / unhandled error\n const digest =\n parsed?.type === 'render-error' ? { code: parsed.code, data: parsed.data } : null;\n\n return createElement(this.props.fallbackComponent as never, {\n error,\n digest,\n reset: this.reset,\n });\n }\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\n/**\n * Parse the structured digest from the error.\n * React sets `error.digest` from the string returned by RSC's onError.\n */\nfunction parseDigest(error: Error): ParsedDigest | null {\n const raw = (error as { digest?: string }).digest;\n if (typeof raw !== 'string') return null;\n try {\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === 'object' && typeof parsed.type === 'string') {\n return parsed as ParsedDigest;\n }\n } catch (e) {\n console.debug('[timber] error digest is not valid JSON:', e instanceof Error ? e.message : e);\n }\n return null;\n}\n\n/**\n * The Error an `error.tsx` serving a deny should receive.\n *\n * The error that actually arrives here is React's, not the framework's: a\n * server-thrown error crossing Flight has its message and name replaced with\n * a generic redacted string in production, by design. An `error.tsx` written\n * per the docs branches on `error.message` — so handing it the redacted error\n * gives it nothing to branch on, while the in-tree path hands the same file a\n * `DenySignal` named error reading `Access denied with status <N>` (see\n * `renderDenyEntry`). This reconstructs that shape from the digest, which is\n * where the status already is.\n *\n * Derived from the status alone — no stack, no server data — so it is safe on\n * the client for the same reason the digest is. See design/13-security.md\n * §\"Errors don't leak\", TIM-1081.\n */\nfunction denyError(status: number, original: Error): Error {\n // A dev build may have preserved something more useful than the redaction.\n if (original.name === 'DenySignal') return original;\n const error = new Error(`Access denied with status ${status}`);\n error.name = 'DenySignal';\n return error;\n}\n\n/**\n * Extract the HTTP status code from a parsed digest or error message.\n * Falls back to message pattern matching for errors without a digest.\n */\nfunction getErrorStatus(parsed: ParsedDigest | null, error: Error): number | null {\n if (parsed?.type === 'deny') return parsed.status;\n if (parsed?.type === 'render-error') return parsed.status;\n if (parsed?.type === 'redirect') return parsed.status;\n\n // Fallback: parse DenySignal message pattern for errors that lost their digest\n const match = error.message.match(/^Access denied with status (\\d+)$/);\n if (match) return parseInt(match[1], 10);\n\n // Unhandled errors are implicitly 500\n return 500;\n}\n\n/**\n * Check whether an error's status matches the boundary's status filter.\n * Category markers (400, 500) match any status in that range.\n */\nfunction statusMatches(boundaryStatus: number, errorStatus: number): boolean {\n // Category catch-all: 400 matches any 4xx, 500 matches any 5xx\n if (boundaryStatus === 400) return errorStatus >= 400 && errorStatus <= 499;\n if (boundaryStatus === 500) return errorStatus >= 500 && errorStatus <= 599;\n // Exact match\n return boundaryStatus === errorStatus;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"error-boundary-D-lkwyaD.js","names":[],"sources":["../../src/client/unload-guard.ts","../../src/client/stale-client.ts","../../src/client/error-boundary.tsx"],"sourcesContent":["/**\n * Page unload detection — suppresses spurious errors during page refresh/navigation.\n *\n * When the user refreshes the page or navigates away while React is still\n * streaming Suspense content, the aborted connection causes streaming errors.\n * These are not application errors — they're a side effect of the browser\n * tearing down the connection. This module tracks whether the page is being\n * unloaded so error boundaries and error handlers can suppress abort-related\n * errors during the unload window.\n *\n * This is the single owner of \"is this document going away\". Anything that\n * needs the answer imports `isPageUnloading()` — an unload flag maintained a\n * second time somewhere else is a flag that will drift out of agreement with\n * this one (TIM-1260).\n *\n * Mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n *\n * See design/10-error-handling.md §\"Known limitation: deny() inside Suspense and hydration\"\n */\n\nimport { unloading, _setUnloading } from './state.ts';\n\n/**\n * Departure is *committed*: `pagehide` has fired, or this document has called\n * reload() on itself. `beforeunload` alone doesn't qualify — it is\n * speculative, and the user can still cancel out of it.\n */\nlet committed = false;\n\nif (typeof window !== 'undefined') {\n window.addEventListener('beforeunload', () => {\n _setUnloading(true);\n\n // An app with an unsaved-changes prompt can have this cancelled. Nothing\n // fires to say so, so the only signal is that the page is still running\n // afterwards — and timers are blocked while the modal is up, so this task\n // cannot run until the user has chosen. If they left, `pagehide` has\n // already committed and this is a no-op.\n setTimeout(() => {\n if (!committed) _setUnloading(false);\n }, 0);\n });\n\n // pagehide is the committed-departure event, and fires for both navigations\n // and bfcache entry. Safari in particular may skip beforeunload entirely.\n window.addEventListener('pagehide', () => {\n committed = true;\n _setUnloading(true);\n });\n\n window.addEventListener('pageshow', (event) => {\n // A bfcache restore un-does the teardown `pagehide` announced: the same\n // document is alive again, with this module's state intact. Without the\n // reset, every consumer would go on suppressing errors on a live page.\n if (event.persisted) {\n committed = false;\n _setUnloading(false);\n }\n });\n}\n\n/**\n * Returns true if the page is currently being unloaded (user refreshed\n * or navigated away). Error boundaries should suppress errors in this state.\n */\nexport function isPageUnloading(): boolean {\n return unloading;\n}\n","/**\n * Stale Client — knowing when this bundle has been superseded.\n *\n * Layer 1 of version skew handling (see design/33-version-skew.md) is\n * proactive: the server compares `X-Timber-Deployment-Id` against the current\n * build and answers a stale client with `X-Timber-Reload: 1`. That covers\n * every RSC navigation and every server action — as long as there is a server\n * in the request path.\n *\n * This module is the reactive half, plus the state both halves feed. It\n * recognizes the two error shapes a stale client produces on its own:\n *\n * 1. **Stale client references** — React's Flight client throws\n * \"Could not find the module …\" when an RSC payload names a client module\n * ID that isn't in this bundle's manifest.\n * 2. **Chunk load failures** — a content-hashed chunk that no longer exists on\n * the CDN fails to `import()`. Every engine words this differently.\n *\n * It matters most for `output: 'static'`, where there is no server to send the\n * reload header and this is the *only* skew detection that can run.\n *\n * **Nothing here reloads the page.** A stale client is recorded, not acted on;\n * the router turns the next user-initiated navigation into a full document\n * load. See design/33-version-skew.md §\"Recovery on the next navigation\" for\n * why — a self-issued `location.reload()` cannot tell a recovery from a bounce\n * between two live deployments, and mid-rollout it produces exactly that loop.\n *\n * See TIM-1260, TIM-1275.\n */\n\n// The only import: the shared unload state. Kept minimal because this module\n// is reached from the error boundary, a `'use client'` component evaluated in\n// the SSR graph too — a dependency on the client fetch layer would drag that\n// graph along with it.\nimport { isPageUnloading } from './unload-guard.ts';\n\n// ─── Error Shapes ────────────────────────────────────────────────\n\n/**\n * Substrings that identify a failed dynamic import, lowercased for\n * case-insensitive matching. Engines and bundlers each word this\n * differently and none of them expose a stable error code.\n */\nconst CHUNK_LOAD_PATTERNS = [\n // Chromium\n 'failed to fetch dynamically imported module',\n // Firefox\n 'error loading dynamically imported module',\n // Safari\n 'importing a module script failed',\n // Vite's preload helper, for the stylesheet of a lazily-imported chunk\n 'unable to preload css for',\n];\n\n/** Substring React's Flight client uses when a client module ID is unknown. */\nconst STALE_REFERENCE_PATTERN = 'could not find the module';\n\n/**\n * Pull a comparable message out of an unknown thrown value. Rejections cross\n * enough boundaries (event handlers, `unhandledrejection`, Vite's preload\n * event) that the value is not reliably an `Error`.\n */\nfunction messageOf(error: unknown): string {\n if (typeof error === 'string') return error.toLowerCase();\n if (error instanceof Error) return error.message.toLowerCase();\n if (error && typeof error === 'object' && 'message' in error) {\n const message = (error as { message: unknown }).message;\n if (typeof message === 'string') return message.toLowerCase();\n }\n return '';\n}\n\n/**\n * True when the error is React's \"client reference not in this manifest\"\n * failure — the payload was produced by a build this bundle doesn't match.\n */\nexport function isStaleClientReference(error: unknown): boolean {\n return messageOf(error).includes(STALE_REFERENCE_PATTERN);\n}\n\n/**\n * True when the error is a failed dynamic import or CSS preload — the\n * content-hashed chunk this bundle asks for is gone from the CDN.\n */\nexport function isChunkLoadError(error: unknown): boolean {\n if (error instanceof Error && error.name === 'ChunkLoadError') return true;\n const message = messageOf(error);\n return CHUNK_LOAD_PATTERNS.some((pattern) => message.includes(pattern));\n}\n\n// ─── Staleness ───────────────────────────────────────────────────\n\n/**\n * True once this client is known to be running against a superseded\n * deployment. One-way: a document that has gone stale cannot come back, since\n * the only way to get fresh code is to load a new document.\n *\n * In-memory on purpose. Persisting it would be wrong — the next document gets\n * whatever build the server hands it, and if that one is stale too it will\n * find out the same way.\n */\nlet stale = false;\n\n/**\n * True once {@link initStaleClient} has seen a deployment ID — i.e. this is a\n * production build. False in dev, where nothing is ever stale: HMR owns code\n * updates, and a failed dynamic import is a real error the developer needs to\n * see rather than something a page load fixes.\n */\nlet deployed = false;\n\n/** Whether this client has been superseded by a newer deployment. */\nexport function isClientStale(): boolean {\n return stale;\n}\n\n/**\n * Record that the origin has told us this client is stale — the `X-Timber-Reload`\n * header on an RSC or action response. Authoritative, so it is not gated on\n * anything: a dev server never sends the header in the first place.\n */\nexport function markClientStale(): void {\n stale = true;\n}\n\n/**\n * Record staleness if `error` is one of the shapes a superseded bundle\n * produces. Returns whether it did, so a caller mid-navigation can decide to\n * hard-navigate instead of surfacing the error.\n *\n * Inference, unlike {@link markClientStale}, so it is gated on being a\n * production build. This is the single place that gate lives — call sites\n * cannot see each other, and \"remember to gate the next one too\" is not a\n * guarantee.\n */\nexport function markStaleFromError(error: unknown): boolean {\n if (!deployed) return false;\n\n // A document being torn down aborts its in-flight dynamic imports, and an\n // aborted import reads exactly like a chunk the CDN has dropped. That is not\n // evidence of staleness, and the bit outlives the teardown that produced it:\n // a document entering the back-forward cache is frozen, not destroyed, so a\n // false positive recorded on the way out would cost the *restored* page its\n // client-side navigation for good. `unload-guard.ts` clears its own flag on\n // a persisted `pageshow`; this one is one-way by design, so it has to not be\n // set in the first place.\n if (isPageUnloading()) return false;\n\n if (!isStaleClientReference(error) && !isChunkLoadError(error)) return false;\n stale = true;\n return true;\n}\n\n// ─── Initialization ──────────────────────────────────────────────\n\n/**\n * The underlying error Vite attaches to a `vite:preloadError` event.\n *\n * Narrowed at runtime rather than cast: the property is Vite's, not ours, and\n * a shape change should fall back to matching the event itself rather than\n * silently reading `undefined` forever.\n */\nfunction preloadErrorPayload(event: Event): unknown {\n return 'payload' in event ? (event as Event & { payload: unknown }).payload : event;\n}\n\n/**\n * Arm the module, and register the listeners that catch stale-client errors\n * never passing through the router: a lazily imported client component, a\n * preloaded stylesheet, anything that rejects outside a navigation.\n *\n * No-op when `deploymentId` is null — that is dev. Called from the browser\n * entry before hydration, so it precedes any render or navigation that could\n * throw.\n */\nexport function initStaleClient(deploymentId: string | null): void {\n if (!deploymentId) return;\n deployed = true;\n\n // Vite dispatches this for failed chunk/CSS preloads. Note what is *not*\n // here: `preventDefault()`. Recording staleness is not recovering from this\n // error — the page is still broken until the user navigates — so Vite must\n // go on rethrowing it rather than have it silently swallowed.\n window.addEventListener('vite:preloadError', (event) => {\n markStaleFromError(preloadErrorPayload(event));\n });\n\n window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => {\n markStaleFromError(event.reason);\n });\n}\n","'use client';\n\n/**\n * Framework-injected React error boundary.\n *\n * Catches errors thrown by children and renders a fallback component\n * with the appropriate props based on error type:\n * - DenySignal (4xx) → { status, dangerouslyPassData }\n * - RenderError (5xx) → { error, digest, reset }\n * - Unhandled error → { error, digest: null, reset }\n *\n * The `status` prop controls which errors this boundary catches:\n * - Specific code (e.g. 403) → only that status\n * - Category (400) → any 4xx\n * - Category (500) → any 5xx\n * - Omitted → catches everything (error.tsx behavior)\n *\n * See design/10-error-handling.md §\"Status-Code Files\"\n */\n\nimport { Component, createElement, type ReactNode } from 'react';\nimport { getSsrData } from './ssr-data.ts';\nimport { getRouterOrNull } from './router-ref.ts';\nimport { markStaleFromError } from './stale-client.ts';\n// Page unload detection: when the page is being torn down, error boundaries\n// suppress activation — the error is from the aborted connection, not the\n// application. unload-guard.ts is the single owner of that state; this module\n// used to keep its own copy, which drifted (TIM-1260).\nimport { isPageUnloading } from './unload-guard.ts';\n\n// ─── Digest Types ────────────────────────────────────────────────────────────\n\n/** Structured digest returned by RSC onError for DenySignal. */\ninterface DenyDigest {\n type: 'deny';\n status: number;\n data: unknown;\n /**\n * Segment key of the segment that owns the matched deny page, present when\n * an in-tree catch site matched a file it may not render at its own\n * position and re-threw to reach the owner. See TIM-1356.\n */\n ownerKey?: string;\n}\n\n/** Structured digest returned by RSC onError for RenderError. */\ninterface RenderErrorDigest {\n type: 'render-error';\n code: string;\n data: unknown;\n status: number;\n}\n\n/** Structured digest returned by RSC onError for RedirectSignal. */\ninterface RedirectDigest {\n type: 'redirect';\n location: string;\n status: number;\n}\n\ntype ParsedDigest = DenyDigest | RenderErrorDigest | RedirectDigest;\n\n// ─── Props & State ───────────────────────────────────────────────────────────\n\nexport interface TimberErrorBoundaryProps {\n /** The component to render when an error is caught. */\n fallbackComponent?: (...args: unknown[]) => ReactNode;\n /**\n * Pre-rendered fallback element. Used for MDX status files which are server\n * components and cannot be passed as function props across the RSC→client\n * boundary. When set, rendered directly instead of calling fallbackComponent.\n *\n * See design/10-error-handling.md §\"Status-Code File Variants\" — MDX status\n * files are server components by default (zero client JS).\n */\n fallbackElement?: ReactNode;\n /**\n * Status code filter. If set, only catches errors matching this status.\n * 400 = any 4xx, 500 = any 5xx, specific number = exact match.\n */\n status?: number;\n /**\n * When true, this boundary wraps a parallel slot. Slot denials are\n * graceful degradation — they must NOT change the HTTP status code.\n */\n isSlotBoundary?: boolean;\n /**\n * Segment key of the segment that declared this boundary. Only meaningful\n * against a deny digest carrying an `ownerKey`. See the decline rule in\n * `render()` and TIM-1356.\n */\n segmentKey?: string;\n children: ReactNode;\n}\n\ninterface TimberErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n}\n\n// Module-level guard: the first boundary to catch a given redirect digest\n// schedules the navigation and marks it handled here. Subsequent renders\n// (React retries the error-boundary render path multiple times in dev, and\n// outer boundaries that also see the error) become no-ops, preventing\n// duplicate router.navigate() calls.\nconst _handledRedirects = new WeakSet<object>();\n\n// Companion guard keyed by location: client-render redirect() (the\n// next/navigation shim) throws a FRESH error instance on every render\n// attempt, and React replays the errored render — so the WeakSet alone\n// would schedule the navigation twice. While a navigation to a location\n// is pending, further redirect errors for the same location are no-ops.\nlet _pendingRedirectLocation: string | null = null;\n\n// ─── Component ───────────────────────────────────────────────────────────────\n\nexport class TimberErrorBoundary extends Component<\n TimberErrorBoundaryProps,\n TimberErrorBoundaryState\n> {\n constructor(props: TimberErrorBoundaryProps) {\n super(props);\n this.state = { hasError: false, error: null };\n }\n\n static getDerivedStateFromError(error: Error): TimberErrorBoundaryState {\n // Suppress error boundaries during page unload (refresh/navigate away).\n // The aborted connection causes React's streaming hydration to error,\n // but the page is about to be replaced — showing an error boundary\n // would be a jarring flash for the user.\n if (isPageUnloading()) {\n return { hasError: false, error: null };\n }\n\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error): void {\n // A stale bundle most often surfaces here rather than as a rejected\n // navigation: a client reference missing from this build's manifest, or\n // a lazily-imported chunk the CDN no longer has, thrown during render.\n //\n // Recording it is all that happens — this boundary still renders its\n // fallback, and the recovery is the user's next navigation, which the\n // router turns into a full document load. Reloading from here would be a\n // page load the user did not ask for, on a page that may be mid-rollout\n // and would come back just as stale (TIM-1275).\n markStaleFromError(error);\n }\n\n componentDidUpdate(prevProps: TimberErrorBoundaryProps): void {\n // Reset error state when children change (e.g. client-side navigation).\n // Without this, navigating from one error page to another keeps the\n // stale error — getDerivedStateFromError doesn't re-fire for new children.\n if (this.state.hasError && prevProps.children !== this.props.children) {\n this.setState({ hasError: false, error: null });\n }\n }\n\n /** Reset the error state so children re-render. */\n private reset = () => {\n this.setState({ hasError: false, error: null });\n };\n\n render(): ReactNode {\n if (!this.state.hasError || !this.state.error) {\n return this.props.children;\n }\n\n const error = this.state.error;\n const parsed = parseDigest(error);\n\n // RedirectSignal handling splits by environment:\n //\n // - SSR (no window): re-throw so the Fizz shell fails and the server\n // pipeline catch block can produce a proper HTTP 3xx response. See\n // design/04-authorization.md.\n //\n // - Client (window exists): a RedirectSignal that leaked into the RSC\n // Flight payload during client navigation would otherwise bubble past\n // every error boundary to `window.onerror`, which in dev triggers the\n // Vite error overlay (false positive — navigation itself works).\n // Instead, ask the router to perform an SPA navigation to the target\n // and render nothing while it takes over. The router's own catch path\n // handles cross-origin / hard navigations. See TIM-838.\n if (parsed?.type === 'redirect') {\n if (typeof window === 'undefined') {\n throw error;\n }\n // Dedupe: React may render the error-boundary fallback path more\n // than once for a single caught error (dev-mode double render, and\n // additional boundaries above us in the tree). Only schedule the\n // navigation on the first render that sees this error instance.\n const alreadyHandled =\n _handledRedirects.has(error) || _pendingRedirectLocation === parsed.location;\n if (!alreadyHandled) {\n _handledRedirects.add(error);\n const router = getRouterOrNull();\n if (router) {\n _pendingRedirectLocation = parsed.location;\n // Schedule the navigation outside the render phase to avoid\n // setState-during-render warnings from router state updates.\n queueMicrotask(() => {\n router\n .navigate(parsed.location, { replace: true })\n .catch(() => {\n // Fall back to a hard navigation if the soft navigation fails.\n window.location.href = parsed.location;\n })\n .finally(() => {\n _pendingRedirectLocation = null;\n });\n });\n } else {\n // No router available (shouldn't happen post-bootstrap) — fall\n // back to a hard navigation so the user still ends up at the\n // target.\n window.location.href = parsed.location;\n }\n }\n return null;\n }\n\n // Owner-addressed denies: an in-tree catch site already resolved the\n // fallback chain and matched a file it may not render at its own\n // position, so it re-threw to reach the segment that owns that file.\n // Every boundary but that one declines — otherwise the nearest boundary\n // whose status filter happens to match catches first and renders the\n // deny page at its own depth, wrapped in the layouts the hoist existed\n // to skip, and from a different file than the chain chose. (Boundary\n // nesting is per-segment `{status} → category → error`; the 4xx chain\n // runs status files across ALL segments before any error.tsx, so a\n // leaf error.tsx would otherwise beat a root 404.tsx.)\n //\n // Declining is the default: an unkeyed boundary — a slot boundary, or\n // one from a construction site that builds no deny chains — never\n // claims a deny addressed to a segment. See TIM-1356.\n // `typeof === 'string'`, not a null check: the digest is untrusted JSON —\n // `parseDigest` validates only `type`, and any component can set\n // `error.digest` itself. A non-string `ownerKey` is unequal to every\n // boundary's key, so a truthiness test would let one error decline its\n // way past the entire tree and blank the page, where it would otherwise\n // have hit the nearest matching boundary.\n if (\n parsed?.type === 'deny' &&\n typeof parsed.ownerKey === 'string' &&\n parsed.ownerKey !== this.props.segmentKey\n ) {\n throw error;\n }\n\n // If this boundary has a status filter, check whether the error matches.\n // Non-matching errors re-throw so an outer boundary can catch them.\n if (this.props.status != null) {\n const errorStatus = getErrorStatus(parsed, error);\n if (errorStatus == null || !statusMatches(this.props.status, errorStatus)) {\n // Re-throw: this boundary doesn't handle this error.\n throw error;\n }\n }\n\n // Report DenySignal handling so the pipeline skips the redundant\n // renderDenyPage() re-render — but ONLY when this boundary can fully\n // handle the deny in-tree:\n //\n // ✅ TSX boundaries (fallbackComponent) — can receive runtime props\n // (status, dangerouslyPassData) dynamically in render()\n // ❌ MDX boundaries (fallbackElement) — pre-rendered at tree-build time,\n // cannot receive runtime deny props. Must fall through to re-render.\n //\n // For qualifying segment boundaries: also set the HTTP status code from\n // the DenySignal so the Response has the correct 4xx status. This runs\n // synchronously during Fizz rendering, BEFORE onShellReady.\n //\n // For slot boundaries: set _denyHandledByBoundary but do NOT change\n // statusCode — slot denials are graceful degradation.\n //\n // See TIM-664, LOCAL-298.\n if (parsed?.type === 'deny') {\n const canHandleInTree = this.props.fallbackElement == null;\n\n if (canHandleInTree || this.props.isSlotBoundary) {\n const ssrData = getSsrData();\n if (ssrData?._navContext) {\n ssrData._navContext._denyHandledByBoundary = true;\n if (!this.props.isSlotBoundary) {\n ssrData._navContext.statusCode = parsed.status;\n }\n }\n }\n }\n\n // Pre-rendered fallback element (MDX status files) — render directly.\n // MDX components are server components that cannot be passed as function\n // props across the RSC→client boundary. Instead, they are pre-rendered\n // as elements in the RSC environment and passed here as fallbackElement.\n if (this.props.fallbackElement != null) {\n return this.props.fallbackElement;\n }\n\n // Render the fallback component with the right props shape.\n if (parsed?.type === 'deny') {\n // Status-code file boundaries (404.tsx, 4xx.tsx, …) get the documented\n // 4xx props. error.tsx boundaries (no status filter) get their\n // documented { error, digest, reset } contract — plus the deny extras\n // for dual-shape implementations. Without the error prop, an error.tsx\n // reading error.message crashes on deny fall-through. See TIM-1081.\n if (this.props.status != null) {\n return createElement(this.props.fallbackComponent as never, {\n status: parsed.status,\n dangerouslyPassData: parsed.data,\n });\n }\n return createElement(this.props.fallbackComponent as never, {\n error: denyError(parsed.status, error),\n digest: null,\n reset: this.reset,\n status: parsed.status,\n dangerouslyPassData: parsed.data,\n });\n }\n\n // 5xx / RenderError / unhandled error\n const digest =\n parsed?.type === 'render-error' ? { code: parsed.code, data: parsed.data } : null;\n\n return createElement(this.props.fallbackComponent as never, {\n error,\n digest,\n reset: this.reset,\n });\n }\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────────\n\n/**\n * Parse the structured digest from the error.\n * React sets `error.digest` from the string returned by RSC's onError.\n */\nfunction parseDigest(error: Error): ParsedDigest | null {\n const raw = (error as { digest?: string }).digest;\n if (typeof raw !== 'string') return null;\n try {\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === 'object' && typeof parsed.type === 'string') {\n return parsed as ParsedDigest;\n }\n } catch (e) {\n console.debug('[timber] error digest is not valid JSON:', e instanceof Error ? e.message : e);\n }\n return null;\n}\n\n/**\n * The Error an `error.tsx` serving a deny should receive.\n *\n * The error that actually arrives here is React's, not the framework's: a\n * server-thrown error crossing Flight has its message and name replaced with\n * a generic redacted string in production, by design. An `error.tsx` written\n * per the docs branches on `error.message` — so handing it the redacted error\n * gives it nothing to branch on, while the in-tree path hands the same file a\n * `DenySignal` named error reading `Access denied with status <N>` (see\n * `renderDenyEntry`). This reconstructs that shape from the digest, which is\n * where the status already is.\n *\n * Derived from the status alone — no stack, no server data — so it is safe on\n * the client for the same reason the digest is. See design/13-security.md\n * §\"Errors don't leak\", TIM-1081.\n */\nfunction denyError(status: number, original: Error): Error {\n // A dev build may have preserved something more useful than the redaction.\n if (original.name === 'DenySignal') return original;\n const error = new Error(`Access denied with status ${status}`);\n error.name = 'DenySignal';\n return error;\n}\n\n/**\n * Extract the HTTP status code from a parsed digest or error message.\n * Falls back to message pattern matching for errors without a digest.\n */\nfunction getErrorStatus(parsed: ParsedDigest | null, error: Error): number | null {\n if (parsed?.type === 'deny') return parsed.status;\n if (parsed?.type === 'render-error') return parsed.status;\n if (parsed?.type === 'redirect') return parsed.status;\n\n // Fallback: parse DenySignal message pattern for errors that lost their digest\n const match = error.message.match(/^Access denied with status (\\d+)$/);\n if (match) return parseInt(match[1], 10);\n\n // Unhandled errors are implicitly 500\n return 500;\n}\n\n/**\n * Check whether an error's status matches the boundary's status filter.\n * Category markers (400, 500) match any status in that range.\n */\nfunction statusMatches(boundaryStatus: number, errorStatus: number): boolean {\n // Category catch-all: 400 matches any 4xx, 500 matches any 5xx\n if (boundaryStatus === 400) return errorStatus >= 400 && errorStatus <= 499;\n if (boundaryStatus === 500) return errorStatus >= 500 && errorStatus <= 599;\n // Exact match\n return boundaryStatus === errorStatus;\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { p as globalRouter, s as _setGlobalRouter } from "./ssr-data-14MXm7Pj.js";
1
+ import { p as globalRouter, s as _setGlobalRouter } from "./ssr-data-Ya2HJPFp.js";
2
2
  //#region src/client/router-ref.ts
3
3
  /**
4
4
  * Set the global router instance. Called once during bootstrap.
@@ -25,4 +25,4 @@ function getRouterOrNull() {
25
25
  //#endregion
26
26
  export { getRouterOrNull as n, setGlobalRouter as r, getRouter as t };
27
27
 
28
- //# sourceMappingURL=router-ref-DuYuV_0Q.js.map
28
+ //# sourceMappingURL=router-ref-BzqbPwYC.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"router-ref-DuYuV_0Q.js","names":[],"sources":["../../src/client/router-ref.ts"],"sourcesContent":["// Global router reference — shared between browser-entry and client hooks.\n//\n// Delegates to client/state.ts for the actual module-level variable.\n// This ensures singleton semantics regardless of import path — all\n// callers converge on the same state.ts instance via the barrel.\n//\n// See design/18-build-system.md §\"Module Singleton Strategy\"\n\nimport type { RouterInstance } from './router-types.ts';\nimport { globalRouter, _setGlobalRouter } from './state.ts';\n\n/**\n * Set the global router instance. Called once during bootstrap.\n */\nexport function setGlobalRouter(router: RouterInstance): void {\n _setGlobalRouter(router);\n}\n\n/**\n * Get the global router instance. Throws if called before bootstrap.\n * Used by client-side hooks (usePendingNavigation, etc.)\n */\nexport function getRouter(): RouterInstance {\n if (!globalRouter) {\n throw new Error('[timber] Router not initialized. getRouter() was called before bootstrap().');\n }\n return globalRouter;\n}\n\n/**\n * Get the global router instance or null if not yet initialized.\n * Used by useRouter() methods to avoid silent failures — callers\n * can log a meaningful warning instead of silently no-oping.\n */\nexport function getRouterOrNull(): RouterInstance | null {\n return globalRouter;\n}\n\n/**\n * Reset the global router to null. Used only in tests to isolate\n * module-level state between test cases.\n * @internal\n */\nexport function resetGlobalRouter(): void {\n _setGlobalRouter(null);\n}\n"],"mappings":";;;;;AAcA,SAAgB,gBAAgB,QAA8B;CAC5D,iBAAiB,MAAM;AACzB;;;;;AAMA,SAAgB,YAA4B;CAC1C,IAAI,CAAC,cACH,MAAM,IAAI,MAAM,6EAA6E;CAE/F,OAAO;AACT;;;;;;AAOA,SAAgB,kBAAyC;CACvD,OAAO;AACT"}
1
+ {"version":3,"file":"router-ref-BzqbPwYC.js","names":[],"sources":["../../src/client/router-ref.ts"],"sourcesContent":["// Global router reference — shared between browser-entry and client hooks.\n//\n// Delegates to client/state.ts for the actual module-level variable.\n// This ensures singleton semantics regardless of import path — all\n// callers converge on the same state.ts instance via the barrel.\n//\n// See design/18-build-system.md §\"Module Singleton Strategy\"\n\nimport type { RouterInstance } from './router-types.ts';\nimport { globalRouter, _setGlobalRouter } from './state.ts';\n\n/**\n * Set the global router instance. Called once during bootstrap.\n */\nexport function setGlobalRouter(router: RouterInstance): void {\n _setGlobalRouter(router);\n}\n\n/**\n * Get the global router instance. Throws if called before bootstrap.\n * Used by client-side hooks (usePendingNavigation, etc.)\n */\nexport function getRouter(): RouterInstance {\n if (!globalRouter) {\n throw new Error('[timber] Router not initialized. getRouter() was called before bootstrap().');\n }\n return globalRouter;\n}\n\n/**\n * Get the global router instance or null if not yet initialized.\n * Used by useRouter() methods to avoid silent failures — callers\n * can log a meaningful warning instead of silently no-oping.\n */\nexport function getRouterOrNull(): RouterInstance | null {\n return globalRouter;\n}\n\n/**\n * Reset the global router to null. Used only in tests to isolate\n * module-level state between test cases.\n * @internal\n */\nexport function resetGlobalRouter(): void {\n _setGlobalRouter(null);\n}\n"],"mappings":";;;;;AAcA,SAAgB,gBAAgB,QAA8B;CAC5D,iBAAiB,MAAM;AACzB;;;;;AAMA,SAAgB,YAA4B;CAC1C,IAAI,CAAC,cACH,MAAM,IAAI,MAAM,6EAA6E;CAE/F,OAAO;AACT;;;;;;AAOA,SAAgB,kBAAyC;CACvD,OAAO;AACT"}
@@ -4,11 +4,6 @@ var globalRouter = null;
4
4
  function _setGlobalRouter(router) {
5
5
  globalRouter = router;
6
6
  }
7
- /**
8
- * ALS-backed SSR data provider. When registered, getSsrData() reads from
9
- * this function (ALS store) instead of module-level currentSsrData.
10
- */
11
- var ssrDataProvider;
12
7
  /** Fallback SSR data for tests and environments without ALS. */
13
8
  var currentSsrData;
14
9
  function _setCurrentSsrData(data) {
@@ -94,10 +89,9 @@ function clearSsrData() {
94
89
  * Used by client hooks' server snapshot functions.
95
90
  */
96
91
  function getSsrData() {
97
- if (ssrDataProvider) return ssrDataProvider();
98
92
  return currentSsrData;
99
93
  }
100
94
  //#endregion
101
95
  export { _setCurrentParams as a, _setUnloading as c, currentParams as d, currentSlotParams as f, _setCachedSearch as i, cachedSearch as l, unloading as m, getSsrData as n, _setCurrentSlotParams as o, globalRouter as p, setSsrData as r, _setGlobalRouter as s, clearSsrData as t, cachedSearchParams as u };
102
96
 
103
- //# sourceMappingURL=ssr-data-14MXm7Pj.js.map
97
+ //# sourceMappingURL=ssr-data-Ya2HJPFp.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ssr-data-14MXm7Pj.js","names":[],"sources":["../../src/client/state.ts","../../src/client/ssr-data.ts"],"sourcesContent":["/**\n * Centralized client singleton state registry.\n *\n * ALL mutable module-level state that must have singleton semantics across\n * the client bundle lives here. Individual modules (router-ref.ts, ssr-data.ts,\n * use-segment-params.ts, use-search-params.ts, unload-guard.ts) import from this file\n * and re-export thin wrapper functions.\n *\n * Why: In Vite dev, a module is instantiated separately if reached via different\n * import paths (e.g., relative `./foo.js` vs barrel `@timber-js/app/client`).\n * By centralizing all mutable state in a single module that is always reached\n * through the same dependency chain (barrel → wrapper → state.ts), we guarantee\n * a single instance of every piece of shared state.\n *\n * DO NOT import this file from outside client/. Server code must never depend\n * on client state. The barrel (client/index.ts) is the public entry point.\n *\n * See design/18-build-system.md §\"Module Singleton Strategy\" and\n * §\"Singleton State Registry\".\n */\n\nimport type { CoercedParams } from '../shared/param-value.ts';\nimport type { SlotParamsRecord } from '../shared/slot-params.ts';\nimport type { RouterInstance } from './router-types.ts';\nimport type { SsrData } from './ssr-data.ts';\n\n// ─── Router (from router-ref.ts) ──────────────────────────────────────────\n\n/** The global router singleton — set once during bootstrap. */\nexport let globalRouter: RouterInstance | null = null;\n\nexport function _setGlobalRouter(router: RouterInstance | null): void {\n globalRouter = router;\n}\n\n// ─── SSR Data Provider (from ssr-data.ts) ──────────────────────────────────\n\n/**\n * ALS-backed SSR data provider. When registered, getSsrData() reads from\n * this function (ALS store) instead of module-level currentSsrData.\n */\nexport let ssrDataProvider: (() => SsrData | undefined) | undefined;\n\nexport function _setSsrDataProvider(provider: (() => SsrData | undefined) | undefined): void {\n ssrDataProvider = provider;\n}\n\n/** Fallback SSR data for tests and environments without ALS. */\nexport let currentSsrData: SsrData | undefined;\n\nexport function _setCurrentSsrData(data: SsrData | undefined): void {\n currentSsrData = data;\n}\n\n// ─── Route Params (from use-segment-params.ts) ──────────────────────────────────────\n\n/** Current route params snapshot — replaced (not mutated) on each navigation. */\nexport let currentParams: CoercedParams = {};\n\nexport function _setCurrentParams(params: CoercedParams): void {\n currentParams = params;\n}\n\n/**\n * Per-slot params snapshot, keyed by slot tree path — the module-level\n * fallback behind `useSegmentParams(slotPath)` when no NavigationContext is\n * mounted (tests, calls outside a component). Replaced, never mutated, on\n * each navigation. See TIM-1285.\n */\nexport let currentSlotParams: SlotParamsRecord | null = null;\n\nexport function _setCurrentSlotParams(slotParams: SlotParamsRecord | null): void {\n currentSlotParams = slotParams;\n}\n\n/** Listeners notified when currentParams changes. */\nexport const paramsListeners = new Set<() => void>();\n\n// ─── Search Params Cache (from use-search-params.ts) ────────────────────────\n\n/** Cached search string — avoids reparsing when URL hasn't changed. */\nexport let cachedSearch = '';\nexport let cachedSearchParams = new URLSearchParams();\n\nexport function _setCachedSearch(search: string, params: URLSearchParams): void {\n cachedSearch = search;\n cachedSearchParams = params;\n}\n\n// ─── Unload Guard (from unload-guard.ts) ─────────────────────────────────────\n\n/** Whether the page is currently being unloaded. */\nexport let unloading = false;\n\nexport function _setUnloading(value: boolean): void {\n unloading = value;\n}\n","/**\n * SSR Data — per-request state for client hooks during server-side rendering.\n *\n * RSC and SSR are separate Vite module graphs (see design/18-build-system.md),\n * so the RSC environment's request-context ALS is not visible to SSR modules.\n * This module provides getter/setter functions that ssr-entry.ts uses to\n * populate per-request data for React's render.\n *\n * Request isolation: On the server, ssr-entry.ts registers an ALS-backed\n * provider via registerSsrDataProvider(). getSsrData() reads from the ALS\n * store, ensuring correct per-request data even when Suspense boundaries\n * resolve asynchronously across concurrent requests. The module-level\n * setSsrData/clearSsrData functions are kept as a fallback for tests\n * and environments without ALS.\n *\n * IMPORTANT: This module must NOT import node:async_hooks or any Node.js-only\n * APIs, as it's imported by 'use client' hooks that are bundled for the browser.\n * The ALS instance lives in ssr-entry.ts (server-only); this module only holds\n * a reference to the provider function.\n *\n * All mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n */\n\nimport {\n ssrDataProvider,\n currentSsrData,\n _setSsrDataProvider,\n _setCurrentSsrData,\n} from './state.ts';\nimport type { CoercedParams } from '../shared/param-value.ts';\nimport type { SlotParamsRecord } from '../shared/slot-params.ts';\n\n// ─── Types ────────────────────────────────────────────────────────\n\nexport interface SsrData {\n /** The request's URL pathname (e.g. '/dashboard/settings') */\n pathname: string;\n /** The request's search params as a plain record */\n searchParams: Record<string, string>;\n /** The request's cookies as name→value pairs */\n cookies: Map<string, string>;\n /** The request's route params (e.g. { id: '123' }) */\n params: CoercedParams;\n /**\n * Per-slot params keyed by slot tree path, absent when the request rendered\n * no slot with params of its own (TIM-1285).\n */\n slotParams?: SlotParamsRecord;\n /**\n * Mutable reference to NavContext for error boundary → pipeline communication.\n *\n * When TimberErrorBoundary catches a DenySignal during SSR, it:\n * 1. Sets `statusCode` to the deny status (e.g., 403) — so the HTTP\n * Response has the correct status code without a re-render.\n * 2. Sets `_denyHandledByBoundary = true` — so the pipeline skips\n * the redundant renderDenyPage() re-render.\n *\n * This runs synchronously during Fizz rendering, BEFORE onShellReady,\n * so the status code is committed before any bytes are sent.\n *\n * See TIM-664, design/04-authorization.md §\"React.cache Scope in Deny/Error Re-renders\"\n */\n _navContext?: { statusCode?: number; _denyHandledByBoundary?: boolean };\n}\n\n// ─── ALS-Backed Provider ─────────────────────────────────────────\n//\n// Server-side code (ssr-entry.ts) registers a provider that reads\n// from AsyncLocalStorage. This avoids importing node:async_hooks\n// in this browser-bundled module.\n//\n// Module singleton guarantee: In Vite's SSR environment, both\n// ssr-entry.ts (via #/client/ssr-data.js) and client component hooks\n// (via @timber-js/app/client) must resolve to the SAME module instance\n// of this file. The timber-shims plugin ensures this by remapping\n// @timber-js/app/client → src/client/index.ts in the SSR environment.\n// Without this remap, @timber-js/app/client resolves to dist/ (via\n// package.json exports), creating a split where registerSsrDataProvider\n// writes to one instance but getSsrData reads from another.\n// See timber-shims plugin resolveId for details.\n\n/**\n * Register an ALS-backed SSR data provider. Called once at module load\n * by ssr-entry.ts to wire up per-request data via AsyncLocalStorage.\n *\n * When registered, getSsrData() reads from the provider (ALS store)\n * instead of module-level state, ensuring correct isolation for\n * concurrent requests with streaming Suspense.\n */\nexport function registerSsrDataProvider(provider: () => SsrData | undefined): void {\n _setSsrDataProvider(provider);\n}\n\n// ─── Module-Level Fallback ────────────────────────────────────────\n//\n// Used by tests and as a fallback when no ALS provider is registered.\n\n/**\n * Set the SSR data for the current request via module-level state.\n *\n * In production, ssr-entry.ts uses ALS (runWithSsrData) instead.\n * This function is retained for tests and as a fallback.\n */\nexport function setSsrData(data: SsrData): void {\n _setCurrentSsrData(data);\n}\n\n/**\n * Clear the SSR data after rendering completes.\n *\n * In production, ALS scope handles cleanup automatically.\n * This function is retained for tests and as a fallback.\n */\nexport function clearSsrData(): void {\n _setCurrentSsrData(undefined);\n}\n\n/**\n * Read the current request's SSR data. Returns undefined when called\n * outside an SSR render (i.e. on the client after hydration).\n *\n * Prefers the ALS-backed provider when registered (server-side),\n * falling back to module-level state (tests, legacy).\n *\n * Used by client hooks' server snapshot functions.\n */\nexport function getSsrData(): SsrData | undefined {\n if (ssrDataProvider) {\n return ssrDataProvider();\n }\n return currentSsrData;\n}\n"],"mappings":";;AA6BA,IAAW,eAAsC;AAEjD,SAAgB,iBAAiB,QAAqC;CACpE,eAAe;AACjB;;;;;AAQA,IAAW;;AAOX,IAAW;AAEX,SAAgB,mBAAmB,MAAiC;CAClE,iBAAiB;AACnB;;AAKA,IAAW,gBAA+B,CAAC;AAE3C,SAAgB,kBAAkB,QAA6B;CAC7D,gBAAgB;AAClB;;;;;;;AAQA,IAAW,oBAA6C;AAExD,SAAgB,sBAAsB,YAA2C;CAC/E,oBAAoB;AACtB;;AAQA,IAAW,eAAe;AAC1B,IAAW,qBAAqB,IAAI,gBAAgB;AAEpD,SAAgB,iBAAiB,QAAgB,QAA+B;CAC9E,eAAe;CACf,qBAAqB;AACvB;;AAKA,IAAW,YAAY;AAEvB,SAAgB,cAAc,OAAsB;CAClD,YAAY;AACd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACQA,SAAgB,WAAW,MAAqB;CAC9C,mBAAmB,IAAI;AACzB;;;;;;;AAQA,SAAgB,eAAqB;CACnC,mBAAmB,KAAA,CAAS;AAC9B;;;;;;;;;;AAWA,SAAgB,aAAkC;CAChD,IAAI,iBACF,OAAO,gBAAgB;CAEzB,OAAO;AACT"}
1
+ {"version":3,"file":"ssr-data-Ya2HJPFp.js","names":[],"sources":["../../src/client/state.ts","../../src/client/ssr-data.ts"],"sourcesContent":["/**\n * Centralized client singleton state registry.\n *\n * ALL mutable module-level state that must have singleton semantics across\n * the client bundle lives here. Individual modules (router-ref.ts, ssr-data.ts,\n * use-segment-params.ts, use-search-params.ts, unload-guard.ts) import from this file\n * and re-export thin wrapper functions.\n *\n * Why: In Vite dev, a module is instantiated separately if reached via different\n * import paths (e.g., relative `./foo.js` vs barrel `@timber-js/app/client`).\n * By centralizing all mutable state in a single module that is always reached\n * through the same dependency chain (barrel → wrapper → state.ts), we guarantee\n * a single instance of every piece of shared state.\n *\n * DO NOT import this file from outside client/. Server code must never depend\n * on client state. The barrel (client/index.ts) is the public entry point.\n *\n * See design/18-build-system.md §\"Module Singleton Strategy\" and\n * §\"Singleton State Registry\".\n */\n\nimport type { CoercedParams } from '../shared/param-value.ts';\nimport type { SlotParamsRecord } from '../shared/slot-params.ts';\nimport type { RouterInstance } from './router-types.ts';\nimport type { SsrData } from './ssr-data.ts';\n\n// ─── Router (from router-ref.ts) ──────────────────────────────────────────\n\n/** The global router singleton — set once during bootstrap. */\nexport let globalRouter: RouterInstance | null = null;\n\nexport function _setGlobalRouter(router: RouterInstance | null): void {\n globalRouter = router;\n}\n\n// ─── SSR Data Provider (from ssr-data.ts) ──────────────────────────────────\n\n/**\n * ALS-backed SSR data provider. When registered, getSsrData() reads from\n * this function (ALS store) instead of module-level currentSsrData.\n */\nexport let ssrDataProvider: (() => SsrData | undefined) | undefined;\n\nexport function _setSsrDataProvider(provider: (() => SsrData | undefined) | undefined): void {\n ssrDataProvider = provider;\n}\n\n/** Fallback SSR data for tests and environments without ALS. */\nexport let currentSsrData: SsrData | undefined;\n\nexport function _setCurrentSsrData(data: SsrData | undefined): void {\n currentSsrData = data;\n}\n\n// ─── Route Params (from use-segment-params.ts) ──────────────────────────────────────\n\n/** Current route params snapshot — replaced (not mutated) on each navigation. */\nexport let currentParams: CoercedParams = {};\n\nexport function _setCurrentParams(params: CoercedParams): void {\n currentParams = params;\n}\n\n/**\n * Per-slot params snapshot, keyed by slot tree path — the module-level\n * fallback behind `useSegmentParams(slotPath)` when no NavigationContext is\n * mounted (tests, calls outside a component). Replaced, never mutated, on\n * each navigation. See TIM-1285.\n */\nexport let currentSlotParams: SlotParamsRecord | null = null;\n\nexport function _setCurrentSlotParams(slotParams: SlotParamsRecord | null): void {\n currentSlotParams = slotParams;\n}\n\n/** Listeners notified when currentParams changes. */\nexport const paramsListeners = new Set<() => void>();\n\n// ─── Search Params Cache (from use-search-params.ts) ────────────────────────\n\n/** Cached search string — avoids reparsing when URL hasn't changed. */\nexport let cachedSearch = '';\nexport let cachedSearchParams = new URLSearchParams();\n\nexport function _setCachedSearch(search: string, params: URLSearchParams): void {\n cachedSearch = search;\n cachedSearchParams = params;\n}\n\n// ─── Unload Guard (from unload-guard.ts) ─────────────────────────────────────\n\n/** Whether the page is currently being unloaded. */\nexport let unloading = false;\n\nexport function _setUnloading(value: boolean): void {\n unloading = value;\n}\n","/**\n * SSR Data — per-request state for client hooks during server-side rendering.\n *\n * RSC and SSR are separate Vite module graphs (see design/18-build-system.md),\n * so the RSC environment's request-context ALS is not visible to SSR modules.\n * This module provides getter/setter functions that ssr-entry.ts uses to\n * populate per-request data for React's render.\n *\n * Request isolation: On the server, ssr-entry.ts registers an ALS-backed\n * provider via registerSsrDataProvider(). getSsrData() reads from the ALS\n * store, ensuring correct per-request data even when Suspense boundaries\n * resolve asynchronously across concurrent requests. The module-level\n * setSsrData/clearSsrData functions are kept as a fallback for tests\n * and environments without ALS.\n *\n * IMPORTANT: This module must NOT import node:async_hooks or any Node.js-only\n * APIs, as it's imported by 'use client' hooks that are bundled for the browser.\n * The ALS instance lives in ssr-entry.ts (server-only); this module only holds\n * a reference to the provider function.\n *\n * All mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n */\n\nimport {\n ssrDataProvider,\n currentSsrData,\n _setSsrDataProvider,\n _setCurrentSsrData,\n} from './state.ts';\nimport type { CoercedParams } from '../shared/param-value.ts';\nimport type { SlotParamsRecord } from '../shared/slot-params.ts';\n\n// ─── Types ────────────────────────────────────────────────────────\n\nexport interface SsrData {\n /** The request's URL pathname (e.g. '/dashboard/settings') */\n pathname: string;\n /** The request's search params as a plain record */\n searchParams: Record<string, string>;\n /** The request's cookies as name→value pairs */\n cookies: Map<string, string>;\n /** The request's route params (e.g. { id: '123' }) */\n params: CoercedParams;\n /**\n * Per-slot params keyed by slot tree path, absent when the request rendered\n * no slot with params of its own (TIM-1285).\n */\n slotParams?: SlotParamsRecord;\n /**\n * Mutable reference to NavContext for error boundary → pipeline communication.\n *\n * When TimberErrorBoundary catches a DenySignal during SSR, it:\n * 1. Sets `statusCode` to the deny status (e.g., 403) — so the HTTP\n * Response has the correct status code without a re-render.\n * 2. Sets `_denyHandledByBoundary = true` — so the pipeline skips\n * the redundant renderDenyPage() re-render.\n *\n * This runs synchronously during Fizz rendering, BEFORE onShellReady,\n * so the status code is committed before any bytes are sent.\n *\n * See TIM-664, design/04-authorization.md §\"React.cache Scope in Deny/Error Re-renders\"\n */\n _navContext?: { statusCode?: number; _denyHandledByBoundary?: boolean };\n}\n\n// ─── ALS-Backed Provider ─────────────────────────────────────────\n//\n// Server-side code (ssr-entry.ts) registers a provider that reads\n// from AsyncLocalStorage. This avoids importing node:async_hooks\n// in this browser-bundled module.\n//\n// Module singleton guarantee: In Vite's SSR environment, both\n// ssr-entry.ts (via #/client/ssr-data.js) and client component hooks\n// (via @timber-js/app/client) must resolve to the SAME module instance\n// of this file. The timber-shims plugin ensures this by remapping\n// @timber-js/app/client → src/client/index.ts in the SSR environment.\n// Without this remap, @timber-js/app/client resolves to dist/ (via\n// package.json exports), creating a split where registerSsrDataProvider\n// writes to one instance but getSsrData reads from another.\n// See timber-shims plugin resolveId for details.\n\n/**\n * Register an ALS-backed SSR data provider. Called once at module load\n * by ssr-entry.ts to wire up per-request data via AsyncLocalStorage.\n *\n * When registered, getSsrData() reads from the provider (ALS store)\n * instead of module-level state, ensuring correct isolation for\n * concurrent requests with streaming Suspense.\n */\nexport function registerSsrDataProvider(provider: () => SsrData | undefined): void {\n _setSsrDataProvider(provider);\n}\n\n// ─── Module-Level Fallback ────────────────────────────────────────\n//\n// Used by tests and as a fallback when no ALS provider is registered.\n\n/**\n * Set the SSR data for the current request via module-level state.\n *\n * In production, ssr-entry.ts uses ALS (runWithSsrData) instead.\n * This function is retained for tests and as a fallback.\n */\nexport function setSsrData(data: SsrData): void {\n _setCurrentSsrData(data);\n}\n\n/**\n * Clear the SSR data after rendering completes.\n *\n * In production, ALS scope handles cleanup automatically.\n * This function is retained for tests and as a fallback.\n */\nexport function clearSsrData(): void {\n _setCurrentSsrData(undefined);\n}\n\n/**\n * Read the current request's SSR data. Returns undefined when called\n * outside an SSR render (i.e. on the client after hydration).\n *\n * Prefers the ALS-backed provider when registered (server-side),\n * falling back to module-level state (tests, legacy).\n *\n * Used by client hooks' server snapshot functions.\n */\nexport function getSsrData(): SsrData | undefined {\n if (ssrDataProvider) {\n return ssrDataProvider();\n }\n return currentSsrData;\n}\n"],"mappings":";;AA6BA,IAAW,eAAsC;AAEjD,SAAgB,iBAAiB,QAAqC;CACpE,eAAe;AACjB;;AAeA,IAAW;AAEX,SAAgB,mBAAmB,MAAiC;CAClE,iBAAiB;AACnB;;AAKA,IAAW,gBAA+B,CAAC;AAE3C,SAAgB,kBAAkB,QAA6B;CAC7D,gBAAgB;AAClB;;;;;;;AAQA,IAAW,oBAA6C;AAExD,SAAgB,sBAAsB,YAA2C;CAC/E,oBAAoB;AACtB;;AAQA,IAAW,eAAe;AAC1B,IAAW,qBAAqB,IAAI,gBAAgB;AAEpD,SAAgB,iBAAiB,QAAgB,QAA+B;CAC9E,eAAe;CACf,qBAAqB;AACvB;;AAKA,IAAW,YAAY;AAEvB,SAAgB,cAAc,OAAsB;CAClD,YAAY;AACd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACQA,SAAgB,WAAW,MAAqB;CAC9C,mBAAmB,IAAI;AACzB;;;;;;;AAQA,SAAgB,eAAqB;CACnC,mBAAmB,KAAA,CAAS;AAC9B;;;;;;;;;;AAWA,SAAgB,aAAkC;CAIhD,OAAO;AACT"}
@@ -1,6 +1,6 @@
1
1
  import { t as resolveSegmentParams } from "./slot-params-BCTmZkQB.js";
2
- import { a as _setCurrentParams, d as currentParams, f as currentSlotParams, n as getSsrData, o as _setCurrentSlotParams } from "./ssr-data-14MXm7Pj.js";
3
- import { n as getRouterOrNull } from "./router-ref-DuYuV_0Q.js";
2
+ import { a as _setCurrentParams, d as currentParams, f as currentSlotParams, n as getSsrData, o as _setCurrentSlotParams } from "./ssr-data-Ya2HJPFp.js";
3
+ import { n as getRouterOrNull } from "./router-ref-BzqbPwYC.js";
4
4
  import "./segment-context-CjOlyB8Y.js";
5
5
  import React, { createElement, useSyncExternalStore } from "react";
6
6
  //#region src/client/use-pending-navigation.ts
@@ -395,4 +395,4 @@ function useSegmentParams(segmentPath) {
395
395
  //#endregion
396
396
  export { supersedeNavigationTransitions as a, setNavigationState as c, setHardNavigating as i, useNavigationContext as l, setCurrentSlotParams as n, NavigationProvider as o, useSegmentParams as r, getNavigationState as s, setCurrentParams as t, usePendingNavigation as u };
397
397
 
398
- //# sourceMappingURL=use-segment-params-C4r4BD9T.js.map
398
+ //# sourceMappingURL=use-segment-params-DzTBpkvj.js.map