@timber-js/app 0.2.0-alpha.210 → 0.2.0-alpha.212
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-skill.md +10 -5
- package/dist/_chunks/{actions-Rjk4htmA.js → actions-CCdnVtWm.js} +8 -6
- package/dist/_chunks/actions-CCdnVtWm.js.map +1 -0
- package/dist/_chunks/{als-registry-DaxkVjt5.js → als-registry-BZqHCtq-.js} +2 -4
- package/dist/_chunks/als-registry-BZqHCtq-.js.map +1 -0
- package/dist/_chunks/{cache-api-DGdYfNJn.js → cache-api-LA3sBpUS.js} +5 -5
- package/dist/_chunks/{cache-api-DGdYfNJn.js.map → cache-api-LA3sBpUS.js.map} +1 -1
- package/dist/_chunks/{chains-BoO51joc.js → chains-BfoPFraI.js} +5 -146
- package/dist/_chunks/chains-BfoPFraI.js.map +1 -0
- package/dist/_chunks/classify-BT66U83D.js +147 -0
- package/dist/_chunks/classify-BT66U83D.js.map +1 -0
- package/dist/_chunks/{cli-check-ajNY3B2e.js → cli-check-BfQ54-UJ.js} +3 -3
- package/dist/_chunks/{cli-check-ajNY3B2e.js.map → cli-check-BfQ54-UJ.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-D2eI8jEg.js → cli-schema-sync-czh2dsLs.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-D2eI8jEg.js.map → cli-schema-sync-czh2dsLs.js.map} +1 -1
- package/dist/_chunks/client-dep-entries-CQwpb8dI.js +66 -0
- package/dist/_chunks/client-dep-entries-CQwpb8dI.js.map +1 -0
- package/dist/_chunks/{convention-lint-DLmhGsRS.js → convention-lint-BEVW4EID.js} +3 -3
- package/dist/_chunks/{convention-lint-DLmhGsRS.js.map → convention-lint-BEVW4EID.js.map} +1 -1
- package/dist/_chunks/{dev-server-v97rQH4b.js → dev-server-FKxptbnI.js} +2 -2
- package/dist/_chunks/{dev-server-v97rQH4b.js.map → dev-server-FKxptbnI.js.map} +1 -1
- package/dist/_chunks/{json-lossy-check-CVuRs2hG.js → json-lossy-check-C8zBY2uZ.js} +2 -2
- package/dist/_chunks/{json-lossy-check-CVuRs2hG.js.map → json-lossy-check-C8zBY2uZ.js.map} +1 -1
- package/dist/_chunks/{live-graph-9cSnn_h9.js → live-graph-C_4v-fHv.js} +4 -3
- package/dist/_chunks/{live-graph-9cSnn_h9.js.map → live-graph-C_4v-fHv.js.map} +1 -1
- package/dist/_chunks/{logger-BP0LN6vP.js → logger-CbLdcy-W.js} +2 -2
- package/dist/_chunks/{logger-BP0LN6vP.js.map → logger-CbLdcy-W.js.map} +1 -1
- package/dist/_chunks/{poison-scan-vGV7Re0B.js → poison-scan-C92liMAr.js} +2 -4
- package/dist/_chunks/poison-scan-C92liMAr.js.map +1 -0
- package/dist/_chunks/{scanner-DmqdxzbW.js → scanner-CQt12vE2.js} +2 -2
- package/dist/_chunks/{scanner-DmqdxzbW.js.map → scanner-CQt12vE2.js.map} +1 -1
- package/dist/_chunks/{sizeof-BM1409x2.js → sizeof-QPE5nd3u.js} +2 -2
- package/dist/_chunks/{sizeof-BM1409x2.js.map → sizeof-QPE5nd3u.js.map} +1 -1
- package/dist/_chunks/{walkers-Czu2jXFq.js → walkers-DAT4avhZ.js} +3 -3
- package/dist/_chunks/{walkers-Czu2jXFq.js.map → walkers-DAT4avhZ.js.map} +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/analyze/classify.d.ts +2 -0
- package/dist/analyze/classify.d.ts.map +1 -1
- package/dist/analyze/crawl-entry.js +3 -2
- package/dist/analyze/crawl-entry.js.map +1 -1
- package/dist/analyze/graph-command.js +2 -2
- package/dist/analyze/poison-scan.d.ts.map +1 -1
- package/dist/cache/index.js +2 -2
- package/dist/cache/stores/memory.js +1 -1
- package/dist/cdn/workers-cache-purge.js +1 -1
- package/dist/cli.js +3 -3
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/form.d.ts +17 -64
- package/dist/client/form.d.ts.map +1 -1
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +23 -51
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +25 -25
- package/dist/client/internal.js.map +1 -1
- package/dist/client/navigation-api.d.ts +25 -63
- package/dist/client/navigation-api.d.ts.map +1 -1
- package/dist/client/router-lifecycle.d.ts +17 -11
- package/dist/client/router-lifecycle.d.ts.map +1 -1
- package/dist/client/router-pipeline.d.ts +0 -1
- package/dist/client/router-pipeline.d.ts.map +1 -1
- package/dist/client/router-types.d.ts +14 -45
- package/dist/client/router-types.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -7
- package/dist/index.js.map +1 -1
- package/dist/plugins/client-dep-entries.d.ts +44 -0
- package/dist/plugins/client-dep-entries.d.ts.map +1 -0
- package/dist/plugins/content.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts +6 -0
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/rsc-runtime/rsc.d.ts +1 -1
- package/dist/rsc-runtime/rsc.d.ts.map +1 -1
- package/dist/server/action-client.d.ts +9 -5
- package/dist/server/action-client.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts +27 -8
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +22 -2
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/client-error-message.d.ts +12 -0
- package/dist/server/client-error-message.d.ts.map +1 -0
- package/dist/server/form-state-embed.d.ts +32 -0
- package/dist/server/form-state-embed.d.ts.map +1 -0
- package/dist/server/index.d.ts +0 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +7 -40
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.js +10 -6
- package/dist/server/internal.js.map +1 -1
- package/dist/server/logger.d.ts +1 -0
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +20 -6
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/request-context.d.ts +27 -2
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-dispatcher.d.ts +6 -5
- package/dist/server/rsc-entry/action-dispatcher.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/ssr-bridge-types.d.ts +8 -0
- package/dist/server/ssr-bridge-types.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts +3 -0
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/docs/api/31-api-client.mdx +9 -3
- package/docs/learn/08-forms-and-actions.mdx +21 -28
- package/package.json +1 -1
- package/src/analyze/classify.ts +1 -1
- package/src/analyze/poison-scan.ts +1 -4
- package/src/client/browser-entry/action-dispatch.ts +74 -18
- package/src/client/browser-entry/hydrate.ts +18 -0
- package/src/client/browser-entry/router-init.ts +4 -18
- package/src/client/form.tsx +33 -98
- package/src/client/index.ts +2 -2
- package/src/client/navigation-api.ts +47 -173
- package/src/client/navigation-transition.ts +2 -2
- package/src/client/router-lifecycle.ts +40 -20
- package/src/client/router-pipeline.ts +3 -9
- package/src/client/router-types.ts +14 -49
- package/src/client/router.ts +38 -58
- package/src/index.ts +21 -2
- package/src/plugins/client-dep-entries.ts +78 -0
- package/src/plugins/content.ts +26 -1
- package/src/plugins/entries.ts +7 -0
- package/src/plugins/shims.ts +0 -2
- package/src/rsc-runtime/rsc.ts +3 -0
- package/src/rsc-runtime/vendor-types.d.ts +14 -0
- package/src/server/action-client.ts +18 -20
- package/src/server/action-handler.ts +133 -95
- package/src/server/als-registry.ts +23 -9
- package/src/server/client-error-message.ts +18 -0
- package/src/server/form-state-embed.ts +63 -0
- package/src/server/index.ts +0 -4
- package/src/server/logger.ts +6 -1
- package/src/server/pipeline.ts +27 -8
- package/src/server/request-context.ts +39 -2
- package/src/server/route-element-builder.ts +12 -1
- package/src/server/rsc-entry/action-dispatcher.ts +37 -34
- package/src/server/rsc-entry/error-renderer.ts +2 -1
- package/src/server/rsc-entry/rsc-stream.ts +20 -11
- package/src/server/rsc-entry/ssr-renderer.ts +14 -1
- package/src/server/ssr-bridge-types.ts +9 -0
- package/src/server/ssr-entry.ts +1 -0
- package/src/server/ssr-render.ts +5 -0
- package/dist/_chunks/actions-Rjk4htmA.js.map +0 -1
- package/dist/_chunks/als-registry-DaxkVjt5.js.map +0 -1
- package/dist/_chunks/chains-BoO51joc.js.map +0 -1
- package/dist/_chunks/poison-scan-vGV7Re0B.js.map +0 -1
- package/dist/server/form-flash.d.ts +0 -78
- package/dist/server/form-flash.d.ts.map +0 -1
- package/src/server/form-flash.ts +0 -89
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-schema-sync-D2eI8jEg.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-czh2dsLs.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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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { i as scanProjectSourceFiles, r as normalizeSlashes } from "./scan-BoP17W3w.js";
|
|
2
|
+
import { i as detectDirective, t as PARSEABLE_EXTENSIONS } from "./classify-BT66U83D.js";
|
|
3
|
+
import { sep } from "node:path";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
//#region src/plugins/client-dep-entries.ts
|
|
6
|
+
/**
|
|
7
|
+
* client-dep-entries — seed the client dep optimizer's startup scan with
|
|
8
|
+
* the app's `'use client'` modules.
|
|
9
|
+
*
|
|
10
|
+
* Vite's dev optimizer discovers bare imports by crawling from its
|
|
11
|
+
* entries before the first request. With no `optimizeDeps.entries`, the
|
|
12
|
+
* client environment crawls its build input — timber's browser entry —
|
|
13
|
+
* and nothing more: every `'use client'` module is reached only through
|
|
14
|
+
* the RSC payload at runtime, never through a static import the crawl
|
|
15
|
+
* can follow. A package imported only from client components is then
|
|
16
|
+
* found on the first browser request, re-optimized mid-load, and the
|
|
17
|
+
* page full-reloads ("optimized dependencies changed. reloading"). That
|
|
18
|
+
* reload can leave react and react-dom from two optimizer runs in the
|
|
19
|
+
* browser (duplicate React → "Invalid hook call").
|
|
20
|
+
*
|
|
21
|
+
* Listing the `'use client'` files as entries lets the startup crawl see
|
|
22
|
+
* those imports. Setting `entries` replaces the build-input default, so
|
|
23
|
+
* the browser entry is listed too. Files that gain the directive after
|
|
24
|
+
* startup still fall back to Vite's late discovery.
|
|
25
|
+
*
|
|
26
|
+
* Design docs: 21-dev-server.md §"Client Dependency Pre-Bundling".
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Every project source file under `root` whose directive prologue holds
|
|
30
|
+
* `'use client'`. Uses the same walker as `timber graph` (node_modules,
|
|
31
|
+
* root-level dot dirs and build outputs skipped) and the same AST
|
|
32
|
+
* directive check the RSC plugin applies. Files under `excludeDir` (the
|
|
33
|
+
* build output) are skipped.
|
|
34
|
+
*/
|
|
35
|
+
async function findUseClientFiles(root, excludeDir) {
|
|
36
|
+
const files = [];
|
|
37
|
+
await scanProjectSourceFiles(root, files, () => {}, 0, PARSEABLE_EXTENSIONS);
|
|
38
|
+
const excludePrefix = excludeDir ? excludeDir + sep : void 0;
|
|
39
|
+
const candidates = excludePrefix ? files.filter((f) => !f.startsWith(excludePrefix)) : files;
|
|
40
|
+
return (await Promise.all(candidates.map(async (file) => {
|
|
41
|
+
const code = await readFile(file, "utf8").catch(() => null);
|
|
42
|
+
if (code === null || !code.includes("use client")) return null;
|
|
43
|
+
return detectDirective(code, file) === "use client" ? file : null;
|
|
44
|
+
}))).filter((file) => file !== null);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Escape a path for use as an `optimizeDeps.entries` pattern. Vite globs
|
|
48
|
+
* entries, and route directories are full of glob syntax: `(group)`,
|
|
49
|
+
* `[slug]`, `[[...catchAll]]`, `@slot`. Slashes are normalized first, so
|
|
50
|
+
* the only backslashes left are literal (POSIX) ones, which are escaped
|
|
51
|
+
* too.
|
|
52
|
+
*/
|
|
53
|
+
function escapeGlobPath(path) {
|
|
54
|
+
return normalizeSlashes(path).replace(/[\\()[\]{}*?|!+@]/g, "\\$&");
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The client environment's `optimizeDeps.entries`: timber's browser
|
|
58
|
+
* entry plus every `'use client'` file in the project.
|
|
59
|
+
*/
|
|
60
|
+
async function clientOptimizeDepsEntries(root, browserEntryFile, buildDir) {
|
|
61
|
+
return [browserEntryFile, ...await findUseClientFiles(root, buildDir)].map(escapeGlobPath);
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
export { clientOptimizeDepsEntries };
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=client-dep-entries-CQwpb8dI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-dep-entries-CQwpb8dI.js","names":[],"sources":["../../src/plugins/client-dep-entries.ts"],"sourcesContent":["/**\n * client-dep-entries — seed the client dep optimizer's startup scan with\n * the app's `'use client'` modules.\n *\n * Vite's dev optimizer discovers bare imports by crawling from its\n * entries before the first request. With no `optimizeDeps.entries`, the\n * client environment crawls its build input — timber's browser entry —\n * and nothing more: every `'use client'` module is reached only through\n * the RSC payload at runtime, never through a static import the crawl\n * can follow. A package imported only from client components is then\n * found on the first browser request, re-optimized mid-load, and the\n * page full-reloads (\"optimized dependencies changed. reloading\"). That\n * reload can leave react and react-dom from two optimizer runs in the\n * browser (duplicate React → \"Invalid hook call\").\n *\n * Listing the `'use client'` files as entries lets the startup crawl see\n * those imports. Setting `entries` replaces the build-input default, so\n * the browser entry is listed too. Files that gain the directive after\n * startup still fall back to Vite's late discovery.\n *\n * Design docs: 21-dev-server.md §\"Client Dependency Pre-Bundling\".\n */\n\nimport { readFile } from 'node:fs/promises';\nimport { sep } from 'node:path';\nimport { detectDirective, PARSEABLE_EXTENSIONS } from '../analyze/classify.ts';\nimport { normalizeSlashes, scanProjectSourceFiles } from '../analyze/scan.ts';\n\n/**\n * Every project source file under `root` whose directive prologue holds\n * `'use client'`. Uses the same walker as `timber graph` (node_modules,\n * root-level dot dirs and build outputs skipped) and the same AST\n * directive check the RSC plugin applies. Files under `excludeDir` (the\n * build output) are skipped.\n */\nexport async function findUseClientFiles(root: string, excludeDir?: string): Promise<string[]> {\n const files: string[] = [];\n // An unreadable directory only means its client deps are discovered\n // late, through Vite's normal fallback — never fatal for dev startup.\n await scanProjectSourceFiles(root, files, () => {}, 0, PARSEABLE_EXTENSIONS);\n const excludePrefix = excludeDir ? excludeDir + sep : undefined;\n const candidates = excludePrefix ? files.filter((f) => !f.startsWith(excludePrefix)) : files;\n\n const matches = await Promise.all(\n candidates.map(async (file) => {\n const code = await readFile(file, 'utf8').catch(() => null);\n // Cheap text gate before parsing: a directive is a literal\n // without escapes, so a file lacking the text cannot carry it.\n if (code === null || !code.includes('use client')) return null;\n return detectDirective(code, file) === 'use client' ? file : null;\n })\n );\n return matches.filter((file): file is string => file !== null);\n}\n\n/**\n * Escape a path for use as an `optimizeDeps.entries` pattern. Vite globs\n * entries, and route directories are full of glob syntax: `(group)`,\n * `[slug]`, `[[...catchAll]]`, `@slot`. Slashes are normalized first, so\n * the only backslashes left are literal (POSIX) ones, which are escaped\n * too.\n */\nexport function escapeGlobPath(path: string): string {\n return normalizeSlashes(path).replace(/[\\\\()[\\]{}*?|!+@]/g, '\\\\$&');\n}\n\n/**\n * The client environment's `optimizeDeps.entries`: timber's browser\n * entry plus every `'use client'` file in the project.\n */\nexport async function clientOptimizeDepsEntries(\n root: string,\n browserEntryFile: string,\n buildDir: string\n): Promise<string[]> {\n const clientFiles = await findUseClientFiles(root, buildDir);\n return [browserEntryFile, ...clientFiles].map(escapeGlobPath);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,eAAsB,mBAAmB,MAAc,YAAwC;CAC7F,MAAM,QAAkB,CAAC;CAGzB,MAAM,uBAAuB,MAAM,aAAa,CAAC,GAAG,GAAG,oBAAoB;CAC3E,MAAM,gBAAgB,aAAa,aAAa,MAAM,KAAA;CACtD,MAAM,aAAa,gBAAgB,MAAM,QAAQ,MAAM,CAAC,EAAE,WAAW,aAAa,CAAC,IAAI;CAWvF,QAAO,MATe,QAAQ,IAC5B,WAAW,IAAI,OAAO,SAAS;EAC7B,MAAM,OAAO,MAAM,SAAS,MAAM,MAAM,CAAC,CAAC,YAAY,IAAI;EAG1D,IAAI,SAAS,QAAQ,CAAC,KAAK,SAAS,YAAY,GAAG,OAAO;EAC1D,OAAO,gBAAgB,MAAM,IAAI,MAAM,eAAe,OAAO;CAC/D,CAAC,CACH,EAAA,CACe,QAAQ,SAAyB,SAAS,IAAI;AAC/D;;;;;;;;AASA,SAAgB,eAAe,MAAsB;CACnD,OAAO,iBAAiB,IAAI,CAAC,CAAC,QAAQ,sBAAsB,MAAM;AACpE;;;;;AAMA,eAAsB,0BACpB,MACA,kBACA,UACmB;CAEnB,OAAO,CAAC,kBAAkB,GAAG,MADH,mBAAmB,MAAM,QAAQ,CACnB,CAAC,CAAC,IAAI,cAAc;AAC9D"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as writeFileAtomic } from "./write-file-atomic-D1qWqHb4.js";
|
|
2
|
-
import { a as fileHasDefaultExport, c as getCsrfExport, i as fileHasAnyExport, o as fileHasDirective, r as fileDefaultExportIsLocalValue, s as fileHasStarExport } from "./scanner-
|
|
3
|
-
import { v as swallow } from "./logger-
|
|
2
|
+
import { a as fileHasDefaultExport, c as getCsrfExport, i as fileHasAnyExport, o as fileHasDirective, r as fileDefaultExportIsLocalValue, s as fileHasStarExport } from "./scanner-CQt12vE2.js";
|
|
3
|
+
import { v as swallow } from "./logger-CbLdcy-W.js";
|
|
4
4
|
import { t as generateRouteMap } from "./codegen-Bps1sLKJ.js";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
@@ -587,4 +587,4 @@ function formatConventionWarnings(warnings, options = {}) {
|
|
|
587
587
|
//#endregion
|
|
588
588
|
export { ENV_DTS_FILENAME as a, buildDirToRouteMap as c, CODEGEN_OUTPUT as i, formatConventionWarnings as n, flushCodegenWrites as o, lintConventions as r, writeRouteCodegen as s, checkAppDirExists as t };
|
|
589
589
|
|
|
590
|
-
//# sourceMappingURL=convention-lint-
|
|
590
|
+
//# sourceMappingURL=convention-lint-BEVW4EID.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"convention-lint-DLmhGsRS.js","names":[],"sources":["../../src/routing/segment-codegen.ts","../../src/routing/codegen-write.ts","../../src/routing/convention-lint.ts"],"sourcesContent":["/**\n * $segment type generation and directory-to-route mapping.\n *\n * Generates per-route $segment.d.ts files in .timber/types/ that mirror\n * the app/ directory structure. TypeScript's rootDirs feature makes these\n * importable as './$segment' from route files.\n *\n * Design doc: design/41-global-params.md §The $segment Module\n */\n\nimport { mkdir } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { writeFileAtomic } from '../utils/write-file-atomic.ts';\nimport type { RouteTree, SegmentNode } from './types.ts';\n\n/** Info carried per directory for type generation. */\nexport interface SegmentInfo {\n /** The URL route pattern (groups and slots stripped). e.g. '/[artistSlug]/[year]' */\n route: string;\n /** The full tree path including groups and slots — uniquely identifies a segment.\n * e.g. '/(browse)/[artistSlug]/@shows/[year]' */\n segmentPath: string;\n}\n\n/**\n * Build a map of absolute filesystem directory → segment info.\n * Used by resolveId to map a $segment import's importer to the correct route,\n * and by writeSegmentTypes to generate per-route $segment.d.ts files.\n *\n * Walks every segment (not just leaves) so layouts and middleware\n * at any depth can import $segment.\n */\nexport function buildDirToRouteMap(tree: RouteTree, appDir: string): Map<string, SegmentInfo> {\n const map = new Map<string, SegmentInfo>();\n\n function walk(node: SegmentNode, fsPath: string, treePath: string): void {\n map.set(fsPath, {\n route: node.urlPath || '/',\n segmentPath: treePath || '/',\n });\n\n for (const child of node.children) {\n const childDir = join(fsPath, child.segmentName);\n const childTreePath = `${treePath}/${child.segmentName}`;\n walk(child, childDir, childTreePath);\n }\n for (const [slotName, slotNode] of Object.entries(node.slots)) {\n const slotDir = join(fsPath, `@${slotName}`);\n const slotTreePath = `${treePath}/@${slotName}`;\n walk(slotNode, slotDir, slotTreePath);\n }\n }\n\n walk(tree.root, appDir, '');\n return map;\n}\n\n/**\n * Generate the full $segment.d.ts content for a segment.\n */\nexport function generateSegmentDts(info: SegmentInfo): string {\n return [\n `export const ROUTE: '${info.route}';`,\n `export const SEGMENT_PATH: '${info.segmentPath}';`,\n '',\n ].join('\\n');\n}\n\n/**\n * Generate per-route $segment.d.ts files in .timber/types/ mirroring\n * the app/ directory structure. These files are importable via TypeScript's\n * rootDirs feature.\n */\nexport async function writeSegmentTypes(\n routeTree: RouteTree,\n appDir: string,\n root: string\n): Promise<void> {\n // Compute the relative prefix from project root to appDir.\n // e.g. if appDir is /project/src/app, relative is 'src/app'\n const relative = appDir.slice(root.length + 1);\n const typesBase = join(root, '.timber', 'types', relative);\n const map = buildDirToRouteMap(routeTree, appDir);\n\n const writes: Promise<void>[] = [];\n for (const [fsDir, info] of map) {\n // Mirror the app/ structure under .timber/types/\n const relDir = fsDir.slice(appDir.length); // e.g. '/[artistSlug]/[year]'\n const segmentDir = join(typesBase, relDir);\n const segmentFile = join(segmentDir, '$segment.d.ts');\n const content = generateSegmentDts(info);\n\n writes.push(\n mkdir(segmentDir, { recursive: true }).then(() => writeFileAtomic(segmentFile, content))\n );\n }\n\n await Promise.all(writes);\n}\n","/**\n * Route-map codegen writer.\n *\n * Materializes the generated route types on disk: `.timber/timber-routes.d.ts`,\n * `.timber/segment-types.d.ts` (via `writeSegmentTypes`), and the root\n * `timber-env.d.ts` that references them.\n *\n * Lives outside `plugins/routing.ts` because it has two callers with two\n * different failure policies: the dev/build plugin starts the write off the hot\n * path and logs failures, while `timber check` awaits it and treats a failure as\n * fatal — a type check run against absent or stale route types reports errors\n * about code that is fine. One implementation, two policies, so the bytes on\n * disk cannot drift between the two commands.\n *\n * Design docs: 09-typescript.md §\"Typed Routes\", 18-build-system.md\n */\n\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { generateRouteMap } from './codegen.ts';\nimport { writeSegmentTypes } from './segment-codegen.ts';\nimport { writeFileAtomic } from '../utils/write-file-atomic.ts';\nimport type { RouteTree } from './types.ts';\n\n/**\n * Path (relative to the project root) of the generated route map declaration\n * file, and the name of the root file that `/// <reference>`s it.\n *\n * Exported because `timber check` has to answer whether the user's tsconfig\n * pulls these into the program — a question about these exact paths. It derives\n * them from here rather than restating them, so a rename cannot leave the check\n * looking for a file nothing writes.\n */\nexport const CODEGEN_OUTPUT = '.timber/timber-routes.d.ts';\n\n/** Root-level file that references the generated declarations. */\nexport const ENV_DTS_FILENAME = 'timber-env.d.ts';\n\n/**\n * Content of the timber-env.d.ts file written to the project root.\n *\n * Uses a triple-slash reference so TypeScript auto-discovers the generated\n * route map without the user needing to edit tsconfig.json — same pattern\n * as Next.js's next-env.d.ts.\n *\n * Also references `vite/client` so side-effect imports like\n * `import './globals.css'` type-check without ts(2882), and so\n * `import.meta.env`, `import.meta.hot`, `import.meta.glob`, asset\n * imports (`*.png`, `*.svg`, …), CSS modules, and `?url`/`?raw`/\n * `?inline` query variants all resolve without the user editing\n * tsconfig.json themselves. Next.js's next-env.d.ts plays the same\n * role via `next/types/global`.\n *\n * `vite` is a peer dep of `@timber-js/app`, so the reference always\n * resolves in a correctly-installed project.\n */\nconst TIMBER_ENV_DTS = [\n '// This file is auto-generated by timber.js. Do not edit.',\n '// It ensures TypeScript picks up the generated route types and',\n '// Vite-provided ambient types (CSS, assets, import.meta.env, HMR).',\n '/* eslint-disable @typescript-eslint/triple-slash-reference */',\n '/// <reference path=\".timber/timber-routes.d.ts\" />',\n '/// <reference types=\"vite/client\" />',\n '/// <reference types=\"react/canary\" />',\n '/// <reference types=\"react-dom/canary\" />',\n '',\n].join('\\n');\n\n/**\n * Codegen writes that have been started but have not yet landed on disk.\n *\n * `writeRouteCodegen` is deliberately unawaited on the dev/build hot path,\n * which leaves that caller no way to know the files exist yet. Anything that\n * tears down the project root — a build process exiting, a test removing its\n * temp root — then races the writes: `rmSync` empties a directory that a\n * pending `mkdir` immediately recreates, and the removal fails with ENOTEMPTY.\n *\n * Every started write registers here and deregisters when it settles, so\n * `flushCodegenWrites()` can await them instead of guessing at a delay.\n */\nconst pendingCodegenWrites = new Set<Promise<void>>();\n\n/**\n * Resolve once every codegen write started so far has landed on disk.\n *\n * Safe to call at any time and from anywhere — writes are tracked per process,\n * not per plugin instance, so a caller does not need a handle on the plugin\n * that started them. Never rejects: the tracked copy of each write has its\n * rejection swallowed, so a failing write cannot turn a teardown barrier into\n * an unhandled rejection. Callers that care about failure await the promise\n * `writeRouteCodegen` returned instead.\n */\nexport async function flushCodegenWrites(): Promise<void> {\n // Loop rather than a single Promise.all: a write started while we were\n // awaiting would otherwise be missed, and the set would still be non-empty\n // on return.\n //\n // Each round drops what it awaited rather than trusting the per-write\n // `finally` to have run, so the two removal paths are independent — a\n // deregistration that stopped firing would leak memory in dev, but it can\n // never turn this into a spin.\n while (pendingCodegenWrites.size > 0) {\n const inFlight = [...pendingCodegenWrites];\n await Promise.all(inFlight);\n for (const write of inFlight) pendingCodegenWrites.delete(write);\n }\n}\n\n/**\n * Write the generated route map, segment types, and the timber-env.d.ts\n * reference file for `tree`.\n *\n * Returns a promise that **rejects** if any write fails. The dev/build plugin\n * does not await it (types are a developer convenience there, not a runtime\n * dependency) and logs instead; `timber check` awaits it and fails the command.\n * Either way the write is registered with `flushCodegenWrites()`.\n */\n/**\n * Write `timber-env.d.ts` only when its content differs.\n *\n * Its content is constant per timber version, so almost every call is a no-op\n * — and a file that is not rewritten cannot be read half-written. It is not\n * replaced atomically because it lives in the user's project root, often\n * committed: a rename would turn a symlink into a regular file, reset its\n * permissions, and a process killed mid-write would leave a temp file next to\n * their source. The in-place write happens only on first run or a timber\n * upgrade.\n */\nasync function writeEnvDts(envPath: string): Promise<void> {\n const existing = await readFile(envPath, 'utf-8').catch(() => null);\n if (existing !== TIMBER_ENV_DTS) await writeFile(envPath, TIMBER_ENV_DTS, 'utf-8');\n}\n\nexport function writeRouteCodegen(tree: RouteTree, appDir: string, root: string): Promise<void> {\n const timberDir = join(root, '.timber');\n const content = generateRouteMap(tree, { appDir, outputDir: timberDir });\n const routesPath = join(root, CODEGEN_OUTPUT);\n const envPath = join(root, ENV_DTS_FILENAME);\n\n const done = mkdir(timberDir, { recursive: true })\n .then(() =>\n // The type checker can read these while they are being rewritten — an\n // editor's TS server during `timber dev`, a `tsgo` run during `vite\n // build`. A truncating write shows it an empty route map for the length\n // of the write, and every typed route degrades to `any` (TIM-1562).\n // The generated files under `.timber/` are replaced atomically.\n Promise.all([\n writeFileAtomic(routesPath, content),\n writeEnvDts(envPath),\n writeSegmentTypes(tree, appDir, root),\n ])\n )\n .then(() => undefined);\n\n // The tracked copy swallows rejection so `flushCodegenWrites` — a teardown\n // barrier — never rejects and never leaves an unhandled rejection behind.\n // The returned promise keeps its rejection for callers that want it.\n const tracked = done.catch(() => undefined);\n pendingCodegenWrites.add(tracked);\n void tracked.finally(() => pendingCodegenWrites.delete(tracked));\n\n return done;\n}\n","/**\n * Convention linter — validates common misconfigurations in the route tree.\n *\n * Runs at scan time (build and dev startup). Each check produces a warning\n * with the file path, what's wrong, and what to do about it.\n *\n * Most checks are warnings — they don't block the build. The goal is to catch\n * issues that would otherwise produce cryptic runtime behavior (silent 404s,\n * empty pages, confusing React errors).\n *\n * A check emits `level: 'error'` only when the misconfiguration is *proven*\n * (never inferred from an absence the analysis cannot see through) and fatal\n * to rendering. `plugins/routing.ts` turns those into a thrown build error,\n * so a false positive there refuses a working app — see\n * `classifyClientBoundaryFile` for how that bar is met.\n *\n * Design doc: 07-routing.md, 10-error-handling.md\n */\n\nimport { existsSync } from 'node:fs';\nimport type { RouteFile, RouteTree, SegmentNode } from './types.ts';\nimport { swallow } from '../server/logger.ts';\nimport {\n fileDefaultExportIsLocalValue,\n fileHasAnyExport,\n fileHasDefaultExport,\n fileHasDirective,\n fileHasStarExport,\n getCsrfExport,\n} from './export-detect.ts';\n\n// ─── Types ──────────────────────────────────────────────────────────────────\n\nexport interface ConventionWarning {\n /** Warning ID for deduplication and filtering. */\n id: string;\n /** Human-readable single-line summary. */\n summary: string;\n /** Multi-line details with file path and fix suggestion. */\n details: string;\n /** Severity: 'warn' for potential issues, 'error' for definite misconfigurations. */\n level: 'warn' | 'error';\n}\n\n// ─── Lint Rules ─────────────────────────────────────────────────────────────\n\n/**\n * Run all convention lint checks on a route tree.\n *\n * Returns an array of warnings. Empty array means everything looks good.\n */\nexport function lintConventions(tree: RouteTree, appDir: string): ConventionWarning[] {\n const warnings: ConventionWarning[] = [];\n\n // Check 1: app/ directory exists (caller should check before scanning,\n // but we validate the root has at least one routable file)\n checkEmptyApp(tree.root, appDir, warnings);\n\n // Check 2: route.ts without recognized HTTP method exports\n checkRouteExports(tree.root, warnings);\n\n // Check 3: Segments with layout but no page/route anywhere in subtree\n // (not a misconfiguration per se — layouts without pages are valid for\n // route groups. Skip this check.)\n\n // Check 4: Root segment has no layout.tsx (no HTML shell)\n checkRootLayout(tree.root, warnings);\n\n // Check 5: page.tsx / layout.tsx without default export\n checkDefaultExports(tree.root, warnings);\n\n // Check 6: Client layouts cannot suppress server children before Flight\n // serializes them. They remain valid UI boundaries, but not authorization\n // or data-hiding boundaries.\n checkClientLayouts(tree.root, warnings);\n\n // Check 7: error.tsx / 404.tsx / 4xx.tsx / … without 'use client' directive\n checkClientBoundaryFiles(tree.root, warnings);\n if (tree.globalError) {\n checkFileUseClientDirective(tree.globalError, 'global-error', warnings);\n }\n\n // Check 8: middleware.ts inside a parallel slot subtree (silently dead)\n checkSlotMiddleware(tree.root, warnings);\n\n // Check 9: `export const csrf` anywhere but route.ts, or in a form the\n // scanner cannot read (TIM-1543)\n checkCsrfExports(tree.root, warnings);\n if (tree.proxy) checkCsrfExport(tree.proxy, 'proxy.ts', warnings);\n if (tree.globalError) checkCsrfExport(tree.globalError, 'global-error', warnings);\n\n return warnings;\n}\n\nfunction checkClientLayouts(node: SegmentNode, warnings: ConventionWarning[]): void {\n if (node.layout && isScriptExtension(node.layout.extension)) {\n try {\n const isClient = fileHasDirective(node.layout.filePath, 'use client');\n const isProvablyServer = fileDefaultExportIsLocalValue(node.layout.filePath);\n if (isClient || !isProvablyServer) {\n const certainty = isClient\n ? `This layout is a client component.`\n : `Timber cannot prove this layout is a server component because its default export comes from another module or an opaque expression.`;\n warnings.push({\n id: `CLIENT_LAYOUT_NOT_AUTH_BOUNDARY:${node.layout.filePath}`,\n summary: isClient\n ? `Client layout cannot securely suppress server children: ${node.layout.filePath}`\n : `Layout may resolve to a client component and cannot be trusted as an auth boundary: ${node.layout.filePath}`,\n details:\n ` File: ${node.layout.filePath}\\n\\n` +\n ` ${certainty}\\n` +\n ` React Flight resolves server content passed to a client layout before the layout runs in the browser.\\n` +\n ` Conditionally omitting children here does not prevent descendant server components from executing or being serialized.\\n\\n` +\n ` To fix: Put authorization in access.ts or a provably server layout with a locally defined default component, and keep client components inside that boundary.\\n`,\n level: 'warn',\n });\n }\n } catch (err) {\n swallow(err, `convention-lint: unreadable layout file ${node.layout.filePath}`);\n }\n }\n for (const child of node.children) checkClientLayouts(child, warnings);\n for (const slot of Object.values(node.slots)) checkClientLayouts(slot, warnings);\n}\n\n// ─── Check: CSRF Exemption Export ──────────────────────────────────────────\n\n/** Single-file segment conventions other than route.ts. */\nconst NON_ROUTE_FILE_KEYS = [\n 'page',\n 'layout',\n 'middleware',\n 'access',\n 'error',\n 'default',\n 'denied',\n] as const satisfies readonly (keyof SegmentNode)[];\n\n/**\n * Fail the build on a `csrf` export the CSRF gate would not honor.\n *\n * Both findings are proven from source, and both are fatal in the sense\n * the error level requires: the developer wrote an exemption that silently\n * does nothing, so a legitimate cross-site POST they meant to accept gets\n * a 403 in production, or they believe a page or action is exempt when\n * the gate still protects it. The exemption exists only on route.ts —\n * server actions always get the check (design/08 §\"Per-route exemption\").\n * A `'use server'` module cannot export it at all: plugin-rsc rejects every\n * non-async-function export there.\n */\nfunction checkCsrfExports(node: SegmentNode, warnings: ConventionWarning[]): void {\n if (node.route) checkCsrfExport(node.route, 'route.ts', warnings);\n for (const key of NON_ROUTE_FILE_KEYS) {\n const file = node[key];\n if (file) checkCsrfExport(file, key, warnings);\n }\n // Status files and metadata routes (sitemap.ts, opengraph-image.tsx, …).\n // JSON status files are skipped by the script-extension check.\n for (const [name, file] of Object.entries(node.statusFiles ?? {})) {\n checkCsrfExport(file, name, warnings);\n }\n for (const [name, file] of Object.entries(node.metadataRoutes ?? {})) {\n checkCsrfExport(file, name, warnings);\n }\n for (const child of node.children) checkCsrfExports(child, warnings);\n for (const slot of Object.values(node.slots)) checkCsrfExports(slot, warnings);\n}\n\nfunction checkCsrfExport(file: RouteFile, convention: string, warnings: ConventionWarning[]): void {\n if (!isScriptExtension(file.extension)) return;\n const found = getCsrfExport(file.filePath);\n if (found === 'absent') return;\n const isRoute = convention === 'route.ts';\n if (isRoute && found === 'exempt') return;\n warnings.push({\n id: `CSRF_EXPORT_INVALID:${file.filePath}`,\n summary: isRoute\n ? `route.ts csrf export must be exactly \\`export const csrf = false\\`: ${file.filePath}`\n : `\\`csrf\\` can only be exported from route.ts, not ${convention}: ${file.filePath}`,\n details:\n ` File: ${file.filePath}\\n\\n` +\n (isRoute\n ? ' The CSRF gate reads this export from source before any module loads, so only\\n' +\n ' the literal form counts. Other values, `let`, and re-exports are not read.\\n\\n' +\n ' To fix: write `export const csrf = false;` in this file, or remove the export.\\n'\n : ' The per-route CSRF exemption applies only to route.ts handlers. Pages, layouts\\n' +\n ' and server actions always get the CSRF check.\\n\\n' +\n ' To fix: remove the export. To accept a cross-site POST, handle it in a route.ts\\n' +\n ' that exports `const csrf = false`.\\n'),\n level: 'error',\n });\n}\n\n// ─── Check: Empty App ───────────────────────────────────────────────────────\n\n/**\n * Warn when the app/ directory has no routable files at all.\n *\n * This catches the \"I created app/ but didn't add any pages\" case where\n * every request silently 404s with no guidance.\n */\nfunction checkEmptyApp(root: SegmentNode, appDir: string, warnings: ConventionWarning[]): void {\n if (hasAnyRoutable(root)) return;\n\n warnings.push({\n id: 'EMPTY_APP',\n summary: 'No pages or route handlers found in app/',\n details:\n ` Directory: ${appDir}\\n\\n` +\n ' Your app/ directory has no page.tsx or route.ts files.\\n' +\n ' Every request will return 404.\\n\\n' +\n ' To fix: Create app/page.tsx with a default export:\\n\\n' +\n ' export default function Home() {\\n return <h1>Hello</h1>;\\n }\\n',\n level: 'warn',\n });\n}\n\n/**\n * Check if a segment tree has any routable files (page or route) anywhere.\n */\nfunction hasAnyRoutable(node: SegmentNode): boolean {\n if (node.page || node.route) return true;\n for (const child of node.children) {\n if (hasAnyRoutable(child)) return true;\n }\n for (const slot of Object.values(node.slots)) {\n if (hasAnyRoutable(slot)) return true;\n }\n return false;\n}\n\n/**\n * Check if a segment tree has any page files (not just route handlers).\n * Used by checkRootLayout to avoid false positives for API-only apps (TIM-794).\n * API-only apps (just route.ts handlers) don't need a root layout.\n */\nfunction hasAnyPage(node: SegmentNode): boolean {\n if (node.page) return true;\n for (const child of node.children) {\n if (hasAnyPage(child)) return true;\n }\n for (const slot of Object.values(node.slots)) {\n if (hasAnyPage(slot)) return true;\n }\n return false;\n}\n\n// ─── Check: Route Exports ───────────────────────────────────────────────────\n\n/** HTTP methods that route.ts can export. */\nconst HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'] as const;\n\n/**\n * Warn when a route.ts file doesn't appear to export any recognized HTTP methods.\n *\n * Uses AST-based detection via parseAst for precise matching.\n * Falls back silently when the file can't be parsed (syntax errors produce\n * no spurious warning — the TS compiler will catch those).\n */\nfunction checkRouteExports(node: SegmentNode, warnings: ConventionWarning[]): void {\n if (node.route) {\n const filePath = node.route.filePath;\n try {\n const methods = fileHasAnyExport(filePath, HTTP_METHODS);\n if (methods.size === 0 && !fileHasStarExport(filePath)) {\n const hasDefault = fileHasDefaultExport(filePath);\n const hint = hasDefault\n ? ' It looks like you have a default export. route.ts uses named exports\\n' +\n ' for HTTP methods (GET, POST, etc.), not a default export.\\n'\n : '';\n\n warnings.push({\n id: 'ROUTE_NO_METHODS',\n summary: `route.ts has no HTTP method exports: ${filePath}`,\n details:\n ` File: ${filePath}\\n\\n` +\n ' route.ts files must export named HTTP method handlers.\\n' +\n ' Without them, the route matches but returns 405 Method Not Allowed.\\n\\n' +\n hint +\n ' Example:\\n\\n' +\n ' export function GET(ctx: RouteContext) {\\n' +\n \" return Response.json({ hello: 'world' });\\n\" +\n ' }\\n',\n level: 'warn',\n });\n }\n } catch (err) {\n swallow(err, `convention-lint: unreadable route.ts ${filePath}`);\n }\n }\n\n // Recurse\n for (const child of node.children) {\n checkRouteExports(child, warnings);\n }\n for (const slot of Object.values(node.slots)) {\n checkRouteExports(slot, warnings);\n }\n}\n\n// ─── Check: Root Layout ─────────────────────────────────────────────────────\n\n/**\n * Warn when the root segment has no layout.tsx.\n *\n * Without a root layout, there's no HTML shell (<html>, <body>).\n * The page will render but may have hydration issues or no proper document structure.\n */\nfunction checkRootLayout(root: SegmentNode, warnings: ConventionWarning[]): void {\n if (root.layout) return;\n\n // Only warn if there are pages — API-only apps (just route.ts handlers)\n // don't need a root layout (TIM-794).\n if (!hasAnyPage(root)) return;\n\n warnings.push({\n id: 'NO_ROOT_LAYOUT',\n summary: 'No root layout.tsx found',\n details:\n ' Your app has pages but no root layout.\\n' +\n ' Without app/layout.tsx, pages have no <html> or <body> wrapper.\\n\\n' +\n ' To fix: Create app/layout.tsx:\\n\\n' +\n ' export default function RootLayout({ children }: { children: React.ReactNode }) {\\n' +\n ' return (\\n' +\n ' <html lang=\"en\">\\n' +\n ' <body>{children}</body>\\n' +\n ' </html>\\n' +\n ' );\\n' +\n ' }\\n',\n level: 'warn',\n });\n}\n\n// ─── Check: Default Exports ───────────────────────────────────────────\n\n/**\n * Warn when page.tsx or layout.tsx files don't have a default export.\n *\n * Without a default export:\n * - page.tsx: the page renders nothing (empty content)\n * - layout.tsx: the layout module loads but has no component to render\n *\n * Uses AST-based detection via parseAst for precise matching. Falls back\n * silently when the file can't be parsed (no spurious warning on syntax errors).\n */\nfunction checkDefaultExports(node: SegmentNode, warnings: ConventionWarning[]): void {\n // Check page files (tsx/ts/jsx/js only — not mdx, which is default-exported by the mdx compiler)\n if (node.page && isScriptExtension(node.page.extension)) {\n checkFileDefaultExport(node.page.filePath, 'page', warnings);\n }\n\n // Check layout files\n if (node.layout && isScriptExtension(node.layout.extension)) {\n checkFileDefaultExport(node.layout.filePath, 'layout', warnings);\n }\n\n // Recurse\n for (const child of node.children) {\n checkDefaultExports(child, warnings);\n }\n for (const slot of Object.values(node.slots)) {\n checkDefaultExports(slot, warnings);\n }\n}\n\nfunction isScriptExtension(ext: string): boolean {\n return ext === 'tsx' || ext === 'ts' || ext === 'jsx' || ext === 'js';\n}\n\nfunction checkFileDefaultExport(\n filePath: string,\n fileType: string,\n warnings: ConventionWarning[]\n): void {\n try {\n if (!fileHasDefaultExport(filePath)) {\n warnings.push({\n id: `NO_DEFAULT_EXPORT:${filePath}`,\n summary: `${fileType}.tsx has no default export: ${filePath}`,\n details:\n ` File: ${filePath}\\n\\n` +\n ` ${fileType}.tsx files must export a default React component.\\n` +\n ` Without a default export, the ${fileType === 'page' ? 'page renders nothing' : 'layout has no component to wrap children'}.\\n\\n` +\n ` To fix: Add a default export:\\n\\n` +\n ` export default function My${fileType === 'page' ? 'Page' : 'Layout'}(${fileType === 'layout' ? '{ children }' : ''}) {\\n` +\n ` return ${fileType === 'layout' ? '<div>{children}</div>' : '<h1>Hello</h1>'};\\n` +\n ` }\\n`,\n level: 'warn',\n });\n }\n } catch (err) {\n swallow(err, `convention-lint: unreadable ${fileType} file ${filePath}`);\n }\n}\n\n// ─── Check: Client Boundary Directive ───────────────────────────────────────\n\n/**\n * Check every file the renderer hands to `TimberErrorBoundary` as a component\n * prop: `error.tsx`, `global-error.tsx`, and the status-code family\n * (`404.tsx`, `4xx.tsx`, `503.tsx`, …).\n *\n * `TimberErrorBoundary` is a client component, so a server component passed as\n * `fallbackComponent` is a function crossing the RSC → client boundary. React\n * Flight refuses to serialize it, and because the boundary wraps the segment's\n * *children*, the failure hits every page under that segment — not just the\n * error path. The raw message (\"Functions cannot be passed directly to Client\n * Components\") names neither the file nor the fix (TIM-1329).\n *\n * `.json` status files never reach React at all, and are not in `statusFiles`.\n */\nfunction checkClientBoundaryFiles(node: SegmentNode, warnings: ConventionWarning[]): void {\n if (node.error) {\n checkFileUseClientDirective(node.error, 'error', warnings);\n }\n\n for (const [key, file] of Object.entries(node.statusFiles ?? {})) {\n checkFileUseClientDirective(file, key, warnings);\n }\n\n for (const child of node.children) {\n checkClientBoundaryFiles(child, warnings);\n }\n for (const slot of Object.values(node.slots)) {\n checkClientBoundaryFiles(slot, warnings);\n }\n}\n\n/**\n * `'client'` — carries the directive.\n * `'server'` — proven server component: no directive, and the default export is\n * a function or class built by this very module, so no other file's directive\n * can be governing it.\n * `'unknown'` — no directive, but the exported value may come from elsewhere\n * (`export { default } from './client-thing'`), or the file could not be\n * read or parsed. Never escalated to a build error.\n */\ntype ClientBoundaryVerdict = 'client' | 'server' | 'unknown';\n\nfunction classifyClientBoundaryFile(filePath: string): ClientBoundaryVerdict {\n if (fileHasDirective(filePath, 'use client')) return 'client';\n if (fileDefaultExportIsLocalValue(filePath)) return 'server';\n return 'unknown';\n}\n\n/** Example component name for the fix snippet, keyed by convention name. */\nfunction exampleComponentName(convention: string): string {\n if (convention === 'global-error') return 'GlobalError';\n if (convention === 'error') return 'ErrorBoundary';\n if (convention === '404') return 'NotFound';\n return 'StatusPage';\n}\n\n/**\n * @param file the route file (its extension decides whether the check applies)\n * @param convention the convention name — `error`, `global-error`, `404`, `4xx`, …\n */\nfunction checkFileUseClientDirective(\n file: RouteFile,\n convention: string,\n warnings: ConventionWarning[]\n): void {\n // Only script extensions need the 'use client' check — .json status files\n // never reach React at all, and are not in `statusFiles`.\n if (!isScriptExtension(file.extension)) return;\n\n const filePath = file.filePath;\n const label = `${convention}.${file.extension}`;\n let verdict: ClientBoundaryVerdict;\n try {\n verdict = classifyClientBoundaryFile(filePath);\n } catch (err) {\n swallow(err, `convention-lint: unreadable ${label} file ${filePath}`);\n return;\n }\n if (verdict === 'client') return;\n\n const isError = verdict === 'server';\n const name = exampleComponentName(convention);\n const props =\n convention === 'error' || convention === 'global-error' ? '{ error, reset }' : '{ status }';\n\n warnings.push({\n id: `NEEDS_USE_CLIENT:${filePath}`,\n summary: isError\n ? `${label} is a server component and must be a client component: ${filePath}`\n : `${label} may be missing 'use client': ${filePath}`,\n details:\n ` File: ${filePath}\\n\\n` +\n ` ${label} is handed to timber's error boundary — a client\\n` +\n ' component — as a prop. A server component cannot cross that boundary:\\n' +\n ' React fails with \"Functions cannot be passed directly to Client\\n' +\n ' Components\", and every page under this segment fails to render.\\n\\n' +\n (isError\n ? \" To fix: Add 'use client' as the first line of the file:\\n\\n\" +\n \" 'use client';\\n\\n\" +\n ` export default function ${name}(${props}) {\\n` +\n ' return <h1>…</h1>;\\n' +\n ' }\\n'\n : // The default export comes from somewhere this analysis cannot follow\n // — a re-export, an imported binding, a wrapper call. It may well\n // already be a client component, so say what was checked rather than\n // assert a break.\n \" This file has no 'use client' directive, and its default export\\n\" +\n ' comes from elsewhere, so timber cannot tell which it is. If the\\n' +\n \" module it re-exports starts with 'use client', nothing is wrong\\n\" +\n ' and you can ignore this. Otherwise add the directive there.\\n'),\n level: isError ? 'error' : 'warn',\n });\n}\n\n// ─── Check: Slot Middleware ─────────────────────────────────────────────────\n\n/**\n * Error when middleware.ts appears inside a parallel slot subtree.\n *\n * Middleware is collected from the main matched segment chain only\n * (route-matcher.ts `collectMiddlewareChain`). The slot resolver never\n * walks `middleware` on its matched chain, so a middleware.ts under any\n * `@slot/` directory silently never executes. `access.ts` in slot chains\n * DOES run (via SlotAccessGate), making the asymmetry invisible unless\n * you know to look.\n *\n * This is a build error rather than a warning: the file provably cannot\n * run and its presence misleads the author into thinking the slot is\n * middleware-protected.\n *\n * See design/07-routing.md §\"Parallel Routes\"\n */\nfunction checkSlotMiddleware(node: SegmentNode, warnings: ConventionWarning[]): void {\n for (const [slotName, slotRoot] of Object.entries(node.slots)) {\n collectSlotMiddleware(slotRoot, slotName, warnings);\n }\n for (const child of node.children) {\n checkSlotMiddleware(child, warnings);\n }\n}\n\n/**\n * Walk a slot subtree and report every middleware.ts found.\n */\nfunction collectSlotMiddleware(\n node: SegmentNode,\n slotName: string,\n warnings: ConventionWarning[]\n): void {\n if (node.middleware) {\n warnings.push({\n id: `SLOT_MIDDLEWARE:${node.middleware.filePath}`,\n summary: `middleware.ts inside slot @${slotName} never executes: ${node.middleware.filePath}`,\n details:\n ` File: ${node.middleware.filePath}\\n\\n` +\n ` Middleware is collected from the main route chain only.\\n` +\n ` The slot resolver does not run middleware — this file is silently dead.\\n` +\n ` (access.ts in slots does run via SlotAccessGate; middleware.ts does not.)\\n\\n` +\n ` To fix: Move the middleware logic into access.ts for slot-scoped auth,\\n` +\n ` or into a middleware.ts on the main route chain outside the slot.\\n`,\n level: 'error',\n });\n }\n for (const child of node.children) {\n collectSlotMiddleware(child, slotName, warnings);\n }\n for (const [nestedSlotName, nestedSlot] of Object.entries(node.slots)) {\n collectSlotMiddleware(nestedSlot, nestedSlotName, warnings);\n }\n}\n\n// ─── Check: App Directory Exists ────────────────────────────────────────────\n\n/**\n * Check if the app/ directory exists. Called before scanning.\n * Returns a warning if missing, or null if the directory exists.\n */\nexport function checkAppDirExists(appDir: string): ConventionWarning | null {\n if (existsSync(appDir)) return null;\n\n return {\n id: 'NO_APP_DIR',\n summary: 'No app/ directory found',\n details:\n ` Expected: ${appDir}\\n\\n` +\n ' timber.js requires an app/ directory for file-system routing.\\n' +\n ' Every request will return 404 until you create it.\\n\\n' +\n ' To fix: Create the app/ directory with a page:\\n\\n' +\n ' mkdir -p app\\n' +\n ' # Create app/layout.tsx and app/page.tsx\\n',\n level: 'error',\n };\n}\n\n// ─── Formatting ─────────────────────────────────────────────────────────────\n\nimport type { InspectColor } from 'node:util';\nimport { styleText } from 'node:util';\n\ntype Format = InspectColor | readonly InspectColor[];\nconst noValidate = { validateStream: false } as const;\n\nexport interface FormatOptions {\n /**\n * Emit ANSI escapes. Off when the text becomes an `Error` message —\n * whoever prints it decides its formatting, and colored escapes inside\n * an exception leak into logs and stack traces.\n */\n color?: boolean;\n}\n\n/**\n * Format warnings for terminal output.\n *\n * Groups by severity, uses colors, and includes fix suggestions.\n */\nexport function formatConventionWarnings(\n warnings: ConventionWarning[],\n options: FormatOptions = {}\n): string {\n const { color = true } = options;\n const style = (fmt: Format, text: string) => (color ? styleText(fmt, text, noValidate) : text);\n if (warnings.length === 0) return '';\n\n const errors = warnings.filter((w) => w.level === 'error');\n const warns = warnings.filter((w) => w.level === 'warn');\n\n const lines: string[] = [];\n\n if (errors.length > 0) {\n lines.push(\n `${style(['red', 'bold'], '[timber]')} ${style('red', `${errors.length} configuration error${errors.length !== 1 ? 's' : ''}:`)}`\n );\n for (const e of errors) {\n lines.push('');\n lines.push(` ${style('red', '✗')} ${e.summary}`);\n lines.push(style('dim', e.details));\n }\n }\n\n if (warns.length > 0) {\n if (errors.length > 0) lines.push('');\n lines.push(\n `${style(['yellow', 'bold'], '[timber]')} ${style('yellow', `${warns.length} configuration warning${warns.length !== 1 ? 's' : ''}:`)}`\n );\n for (const w of warns) {\n lines.push('');\n lines.push(` ${style('yellow', '⚠')} ${w.summary}`);\n lines.push(style('dim', w.details));\n }\n }\n\n return lines.join('\\n');\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"convention-lint-BEVW4EID.js","names":[],"sources":["../../src/routing/segment-codegen.ts","../../src/routing/codegen-write.ts","../../src/routing/convention-lint.ts"],"sourcesContent":["/**\n * $segment type generation and directory-to-route mapping.\n *\n * Generates per-route $segment.d.ts files in .timber/types/ that mirror\n * the app/ directory structure. TypeScript's rootDirs feature makes these\n * importable as './$segment' from route files.\n *\n * Design doc: design/41-global-params.md §The $segment Module\n */\n\nimport { mkdir } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { writeFileAtomic } from '../utils/write-file-atomic.ts';\nimport type { RouteTree, SegmentNode } from './types.ts';\n\n/** Info carried per directory for type generation. */\nexport interface SegmentInfo {\n /** The URL route pattern (groups and slots stripped). e.g. '/[artistSlug]/[year]' */\n route: string;\n /** The full tree path including groups and slots — uniquely identifies a segment.\n * e.g. '/(browse)/[artistSlug]/@shows/[year]' */\n segmentPath: string;\n}\n\n/**\n * Build a map of absolute filesystem directory → segment info.\n * Used by resolveId to map a $segment import's importer to the correct route,\n * and by writeSegmentTypes to generate per-route $segment.d.ts files.\n *\n * Walks every segment (not just leaves) so layouts and middleware\n * at any depth can import $segment.\n */\nexport function buildDirToRouteMap(tree: RouteTree, appDir: string): Map<string, SegmentInfo> {\n const map = new Map<string, SegmentInfo>();\n\n function walk(node: SegmentNode, fsPath: string, treePath: string): void {\n map.set(fsPath, {\n route: node.urlPath || '/',\n segmentPath: treePath || '/',\n });\n\n for (const child of node.children) {\n const childDir = join(fsPath, child.segmentName);\n const childTreePath = `${treePath}/${child.segmentName}`;\n walk(child, childDir, childTreePath);\n }\n for (const [slotName, slotNode] of Object.entries(node.slots)) {\n const slotDir = join(fsPath, `@${slotName}`);\n const slotTreePath = `${treePath}/@${slotName}`;\n walk(slotNode, slotDir, slotTreePath);\n }\n }\n\n walk(tree.root, appDir, '');\n return map;\n}\n\n/**\n * Generate the full $segment.d.ts content for a segment.\n */\nexport function generateSegmentDts(info: SegmentInfo): string {\n return [\n `export const ROUTE: '${info.route}';`,\n `export const SEGMENT_PATH: '${info.segmentPath}';`,\n '',\n ].join('\\n');\n}\n\n/**\n * Generate per-route $segment.d.ts files in .timber/types/ mirroring\n * the app/ directory structure. These files are importable via TypeScript's\n * rootDirs feature.\n */\nexport async function writeSegmentTypes(\n routeTree: RouteTree,\n appDir: string,\n root: string\n): Promise<void> {\n // Compute the relative prefix from project root to appDir.\n // e.g. if appDir is /project/src/app, relative is 'src/app'\n const relative = appDir.slice(root.length + 1);\n const typesBase = join(root, '.timber', 'types', relative);\n const map = buildDirToRouteMap(routeTree, appDir);\n\n const writes: Promise<void>[] = [];\n for (const [fsDir, info] of map) {\n // Mirror the app/ structure under .timber/types/\n const relDir = fsDir.slice(appDir.length); // e.g. '/[artistSlug]/[year]'\n const segmentDir = join(typesBase, relDir);\n const segmentFile = join(segmentDir, '$segment.d.ts');\n const content = generateSegmentDts(info);\n\n writes.push(\n mkdir(segmentDir, { recursive: true }).then(() => writeFileAtomic(segmentFile, content))\n );\n }\n\n await Promise.all(writes);\n}\n","/**\n * Route-map codegen writer.\n *\n * Materializes the generated route types on disk: `.timber/timber-routes.d.ts`,\n * `.timber/segment-types.d.ts` (via `writeSegmentTypes`), and the root\n * `timber-env.d.ts` that references them.\n *\n * Lives outside `plugins/routing.ts` because it has two callers with two\n * different failure policies: the dev/build plugin starts the write off the hot\n * path and logs failures, while `timber check` awaits it and treats a failure as\n * fatal — a type check run against absent or stale route types reports errors\n * about code that is fine. One implementation, two policies, so the bytes on\n * disk cannot drift between the two commands.\n *\n * Design docs: 09-typescript.md §\"Typed Routes\", 18-build-system.md\n */\n\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { generateRouteMap } from './codegen.ts';\nimport { writeSegmentTypes } from './segment-codegen.ts';\nimport { writeFileAtomic } from '../utils/write-file-atomic.ts';\nimport type { RouteTree } from './types.ts';\n\n/**\n * Path (relative to the project root) of the generated route map declaration\n * file, and the name of the root file that `/// <reference>`s it.\n *\n * Exported because `timber check` has to answer whether the user's tsconfig\n * pulls these into the program — a question about these exact paths. It derives\n * them from here rather than restating them, so a rename cannot leave the check\n * looking for a file nothing writes.\n */\nexport const CODEGEN_OUTPUT = '.timber/timber-routes.d.ts';\n\n/** Root-level file that references the generated declarations. */\nexport const ENV_DTS_FILENAME = 'timber-env.d.ts';\n\n/**\n * Content of the timber-env.d.ts file written to the project root.\n *\n * Uses a triple-slash reference so TypeScript auto-discovers the generated\n * route map without the user needing to edit tsconfig.json — same pattern\n * as Next.js's next-env.d.ts.\n *\n * Also references `vite/client` so side-effect imports like\n * `import './globals.css'` type-check without ts(2882), and so\n * `import.meta.env`, `import.meta.hot`, `import.meta.glob`, asset\n * imports (`*.png`, `*.svg`, …), CSS modules, and `?url`/`?raw`/\n * `?inline` query variants all resolve without the user editing\n * tsconfig.json themselves. Next.js's next-env.d.ts plays the same\n * role via `next/types/global`.\n *\n * `vite` is a peer dep of `@timber-js/app`, so the reference always\n * resolves in a correctly-installed project.\n */\nconst TIMBER_ENV_DTS = [\n '// This file is auto-generated by timber.js. Do not edit.',\n '// It ensures TypeScript picks up the generated route types and',\n '// Vite-provided ambient types (CSS, assets, import.meta.env, HMR).',\n '/* eslint-disable @typescript-eslint/triple-slash-reference */',\n '/// <reference path=\".timber/timber-routes.d.ts\" />',\n '/// <reference types=\"vite/client\" />',\n '/// <reference types=\"react/canary\" />',\n '/// <reference types=\"react-dom/canary\" />',\n '',\n].join('\\n');\n\n/**\n * Codegen writes that have been started but have not yet landed on disk.\n *\n * `writeRouteCodegen` is deliberately unawaited on the dev/build hot path,\n * which leaves that caller no way to know the files exist yet. Anything that\n * tears down the project root — a build process exiting, a test removing its\n * temp root — then races the writes: `rmSync` empties a directory that a\n * pending `mkdir` immediately recreates, and the removal fails with ENOTEMPTY.\n *\n * Every started write registers here and deregisters when it settles, so\n * `flushCodegenWrites()` can await them instead of guessing at a delay.\n */\nconst pendingCodegenWrites = new Set<Promise<void>>();\n\n/**\n * Resolve once every codegen write started so far has landed on disk.\n *\n * Safe to call at any time and from anywhere — writes are tracked per process,\n * not per plugin instance, so a caller does not need a handle on the plugin\n * that started them. Never rejects: the tracked copy of each write has its\n * rejection swallowed, so a failing write cannot turn a teardown barrier into\n * an unhandled rejection. Callers that care about failure await the promise\n * `writeRouteCodegen` returned instead.\n */\nexport async function flushCodegenWrites(): Promise<void> {\n // Loop rather than a single Promise.all: a write started while we were\n // awaiting would otherwise be missed, and the set would still be non-empty\n // on return.\n //\n // Each round drops what it awaited rather than trusting the per-write\n // `finally` to have run, so the two removal paths are independent — a\n // deregistration that stopped firing would leak memory in dev, but it can\n // never turn this into a spin.\n while (pendingCodegenWrites.size > 0) {\n const inFlight = [...pendingCodegenWrites];\n await Promise.all(inFlight);\n for (const write of inFlight) pendingCodegenWrites.delete(write);\n }\n}\n\n/**\n * Write the generated route map, segment types, and the timber-env.d.ts\n * reference file for `tree`.\n *\n * Returns a promise that **rejects** if any write fails. The dev/build plugin\n * does not await it (types are a developer convenience there, not a runtime\n * dependency) and logs instead; `timber check` awaits it and fails the command.\n * Either way the write is registered with `flushCodegenWrites()`.\n */\n/**\n * Write `timber-env.d.ts` only when its content differs.\n *\n * Its content is constant per timber version, so almost every call is a no-op\n * — and a file that is not rewritten cannot be read half-written. It is not\n * replaced atomically because it lives in the user's project root, often\n * committed: a rename would turn a symlink into a regular file, reset its\n * permissions, and a process killed mid-write would leave a temp file next to\n * their source. The in-place write happens only on first run or a timber\n * upgrade.\n */\nasync function writeEnvDts(envPath: string): Promise<void> {\n const existing = await readFile(envPath, 'utf-8').catch(() => null);\n if (existing !== TIMBER_ENV_DTS) await writeFile(envPath, TIMBER_ENV_DTS, 'utf-8');\n}\n\nexport function writeRouteCodegen(tree: RouteTree, appDir: string, root: string): Promise<void> {\n const timberDir = join(root, '.timber');\n const content = generateRouteMap(tree, { appDir, outputDir: timberDir });\n const routesPath = join(root, CODEGEN_OUTPUT);\n const envPath = join(root, ENV_DTS_FILENAME);\n\n const done = mkdir(timberDir, { recursive: true })\n .then(() =>\n // The type checker can read these while they are being rewritten — an\n // editor's TS server during `timber dev`, a `tsgo` run during `vite\n // build`. A truncating write shows it an empty route map for the length\n // of the write, and every typed route degrades to `any` (TIM-1562).\n // The generated files under `.timber/` are replaced atomically.\n Promise.all([\n writeFileAtomic(routesPath, content),\n writeEnvDts(envPath),\n writeSegmentTypes(tree, appDir, root),\n ])\n )\n .then(() => undefined);\n\n // The tracked copy swallows rejection so `flushCodegenWrites` — a teardown\n // barrier — never rejects and never leaves an unhandled rejection behind.\n // The returned promise keeps its rejection for callers that want it.\n const tracked = done.catch(() => undefined);\n pendingCodegenWrites.add(tracked);\n void tracked.finally(() => pendingCodegenWrites.delete(tracked));\n\n return done;\n}\n","/**\n * Convention linter — validates common misconfigurations in the route tree.\n *\n * Runs at scan time (build and dev startup). Each check produces a warning\n * with the file path, what's wrong, and what to do about it.\n *\n * Most checks are warnings — they don't block the build. The goal is to catch\n * issues that would otherwise produce cryptic runtime behavior (silent 404s,\n * empty pages, confusing React errors).\n *\n * A check emits `level: 'error'` only when the misconfiguration is *proven*\n * (never inferred from an absence the analysis cannot see through) and fatal\n * to rendering. `plugins/routing.ts` turns those into a thrown build error,\n * so a false positive there refuses a working app — see\n * `classifyClientBoundaryFile` for how that bar is met.\n *\n * Design doc: 07-routing.md, 10-error-handling.md\n */\n\nimport { existsSync } from 'node:fs';\nimport type { RouteFile, RouteTree, SegmentNode } from './types.ts';\nimport { swallow } from '../server/logger.ts';\nimport {\n fileDefaultExportIsLocalValue,\n fileHasAnyExport,\n fileHasDefaultExport,\n fileHasDirective,\n fileHasStarExport,\n getCsrfExport,\n} from './export-detect.ts';\n\n// ─── Types ──────────────────────────────────────────────────────────────────\n\nexport interface ConventionWarning {\n /** Warning ID for deduplication and filtering. */\n id: string;\n /** Human-readable single-line summary. */\n summary: string;\n /** Multi-line details with file path and fix suggestion. */\n details: string;\n /** Severity: 'warn' for potential issues, 'error' for definite misconfigurations. */\n level: 'warn' | 'error';\n}\n\n// ─── Lint Rules ─────────────────────────────────────────────────────────────\n\n/**\n * Run all convention lint checks on a route tree.\n *\n * Returns an array of warnings. Empty array means everything looks good.\n */\nexport function lintConventions(tree: RouteTree, appDir: string): ConventionWarning[] {\n const warnings: ConventionWarning[] = [];\n\n // Check 1: app/ directory exists (caller should check before scanning,\n // but we validate the root has at least one routable file)\n checkEmptyApp(tree.root, appDir, warnings);\n\n // Check 2: route.ts without recognized HTTP method exports\n checkRouteExports(tree.root, warnings);\n\n // Check 3: Segments with layout but no page/route anywhere in subtree\n // (not a misconfiguration per se — layouts without pages are valid for\n // route groups. Skip this check.)\n\n // Check 4: Root segment has no layout.tsx (no HTML shell)\n checkRootLayout(tree.root, warnings);\n\n // Check 5: page.tsx / layout.tsx without default export\n checkDefaultExports(tree.root, warnings);\n\n // Check 6: Client layouts cannot suppress server children before Flight\n // serializes them. They remain valid UI boundaries, but not authorization\n // or data-hiding boundaries.\n checkClientLayouts(tree.root, warnings);\n\n // Check 7: error.tsx / 404.tsx / 4xx.tsx / … without 'use client' directive\n checkClientBoundaryFiles(tree.root, warnings);\n if (tree.globalError) {\n checkFileUseClientDirective(tree.globalError, 'global-error', warnings);\n }\n\n // Check 8: middleware.ts inside a parallel slot subtree (silently dead)\n checkSlotMiddleware(tree.root, warnings);\n\n // Check 9: `export const csrf` anywhere but route.ts, or in a form the\n // scanner cannot read (TIM-1543)\n checkCsrfExports(tree.root, warnings);\n if (tree.proxy) checkCsrfExport(tree.proxy, 'proxy.ts', warnings);\n if (tree.globalError) checkCsrfExport(tree.globalError, 'global-error', warnings);\n\n return warnings;\n}\n\nfunction checkClientLayouts(node: SegmentNode, warnings: ConventionWarning[]): void {\n if (node.layout && isScriptExtension(node.layout.extension)) {\n try {\n const isClient = fileHasDirective(node.layout.filePath, 'use client');\n const isProvablyServer = fileDefaultExportIsLocalValue(node.layout.filePath);\n if (isClient || !isProvablyServer) {\n const certainty = isClient\n ? `This layout is a client component.`\n : `Timber cannot prove this layout is a server component because its default export comes from another module or an opaque expression.`;\n warnings.push({\n id: `CLIENT_LAYOUT_NOT_AUTH_BOUNDARY:${node.layout.filePath}`,\n summary: isClient\n ? `Client layout cannot securely suppress server children: ${node.layout.filePath}`\n : `Layout may resolve to a client component and cannot be trusted as an auth boundary: ${node.layout.filePath}`,\n details:\n ` File: ${node.layout.filePath}\\n\\n` +\n ` ${certainty}\\n` +\n ` React Flight resolves server content passed to a client layout before the layout runs in the browser.\\n` +\n ` Conditionally omitting children here does not prevent descendant server components from executing or being serialized.\\n\\n` +\n ` To fix: Put authorization in access.ts or a provably server layout with a locally defined default component, and keep client components inside that boundary.\\n`,\n level: 'warn',\n });\n }\n } catch (err) {\n swallow(err, `convention-lint: unreadable layout file ${node.layout.filePath}`);\n }\n }\n for (const child of node.children) checkClientLayouts(child, warnings);\n for (const slot of Object.values(node.slots)) checkClientLayouts(slot, warnings);\n}\n\n// ─── Check: CSRF Exemption Export ──────────────────────────────────────────\n\n/** Single-file segment conventions other than route.ts. */\nconst NON_ROUTE_FILE_KEYS = [\n 'page',\n 'layout',\n 'middleware',\n 'access',\n 'error',\n 'default',\n 'denied',\n] as const satisfies readonly (keyof SegmentNode)[];\n\n/**\n * Fail the build on a `csrf` export the CSRF gate would not honor.\n *\n * Both findings are proven from source, and both are fatal in the sense\n * the error level requires: the developer wrote an exemption that silently\n * does nothing, so a legitimate cross-site POST they meant to accept gets\n * a 403 in production, or they believe a page or action is exempt when\n * the gate still protects it. The exemption exists only on route.ts —\n * server actions always get the check (design/08 §\"Per-route exemption\").\n * A `'use server'` module cannot export it at all: plugin-rsc rejects every\n * non-async-function export there.\n */\nfunction checkCsrfExports(node: SegmentNode, warnings: ConventionWarning[]): void {\n if (node.route) checkCsrfExport(node.route, 'route.ts', warnings);\n for (const key of NON_ROUTE_FILE_KEYS) {\n const file = node[key];\n if (file) checkCsrfExport(file, key, warnings);\n }\n // Status files and metadata routes (sitemap.ts, opengraph-image.tsx, …).\n // JSON status files are skipped by the script-extension check.\n for (const [name, file] of Object.entries(node.statusFiles ?? {})) {\n checkCsrfExport(file, name, warnings);\n }\n for (const [name, file] of Object.entries(node.metadataRoutes ?? {})) {\n checkCsrfExport(file, name, warnings);\n }\n for (const child of node.children) checkCsrfExports(child, warnings);\n for (const slot of Object.values(node.slots)) checkCsrfExports(slot, warnings);\n}\n\nfunction checkCsrfExport(file: RouteFile, convention: string, warnings: ConventionWarning[]): void {\n if (!isScriptExtension(file.extension)) return;\n const found = getCsrfExport(file.filePath);\n if (found === 'absent') return;\n const isRoute = convention === 'route.ts';\n if (isRoute && found === 'exempt') return;\n warnings.push({\n id: `CSRF_EXPORT_INVALID:${file.filePath}`,\n summary: isRoute\n ? `route.ts csrf export must be exactly \\`export const csrf = false\\`: ${file.filePath}`\n : `\\`csrf\\` can only be exported from route.ts, not ${convention}: ${file.filePath}`,\n details:\n ` File: ${file.filePath}\\n\\n` +\n (isRoute\n ? ' The CSRF gate reads this export from source before any module loads, so only\\n' +\n ' the literal form counts. Other values, `let`, and re-exports are not read.\\n\\n' +\n ' To fix: write `export const csrf = false;` in this file, or remove the export.\\n'\n : ' The per-route CSRF exemption applies only to route.ts handlers. Pages, layouts\\n' +\n ' and server actions always get the CSRF check.\\n\\n' +\n ' To fix: remove the export. To accept a cross-site POST, handle it in a route.ts\\n' +\n ' that exports `const csrf = false`.\\n'),\n level: 'error',\n });\n}\n\n// ─── Check: Empty App ───────────────────────────────────────────────────────\n\n/**\n * Warn when the app/ directory has no routable files at all.\n *\n * This catches the \"I created app/ but didn't add any pages\" case where\n * every request silently 404s with no guidance.\n */\nfunction checkEmptyApp(root: SegmentNode, appDir: string, warnings: ConventionWarning[]): void {\n if (hasAnyRoutable(root)) return;\n\n warnings.push({\n id: 'EMPTY_APP',\n summary: 'No pages or route handlers found in app/',\n details:\n ` Directory: ${appDir}\\n\\n` +\n ' Your app/ directory has no page.tsx or route.ts files.\\n' +\n ' Every request will return 404.\\n\\n' +\n ' To fix: Create app/page.tsx with a default export:\\n\\n' +\n ' export default function Home() {\\n return <h1>Hello</h1>;\\n }\\n',\n level: 'warn',\n });\n}\n\n/**\n * Check if a segment tree has any routable files (page or route) anywhere.\n */\nfunction hasAnyRoutable(node: SegmentNode): boolean {\n if (node.page || node.route) return true;\n for (const child of node.children) {\n if (hasAnyRoutable(child)) return true;\n }\n for (const slot of Object.values(node.slots)) {\n if (hasAnyRoutable(slot)) return true;\n }\n return false;\n}\n\n/**\n * Check if a segment tree has any page files (not just route handlers).\n * Used by checkRootLayout to avoid false positives for API-only apps (TIM-794).\n * API-only apps (just route.ts handlers) don't need a root layout.\n */\nfunction hasAnyPage(node: SegmentNode): boolean {\n if (node.page) return true;\n for (const child of node.children) {\n if (hasAnyPage(child)) return true;\n }\n for (const slot of Object.values(node.slots)) {\n if (hasAnyPage(slot)) return true;\n }\n return false;\n}\n\n// ─── Check: Route Exports ───────────────────────────────────────────────────\n\n/** HTTP methods that route.ts can export. */\nconst HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'] as const;\n\n/**\n * Warn when a route.ts file doesn't appear to export any recognized HTTP methods.\n *\n * Uses AST-based detection via parseAst for precise matching.\n * Falls back silently when the file can't be parsed (syntax errors produce\n * no spurious warning — the TS compiler will catch those).\n */\nfunction checkRouteExports(node: SegmentNode, warnings: ConventionWarning[]): void {\n if (node.route) {\n const filePath = node.route.filePath;\n try {\n const methods = fileHasAnyExport(filePath, HTTP_METHODS);\n if (methods.size === 0 && !fileHasStarExport(filePath)) {\n const hasDefault = fileHasDefaultExport(filePath);\n const hint = hasDefault\n ? ' It looks like you have a default export. route.ts uses named exports\\n' +\n ' for HTTP methods (GET, POST, etc.), not a default export.\\n'\n : '';\n\n warnings.push({\n id: 'ROUTE_NO_METHODS',\n summary: `route.ts has no HTTP method exports: ${filePath}`,\n details:\n ` File: ${filePath}\\n\\n` +\n ' route.ts files must export named HTTP method handlers.\\n' +\n ' Without them, the route matches but returns 405 Method Not Allowed.\\n\\n' +\n hint +\n ' Example:\\n\\n' +\n ' export function GET(ctx: RouteContext) {\\n' +\n \" return Response.json({ hello: 'world' });\\n\" +\n ' }\\n',\n level: 'warn',\n });\n }\n } catch (err) {\n swallow(err, `convention-lint: unreadable route.ts ${filePath}`);\n }\n }\n\n // Recurse\n for (const child of node.children) {\n checkRouteExports(child, warnings);\n }\n for (const slot of Object.values(node.slots)) {\n checkRouteExports(slot, warnings);\n }\n}\n\n// ─── Check: Root Layout ─────────────────────────────────────────────────────\n\n/**\n * Warn when the root segment has no layout.tsx.\n *\n * Without a root layout, there's no HTML shell (<html>, <body>).\n * The page will render but may have hydration issues or no proper document structure.\n */\nfunction checkRootLayout(root: SegmentNode, warnings: ConventionWarning[]): void {\n if (root.layout) return;\n\n // Only warn if there are pages — API-only apps (just route.ts handlers)\n // don't need a root layout (TIM-794).\n if (!hasAnyPage(root)) return;\n\n warnings.push({\n id: 'NO_ROOT_LAYOUT',\n summary: 'No root layout.tsx found',\n details:\n ' Your app has pages but no root layout.\\n' +\n ' Without app/layout.tsx, pages have no <html> or <body> wrapper.\\n\\n' +\n ' To fix: Create app/layout.tsx:\\n\\n' +\n ' export default function RootLayout({ children }: { children: React.ReactNode }) {\\n' +\n ' return (\\n' +\n ' <html lang=\"en\">\\n' +\n ' <body>{children}</body>\\n' +\n ' </html>\\n' +\n ' );\\n' +\n ' }\\n',\n level: 'warn',\n });\n}\n\n// ─── Check: Default Exports ───────────────────────────────────────────\n\n/**\n * Warn when page.tsx or layout.tsx files don't have a default export.\n *\n * Without a default export:\n * - page.tsx: the page renders nothing (empty content)\n * - layout.tsx: the layout module loads but has no component to render\n *\n * Uses AST-based detection via parseAst for precise matching. Falls back\n * silently when the file can't be parsed (no spurious warning on syntax errors).\n */\nfunction checkDefaultExports(node: SegmentNode, warnings: ConventionWarning[]): void {\n // Check page files (tsx/ts/jsx/js only — not mdx, which is default-exported by the mdx compiler)\n if (node.page && isScriptExtension(node.page.extension)) {\n checkFileDefaultExport(node.page.filePath, 'page', warnings);\n }\n\n // Check layout files\n if (node.layout && isScriptExtension(node.layout.extension)) {\n checkFileDefaultExport(node.layout.filePath, 'layout', warnings);\n }\n\n // Recurse\n for (const child of node.children) {\n checkDefaultExports(child, warnings);\n }\n for (const slot of Object.values(node.slots)) {\n checkDefaultExports(slot, warnings);\n }\n}\n\nfunction isScriptExtension(ext: string): boolean {\n return ext === 'tsx' || ext === 'ts' || ext === 'jsx' || ext === 'js';\n}\n\nfunction checkFileDefaultExport(\n filePath: string,\n fileType: string,\n warnings: ConventionWarning[]\n): void {\n try {\n if (!fileHasDefaultExport(filePath)) {\n warnings.push({\n id: `NO_DEFAULT_EXPORT:${filePath}`,\n summary: `${fileType}.tsx has no default export: ${filePath}`,\n details:\n ` File: ${filePath}\\n\\n` +\n ` ${fileType}.tsx files must export a default React component.\\n` +\n ` Without a default export, the ${fileType === 'page' ? 'page renders nothing' : 'layout has no component to wrap children'}.\\n\\n` +\n ` To fix: Add a default export:\\n\\n` +\n ` export default function My${fileType === 'page' ? 'Page' : 'Layout'}(${fileType === 'layout' ? '{ children }' : ''}) {\\n` +\n ` return ${fileType === 'layout' ? '<div>{children}</div>' : '<h1>Hello</h1>'};\\n` +\n ` }\\n`,\n level: 'warn',\n });\n }\n } catch (err) {\n swallow(err, `convention-lint: unreadable ${fileType} file ${filePath}`);\n }\n}\n\n// ─── Check: Client Boundary Directive ───────────────────────────────────────\n\n/**\n * Check every file the renderer hands to `TimberErrorBoundary` as a component\n * prop: `error.tsx`, `global-error.tsx`, and the status-code family\n * (`404.tsx`, `4xx.tsx`, `503.tsx`, …).\n *\n * `TimberErrorBoundary` is a client component, so a server component passed as\n * `fallbackComponent` is a function crossing the RSC → client boundary. React\n * Flight refuses to serialize it, and because the boundary wraps the segment's\n * *children*, the failure hits every page under that segment — not just the\n * error path. The raw message (\"Functions cannot be passed directly to Client\n * Components\") names neither the file nor the fix (TIM-1329).\n *\n * `.json` status files never reach React at all, and are not in `statusFiles`.\n */\nfunction checkClientBoundaryFiles(node: SegmentNode, warnings: ConventionWarning[]): void {\n if (node.error) {\n checkFileUseClientDirective(node.error, 'error', warnings);\n }\n\n for (const [key, file] of Object.entries(node.statusFiles ?? {})) {\n checkFileUseClientDirective(file, key, warnings);\n }\n\n for (const child of node.children) {\n checkClientBoundaryFiles(child, warnings);\n }\n for (const slot of Object.values(node.slots)) {\n checkClientBoundaryFiles(slot, warnings);\n }\n}\n\n/**\n * `'client'` — carries the directive.\n * `'server'` — proven server component: no directive, and the default export is\n * a function or class built by this very module, so no other file's directive\n * can be governing it.\n * `'unknown'` — no directive, but the exported value may come from elsewhere\n * (`export { default } from './client-thing'`), or the file could not be\n * read or parsed. Never escalated to a build error.\n */\ntype ClientBoundaryVerdict = 'client' | 'server' | 'unknown';\n\nfunction classifyClientBoundaryFile(filePath: string): ClientBoundaryVerdict {\n if (fileHasDirective(filePath, 'use client')) return 'client';\n if (fileDefaultExportIsLocalValue(filePath)) return 'server';\n return 'unknown';\n}\n\n/** Example component name for the fix snippet, keyed by convention name. */\nfunction exampleComponentName(convention: string): string {\n if (convention === 'global-error') return 'GlobalError';\n if (convention === 'error') return 'ErrorBoundary';\n if (convention === '404') return 'NotFound';\n return 'StatusPage';\n}\n\n/**\n * @param file the route file (its extension decides whether the check applies)\n * @param convention the convention name — `error`, `global-error`, `404`, `4xx`, …\n */\nfunction checkFileUseClientDirective(\n file: RouteFile,\n convention: string,\n warnings: ConventionWarning[]\n): void {\n // Only script extensions need the 'use client' check — .json status files\n // never reach React at all, and are not in `statusFiles`.\n if (!isScriptExtension(file.extension)) return;\n\n const filePath = file.filePath;\n const label = `${convention}.${file.extension}`;\n let verdict: ClientBoundaryVerdict;\n try {\n verdict = classifyClientBoundaryFile(filePath);\n } catch (err) {\n swallow(err, `convention-lint: unreadable ${label} file ${filePath}`);\n return;\n }\n if (verdict === 'client') return;\n\n const isError = verdict === 'server';\n const name = exampleComponentName(convention);\n const props =\n convention === 'error' || convention === 'global-error' ? '{ error, reset }' : '{ status }';\n\n warnings.push({\n id: `NEEDS_USE_CLIENT:${filePath}`,\n summary: isError\n ? `${label} is a server component and must be a client component: ${filePath}`\n : `${label} may be missing 'use client': ${filePath}`,\n details:\n ` File: ${filePath}\\n\\n` +\n ` ${label} is handed to timber's error boundary — a client\\n` +\n ' component — as a prop. A server component cannot cross that boundary:\\n' +\n ' React fails with \"Functions cannot be passed directly to Client\\n' +\n ' Components\", and every page under this segment fails to render.\\n\\n' +\n (isError\n ? \" To fix: Add 'use client' as the first line of the file:\\n\\n\" +\n \" 'use client';\\n\\n\" +\n ` export default function ${name}(${props}) {\\n` +\n ' return <h1>…</h1>;\\n' +\n ' }\\n'\n : // The default export comes from somewhere this analysis cannot follow\n // — a re-export, an imported binding, a wrapper call. It may well\n // already be a client component, so say what was checked rather than\n // assert a break.\n \" This file has no 'use client' directive, and its default export\\n\" +\n ' comes from elsewhere, so timber cannot tell which it is. If the\\n' +\n \" module it re-exports starts with 'use client', nothing is wrong\\n\" +\n ' and you can ignore this. Otherwise add the directive there.\\n'),\n level: isError ? 'error' : 'warn',\n });\n}\n\n// ─── Check: Slot Middleware ─────────────────────────────────────────────────\n\n/**\n * Error when middleware.ts appears inside a parallel slot subtree.\n *\n * Middleware is collected from the main matched segment chain only\n * (route-matcher.ts `collectMiddlewareChain`). The slot resolver never\n * walks `middleware` on its matched chain, so a middleware.ts under any\n * `@slot/` directory silently never executes. `access.ts` in slot chains\n * DOES run (via SlotAccessGate), making the asymmetry invisible unless\n * you know to look.\n *\n * This is a build error rather than a warning: the file provably cannot\n * run and its presence misleads the author into thinking the slot is\n * middleware-protected.\n *\n * See design/07-routing.md §\"Parallel Routes\"\n */\nfunction checkSlotMiddleware(node: SegmentNode, warnings: ConventionWarning[]): void {\n for (const [slotName, slotRoot] of Object.entries(node.slots)) {\n collectSlotMiddleware(slotRoot, slotName, warnings);\n }\n for (const child of node.children) {\n checkSlotMiddleware(child, warnings);\n }\n}\n\n/**\n * Walk a slot subtree and report every middleware.ts found.\n */\nfunction collectSlotMiddleware(\n node: SegmentNode,\n slotName: string,\n warnings: ConventionWarning[]\n): void {\n if (node.middleware) {\n warnings.push({\n id: `SLOT_MIDDLEWARE:${node.middleware.filePath}`,\n summary: `middleware.ts inside slot @${slotName} never executes: ${node.middleware.filePath}`,\n details:\n ` File: ${node.middleware.filePath}\\n\\n` +\n ` Middleware is collected from the main route chain only.\\n` +\n ` The slot resolver does not run middleware — this file is silently dead.\\n` +\n ` (access.ts in slots does run via SlotAccessGate; middleware.ts does not.)\\n\\n` +\n ` To fix: Move the middleware logic into access.ts for slot-scoped auth,\\n` +\n ` or into a middleware.ts on the main route chain outside the slot.\\n`,\n level: 'error',\n });\n }\n for (const child of node.children) {\n collectSlotMiddleware(child, slotName, warnings);\n }\n for (const [nestedSlotName, nestedSlot] of Object.entries(node.slots)) {\n collectSlotMiddleware(nestedSlot, nestedSlotName, warnings);\n }\n}\n\n// ─── Check: App Directory Exists ────────────────────────────────────────────\n\n/**\n * Check if the app/ directory exists. Called before scanning.\n * Returns a warning if missing, or null if the directory exists.\n */\nexport function checkAppDirExists(appDir: string): ConventionWarning | null {\n if (existsSync(appDir)) return null;\n\n return {\n id: 'NO_APP_DIR',\n summary: 'No app/ directory found',\n details:\n ` Expected: ${appDir}\\n\\n` +\n ' timber.js requires an app/ directory for file-system routing.\\n' +\n ' Every request will return 404 until you create it.\\n\\n' +\n ' To fix: Create the app/ directory with a page:\\n\\n' +\n ' mkdir -p app\\n' +\n ' # Create app/layout.tsx and app/page.tsx\\n',\n level: 'error',\n };\n}\n\n// ─── Formatting ─────────────────────────────────────────────────────────────\n\nimport type { InspectColor } from 'node:util';\nimport { styleText } from 'node:util';\n\ntype Format = InspectColor | readonly InspectColor[];\nconst noValidate = { validateStream: false } as const;\n\nexport interface FormatOptions {\n /**\n * Emit ANSI escapes. Off when the text becomes an `Error` message —\n * whoever prints it decides its formatting, and colored escapes inside\n * an exception leak into logs and stack traces.\n */\n color?: boolean;\n}\n\n/**\n * Format warnings for terminal output.\n *\n * Groups by severity, uses colors, and includes fix suggestions.\n */\nexport function formatConventionWarnings(\n warnings: ConventionWarning[],\n options: FormatOptions = {}\n): string {\n const { color = true } = options;\n const style = (fmt: Format, text: string) => (color ? styleText(fmt, text, noValidate) : text);\n if (warnings.length === 0) return '';\n\n const errors = warnings.filter((w) => w.level === 'error');\n const warns = warnings.filter((w) => w.level === 'warn');\n\n const lines: string[] = [];\n\n if (errors.length > 0) {\n lines.push(\n `${style(['red', 'bold'], '[timber]')} ${style('red', `${errors.length} configuration error${errors.length !== 1 ? 's' : ''}:`)}`\n );\n for (const e of errors) {\n lines.push('');\n lines.push(` ${style('red', '✗')} ${e.summary}`);\n lines.push(style('dim', e.details));\n }\n }\n\n if (warns.length > 0) {\n if (errors.length > 0) lines.push('');\n lines.push(\n `${style(['yellow', 'bold'], '[timber]')} ${style('yellow', `${warns.length} configuration warning${warns.length !== 1 ? 's' : ''}:`)}`\n );\n for (const w of warns) {\n lines.push('');\n lines.push(` ${style('yellow', '⚠')} ${w.summary}`);\n lines.push(style('dim', w.details));\n }\n }\n\n return lines.join('\\n');\n}\n"],"mappings":"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"}
|
|
@@ -1336,7 +1336,7 @@ function createGraphEndpointMiddleware(server, ctx) {
|
|
|
1336
1336
|
}
|
|
1337
1337
|
reflectCors(res, origin);
|
|
1338
1338
|
try {
|
|
1339
|
-
const { classifyLiveFile, classifyLiveProject } = await import("./live-graph-
|
|
1339
|
+
const { classifyLiveFile, classifyLiveProject } = await import("./live-graph-C_4v-fHv.js");
|
|
1340
1340
|
const preserveSymlinks = server.config.resolve.preserveSymlinks;
|
|
1341
1341
|
const root = normalizePath(preserveSymlinks ? ctx.root : await realpath(ctx.root));
|
|
1342
1342
|
const appDir = normalizePath(preserveSymlinks ? ctx.appDir : await realpath(ctx.appDir).catch(() => join(root, relative(ctx.root, ctx.appDir))));
|
|
@@ -2036,4 +2036,4 @@ function listenForClientErrors(server, projectRoot) {
|
|
|
2036
2036
|
//#endregion
|
|
2037
2037
|
export { reportConfigErrors as a, formatMissingPeers as i, timberDevServer as n, validateConfig as o, checkPeerDependencies as r, dev_server_exports as t };
|
|
2038
2038
|
|
|
2039
|
-
//# sourceMappingURL=dev-server-
|
|
2039
|
+
//# sourceMappingURL=dev-server-FKxptbnI.js.map
|