@timber-js/app 0.2.0-alpha.100 → 0.2.0-alpha.102

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/_chunks/{actions-CQ8Z8VGL.js → actions-n9qTlpmY.js} +7 -10
  2. package/dist/_chunks/actions-n9qTlpmY.js.map +1 -0
  3. package/dist/_chunks/{build-output-helper-DXnW0qjz.js → build-output-helper-5TK2Emwp.js} +1 -1
  4. package/dist/_chunks/{build-output-helper-DXnW0qjz.js.map → build-output-helper-5TK2Emwp.js.map} +1 -1
  5. package/dist/_chunks/{define-CfBPoJb0.js → define-Btmz9B20.js} +3 -3
  6. package/dist/_chunks/{define-CfBPoJb0.js.map → define-Btmz9B20.js.map} +1 -1
  7. package/dist/_chunks/{define-B-Q_UMOD.js → define-Dn98bPbb.js} +3 -3
  8. package/dist/_chunks/{define-B-Q_UMOD.js.map → define-Dn98bPbb.js.map} +1 -1
  9. package/dist/_chunks/{format-Bcn-Iv1x.js → format-CYBGxKtc.js} +1 -1
  10. package/dist/_chunks/{format-Bcn-Iv1x.js.map → format-CYBGxKtc.js.map} +1 -1
  11. package/dist/_chunks/{handler-store-B-lqaGyh.js → handler-store-B_df5Npe.js} +1 -1
  12. package/dist/_chunks/{handler-store-B-lqaGyh.js.map → handler-store-B_df5Npe.js.map} +1 -1
  13. package/dist/_chunks/{logger-0m8MsKdc.js → logger-DDmK5skc.js} +2 -2
  14. package/dist/_chunks/{logger-0m8MsKdc.js.map → logger-DDmK5skc.js.map} +1 -1
  15. package/dist/_chunks/merge-search-params-Cm_KIWDX.js +41 -0
  16. package/dist/_chunks/merge-search-params-Cm_KIWDX.js.map +1 -0
  17. package/dist/_chunks/{metadata-routes-BU684ls2.js → metadata-routes-Cjmvi3rQ.js} +1 -1
  18. package/dist/_chunks/{metadata-routes-BU684ls2.js.map → metadata-routes-Cjmvi3rQ.js.map} +1 -1
  19. package/dist/_chunks/{navigation-root-BCYczjml.js → navigation-root-2ypaZQDx.js} +2 -2
  20. package/dist/_chunks/{navigation-root-BCYczjml.js.map → navigation-root-2ypaZQDx.js.map} +1 -1
  21. package/dist/_chunks/{registry-I2ss-lvy.js → registry-DB93kT5c.js} +1 -1
  22. package/dist/_chunks/{registry-I2ss-lvy.js.map → registry-DB93kT5c.js.map} +1 -1
  23. package/dist/_chunks/{router-ref-h3-UaCQv.js → router-ref-TkUdmqtM.js} +2 -2
  24. package/dist/_chunks/{router-ref-h3-UaCQv.js.map → router-ref-TkUdmqtM.js.map} +1 -1
  25. package/dist/_chunks/{schema-bridge-Cxu4l-7p.js → schema-bridge-C3xl_vfb.js} +1 -1
  26. package/dist/_chunks/{schema-bridge-Cxu4l-7p.js.map → schema-bridge-C3xl_vfb.js.map} +1 -1
  27. package/dist/_chunks/{segment-classify-BjfuctV2.js → segment-classify-CnfLLvir.js} +1 -1
  28. package/dist/_chunks/{segment-classify-BjfuctV2.js.map → segment-classify-CnfLLvir.js.map} +1 -1
  29. package/dist/_chunks/{segment-context-Dx_OizxD.js → segment-context-Dc-GbSTe.js} +1 -1
  30. package/dist/_chunks/{segment-context-Dx_OizxD.js.map → segment-context-Dc-GbSTe.js.map} +1 -1
  31. package/dist/_chunks/{ssr-data-B4CdH7rE.js → ssr-data-DzuI0bIV.js} +1 -1
  32. package/dist/_chunks/{ssr-data-B4CdH7rE.js.map → ssr-data-DzuI0bIV.js.map} +1 -1
  33. package/dist/_chunks/{stale-reload-Bab885FO.js → stale-reload-D6FlD3xn.js} +1 -1
  34. package/dist/_chunks/{stale-reload-Bab885FO.js.map → stale-reload-D6FlD3xn.js.map} +1 -1
  35. package/dist/_chunks/{tracing-C8V-YGsP.js → tracing-DvKLzCFa.js} +1 -1
  36. package/dist/_chunks/{tracing-C8V-YGsP.js.map → tracing-DvKLzCFa.js.map} +1 -1
  37. package/dist/_chunks/{use-query-states-B2XTqxDR.js → use-query-states-B-cLl8mx.js} +2 -2
  38. package/dist/_chunks/{use-query-states-B2XTqxDR.js.map → use-query-states-B-cLl8mx.js.map} +1 -1
  39. package/dist/_chunks/{use-segment-params-BkpKAQ7D.js → use-segment-params-07V1TIxL.js} +3 -3
  40. package/dist/_chunks/{use-segment-params-BkpKAQ7D.js.map → use-segment-params-07V1TIxL.js.map} +1 -1
  41. package/dist/_chunks/{merge-search-params-BphMdht_.js → validation-DfyuMoK2.js} +2 -40
  42. package/dist/_chunks/validation-DfyuMoK2.js.map +1 -0
  43. package/dist/_chunks/{walkers-Tg0Alwcg.js → walkers-DQn5NNrf.js} +4 -4
  44. package/dist/_chunks/{walkers-Tg0Alwcg.js.map → walkers-DQn5NNrf.js.map} +1 -1
  45. package/dist/_chunks/{warnings-Cg47l5sk.js → warnings-B30wpfaS.js} +2 -2
  46. package/dist/_chunks/{warnings-Cg47l5sk.js.map → warnings-B30wpfaS.js.map} +1 -1
  47. package/dist/adapters/cloudflare.js +1 -1
  48. package/dist/adapters/nitro.js +1 -1
  49. package/dist/cache/index.js +3 -3
  50. package/dist/client/error-boundary.js +2 -2
  51. package/dist/client/index.js +10 -117
  52. package/dist/client/index.js.map +1 -1
  53. package/dist/client/internal.d.ts +1 -1
  54. package/dist/client/internal.d.ts.map +1 -1
  55. package/dist/client/internal.js +8 -8
  56. package/dist/client/use-cookie.d.ts +4 -0
  57. package/dist/client/use-cookie.d.ts.map +1 -1
  58. package/dist/codec.js +1 -1
  59. package/dist/cookies/define-cookie.d.ts +5 -45
  60. package/dist/cookies/define-cookie.d.ts.map +1 -1
  61. package/dist/cookies/index.js +252 -1
  62. package/dist/cookies/index.js.map +1 -1
  63. package/dist/fonts/ast.d.ts +98 -6
  64. package/dist/fonts/ast.d.ts.map +1 -1
  65. package/dist/fonts/local.d.ts +1 -1
  66. package/dist/fonts/transform.d.ts +15 -1
  67. package/dist/fonts/transform.d.ts.map +1 -1
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +1586 -11611
  70. package/dist/index.js.map +1 -1
  71. package/dist/plugins/dev-server.d.ts.map +1 -1
  72. package/dist/plugins/routing.d.ts.map +1 -1
  73. package/dist/plugins/static-build.d.ts +2 -23
  74. package/dist/plugins/static-build.d.ts.map +1 -1
  75. package/dist/routing/index.js +2 -2
  76. package/dist/search-params/index.js +3 -3
  77. package/dist/segment-params/index.js +1 -1
  78. package/dist/server/index.js +3 -3
  79. package/dist/server/internal.js +7 -7
  80. package/dist/server/request-context.d.ts.map +1 -1
  81. package/package.json +8 -6
  82. package/src/cli.ts +0 -0
  83. package/src/client/index.ts +0 -9
  84. package/src/client/internal.ts +1 -1
  85. package/src/client/use-cookie.ts +2 -2
  86. package/src/cookies/define-cookie.ts +100 -166
  87. package/src/fonts/ast.ts +488 -16
  88. package/src/fonts/local.ts +1 -1
  89. package/src/fonts/transform.ts +147 -236
  90. package/src/index.ts +14 -2
  91. package/src/plugins/dev-server.ts +8 -9
  92. package/src/plugins/routing.ts +1 -12
  93. package/src/plugins/static-build.ts +3 -83
  94. package/src/server/request-context.ts +1 -8
  95. package/LICENSE +0 -8
  96. package/dist/_chunks/actions-CQ8Z8VGL.js.map +0 -1
  97. package/dist/_chunks/chunk-BYIpzuS7.js +0 -39
  98. package/dist/_chunks/define-cookie-BjpIt4UC.js +0 -194
  99. package/dist/_chunks/define-cookie-BjpIt4UC.js.map +0 -1
  100. package/dist/_chunks/merge-search-params-BphMdht_.js.map +0 -1
  101. package/dist/plugins/server-action-exports.d.ts +0 -26
  102. package/dist/plugins/server-action-exports.d.ts.map +0 -1
  103. package/dist/routing/status-file-lint.d.ts +0 -34
  104. package/dist/routing/status-file-lint.d.ts.map +0 -1
  105. package/dist/utils/directive-parser.d.ts +0 -28
  106. package/dist/utils/directive-parser.d.ts.map +0 -1
  107. package/src/plugins/server-action-exports.ts +0 -220
  108. package/src/routing/status-file-lint.ts +0 -115
  109. package/src/utils/directive-parser.ts +0 -140
@@ -1 +0,0 @@
1
- {"version":3,"file":"define-cookie-BjpIt4UC.js","names":[],"sources":["../../src/cookies/define-cookie.ts"],"sourcesContent":["/**\n * defineCookie — typed cookie definitions.\n *\n * Bundles name + codec + options into a reusable CookieDefinition<T>\n * with sync .get(), .set(), .delete() isomorphic methods (server + client)\n * and a .useCookie() client hook.\n *\n * Uses the registration pattern: server entry registers serverCookieImpl,\n * client entry registers clientCookieImpl. This avoids top-level value\n * imports from either environment and makes defineCookie isomorphic\n * without `typeof window` checks.\n *\n * Standard Schema objects (Zod, Valibot, ArkType) are auto-detected in\n * the codec option — no explicit fromSchema() wrapper needed.\n *\n * See design/29-cookies.md §\"Typed Cookies with Schema Validation\"\n */\n\nimport type { CookieOptions } from '../server/cookie-context.js';\nimport type { ClientCookieOptions } from '../client/use-cookie.js';\n\n// ─── Types ────────────────────────────────────────────────────────────────\n\nimport type { Codec } from '../codec.js';\nimport type { StandardSchemaV1 } from '../schema-bridge.js';\n\n/**\n * A codec that converts between string cookie values and typed values.\n * Type alias for the shared Codec<T> protocol.\n */\nexport type CookieCodec<T> = Codec<T>;\n\n/** Options for defineCookie: codec + CookieOptions merged. */\nexport interface DefineCookieOptions<T, HttpOnly extends boolean = boolean> extends CookieOptions {\n /**\n * Codec for parsing/serializing the cookie value.\n * Accepts a Codec<T> or a Standard Schema object (Zod, Valibot, ArkType)\n * which is auto-wrapped via fromSchema.\n */\n codec: CookieCodec<T> | StandardSchemaV1<T>;\n /**\n * Prevent client-side JS access. Default: true.\n * When true (or omitted), client methods (useCookie, get/set/delete on\n * client) are omitted from the return type and throw at runtime.\n */\n httpOnly?: HttpOnly;\n}\n\n/**\n * Server-only cookie definition. Returned when httpOnly is true or omitted.\n * Client methods are absent from the type — accessing them is a TS error.\n */\nexport interface ServerOnlyCookieDefinition<T> {\n readonly name: string;\n readonly options: CookieOptions;\n readonly codec: CookieCodec<T>;\n\n /** Read the typed value. Sync, isomorphic (server + client). */\n get(): T;\n /** Set the typed value. Sync, isomorphic (server + client). */\n set(value: T): void;\n /** Delete the cookie. Sync, isomorphic (server + client). */\n delete(): void;\n}\n\n/**\n * Full cookie definition with client methods. Returned when httpOnly: false.\n */\nexport interface CookieDefinition<T> extends ServerOnlyCookieDefinition<T> {\n /** Client: React hook for reading/writing this cookie. Returns [value, setter, deleter]. */\n useCookie(): [T, (value: T) => void, () => void];\n}\n\n// ─── Registration Pattern ─────────────────────────────────────────────────\n//\n// Server and client entries register their implementations at module load\n// time. This avoids dynamic imports (which lose ALS context) and typeof\n// window checks (which are fragile).\n\n/** Server-side cookie impl: reads from ALS-backed cookie jar. */\nexport interface ServerCookieImpl {\n getCookieJar(): {\n get(name: string): string | undefined;\n set(name: string, value: string, options?: CookieOptions): void;\n delete(name: string, options?: Pick<CookieOptions, 'path' | 'domain'>): void;\n };\n}\n\n/** Client-side cookie impl: reads/writes document.cookie via useCookie hook. */\nexport interface ClientCookieImpl {\n useCookie(\n name: string,\n options?: ClientCookieOptions\n ): [string | undefined, (value: string, options?: ClientCookieOptions) => void, () => void];\n getCookieValue(name: string): string | undefined;\n setCookieValue(name: string, value: string, options?: ClientCookieOptions): void;\n deleteCookieValue(name: string, options?: ClientCookieOptions): void;\n}\n\nlet _serverImpl: ServerCookieImpl | undefined;\nlet _clientImpl: ClientCookieImpl | undefined;\nlet _fromSchemaFn: ((schema: StandardSchemaV1<unknown>) => CookieCodec<unknown>) | undefined;\n\n/**\n * Register the server-side cookie implementation.\n * Called by server entry at module load time.\n * @internal\n */\nexport function _registerServerCookieImpl(impl: ServerCookieImpl): void {\n _serverImpl = impl;\n}\n\n/**\n * Register the client-side cookie implementation.\n * Called by client entry at module load time.\n * @internal\n */\nexport function _registerClientCookieImpl(impl: ClientCookieImpl): void {\n _clientImpl = impl;\n}\n\n/**\n * Register the fromSchema bridge function.\n * Called by server/client entry at module load time.\n * @internal\n */\nexport function _registerFromSchema(\n fn: (schema: StandardSchemaV1<unknown>) => CookieCodec<unknown>\n): void {\n _fromSchemaFn = fn;\n}\n\n// Legacy registration for useCookie module — still used by client/index.ts\nlet _useCookieModule: typeof import('../client/use-cookie.js') | undefined;\n\nfunction getUseCookieModule(): typeof import('../client/use-cookie.js') {\n if (!_useCookieModule) {\n throw new Error(\n '[timber] defineCookie().useCookie() requires @timber-js/app/client to be loaded. ' +\n 'This hook can only be used in client components.'\n );\n }\n return _useCookieModule;\n}\n\n/**\n * Register the client cookie module. Called by the client entry to wire\n * up the lazy reference without a top-level import.\n *\n * @internal — framework use only\n */\nexport function _registerUseCookieModule(mod: typeof import('../client/use-cookie.js')): void {\n _useCookieModule = mod;\n // Also register the client impl from the module\n _clientImpl = {\n useCookie: mod.useCookie,\n getCookieValue: (name: string) => {\n if (typeof document === 'undefined') return undefined;\n const match = document.cookie.match(\n new RegExp('(?:^|;\\\\s*)' + name.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&') + '\\\\s*=\\\\s*([^;]*)')\n );\n return match ? decodeURIComponent(match[1]) : undefined;\n },\n setCookieValue: (name: string, value: string, options?: ClientCookieOptions) => {\n const parts: string[] = [`${name}=${encodeURIComponent(value)}`];\n const path = options?.path ?? '/';\n parts.push(`Path=${path}`);\n if (options?.domain) parts.push(`Domain=${options.domain}`);\n if (options?.maxAge !== undefined) parts.push(`Max-Age=${options.maxAge}`);\n if (options?.expires) parts.push(`Expires=${options.expires.toUTCString()}`);\n const sameSite = options?.sameSite ?? 'lax';\n parts.push(`SameSite=${sameSite.charAt(0).toUpperCase()}${sameSite.slice(1)}`);\n if (options?.secure) parts.push('Secure');\n document.cookie = parts.join('; ');\n // Notify useCookie subscribers so mounted hooks re-render\n mod.notifyCookieChange(name);\n },\n deleteCookieValue: (name: string, options?: ClientCookieOptions) => {\n const path = options?.path ?? '/';\n const domain = options?.domain;\n let cookieStr = `${name}=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=${path}`;\n if (domain) cookieStr += `; Domain=${domain}`;\n document.cookie = cookieStr;\n mod.notifyCookieChange(name);\n },\n };\n}\n\n// ─── Standard Schema Auto-Detection ───────────────────────────────────────\n\nfunction resolveCodec<T>(codecOrSchema: CookieCodec<T> | StandardSchemaV1<T>): CookieCodec<T> {\n // If it has parse + serialize, it's already a Codec\n if (\n typeof (codecOrSchema as Codec<T>).parse === 'function' &&\n typeof (codecOrSchema as Codec<T>).serialize === 'function'\n ) {\n return codecOrSchema as CookieCodec<T>;\n }\n\n // Auto-detect Standard Schema\n if (typeof codecOrSchema === 'object' && codecOrSchema !== null && '~standard' in codecOrSchema) {\n if (!_fromSchemaFn) {\n throw new Error(\n '[timber] defineCookie: Standard Schema auto-detection requires @timber-js/app/server or ' +\n '@timber-js/app/client to be loaded. Pass an explicit Codec<T> with parse/serialize methods, ' +\n 'or ensure the framework entry module is imported.'\n );\n }\n return _fromSchemaFn(codecOrSchema as StandardSchemaV1<unknown>) as CookieCodec<T>;\n }\n\n throw new Error(\n '[timber] defineCookie: codec must be a Codec<T> (with parse/serialize methods) ' +\n 'or a Standard Schema object (Zod, Valibot, ArkType).'\n );\n}\n\n// ─── Factory ──────────────────────────────────────────────────────────────\n\n/**\n * Define a typed cookie.\n *\n * ```ts\n * import { defineCookie } from '@timber-js/app/cookies';\n * import { z } from 'zod/v4';\n *\n * // httpOnly: false — client methods available\n * export const themeCookie = defineCookie('theme', {\n * codec: z.enum(['light', 'dark', 'system']).default('system'),\n * httpOnly: false,\n * maxAge: 60 * 60 * 24 * 365,\n * });\n *\n * // Server or client\n * const theme = themeCookie.get();\n * themeCookie.set('dark');\n *\n * // Client hook\n * const [theme, setTheme] = themeCookie.useCookie();\n *\n * // httpOnly: true (default) — server-only, no client methods\n * export const sessionCookie = defineCookie('session', {\n * codec: z.string(),\n * });\n * sessionCookie.get(); // works on server\n * sessionCookie.useCookie(); // TS error — httpOnly cookie\n * ```\n */\nexport function defineCookie<T, HttpOnly extends boolean = true>(\n name: string,\n options: DefineCookieOptions<T, HttpOnly>\n): HttpOnly extends false ? CookieDefinition<T> : ServerOnlyCookieDefinition<T> {\n const { codec: codecOrSchema, ...cookieOpts } = options;\n const codec = resolveCodec(codecOrSchema);\n const resolvedOptions: CookieOptions = { ...cookieOpts };\n const isHttpOnly = options.httpOnly !== false; // default true\n\n function getClientOpts(): ClientCookieOptions {\n return {\n path: resolvedOptions.path,\n domain: resolvedOptions.domain,\n maxAge: resolvedOptions.maxAge,\n expires: resolvedOptions.expires,\n sameSite: resolvedOptions.sameSite,\n secure: resolvedOptions.secure,\n };\n }\n\n function assertNotHttpOnlyClient(method: string): void {\n if (isHttpOnly && !_serverImpl) {\n throw new Error(\n `[timber] defineCookie('${name}').${method}() cannot be used with httpOnly cookies — ` +\n `the browser cannot access them. Set httpOnly: false to enable client access.`\n );\n }\n }\n\n const base: ServerOnlyCookieDefinition<T> = {\n name,\n options: resolvedOptions,\n codec,\n\n get(): T {\n if (_serverImpl) {\n const jar = _serverImpl.getCookieJar();\n const raw = jar.get(name);\n return codec.parse(raw);\n }\n // Client path\n assertNotHttpOnlyClient('get');\n if (_clientImpl) {\n const raw = _clientImpl.getCookieValue(name);\n return codec.parse(raw);\n }\n throw new Error(\n `[timber] defineCookie('${name}').get() — no environment registered. ` +\n 'Ensure @timber-js/app/server or @timber-js/app/client is imported.'\n );\n },\n\n set(value: T): void {\n if (_serverImpl) {\n const jar = _serverImpl.getCookieJar();\n const serialized = codec.serialize(value);\n if (serialized === null) {\n jar.delete(name, {\n path: resolvedOptions.path,\n domain: resolvedOptions.domain,\n });\n } else {\n jar.set(name, serialized, resolvedOptions);\n }\n return;\n }\n // Client path\n assertNotHttpOnlyClient('set');\n if (_clientImpl) {\n const serialized = codec.serialize(value);\n if (serialized === null) {\n _clientImpl.deleteCookieValue(name, getClientOpts());\n } else {\n _clientImpl.setCookieValue(name, serialized, getClientOpts());\n }\n return;\n }\n throw new Error(\n `[timber] defineCookie('${name}').set() — no environment registered. ` +\n 'Ensure @timber-js/app/server or @timber-js/app/client is imported.'\n );\n },\n\n delete(): void {\n if (_serverImpl) {\n const jar = _serverImpl.getCookieJar();\n jar.delete(name, {\n path: resolvedOptions.path,\n domain: resolvedOptions.domain,\n });\n return;\n }\n // Client path\n assertNotHttpOnlyClient('delete');\n if (_clientImpl) {\n _clientImpl.deleteCookieValue(name, getClientOpts());\n return;\n }\n throw new Error(\n `[timber] defineCookie('${name}').delete() — no environment registered. ` +\n 'Ensure @timber-js/app/server or @timber-js/app/client is imported.'\n );\n },\n };\n\n if (isHttpOnly) {\n return base as HttpOnly extends false ? CookieDefinition<T> : ServerOnlyCookieDefinition<T>;\n }\n\n // httpOnly: false — add useCookie client hook\n const full: CookieDefinition<T> = {\n ...base,\n\n useCookie(): [T, (value: T) => void, () => void] {\n const { useCookie: useRawCookie } = getUseCookieModule();\n\n const clientOpts = getClientOpts();\n const [raw, setRaw, deleteRaw] = useRawCookie(name, clientOpts);\n const parsed = codec.parse(raw);\n\n const setTyped = (value: T): void => {\n const serialized = codec.serialize(value);\n if (serialized === null) {\n deleteRaw();\n } else {\n setRaw(serialized);\n }\n };\n\n return [parsed, setTyped, deleteRaw];\n },\n };\n\n return full as HttpOnly extends false ? CookieDefinition<T> : ServerOnlyCookieDefinition<T>;\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"merge-search-params-BphMdht_.js","names":[],"sources":["../../src/cookies/validation.ts","../../src/shared/merge-search-params.ts"],"sourcesContent":["/**\n * Cookie name and value validation per RFC 6265 §4.1.1.\n *\n * This module is pure — no server or client imports — so it can be loaded\n * by both `server/request-context.ts` (which guards `getCookies().set()`)\n * and `client/use-cookie.ts` (which guards `useCookie()`'s setter). The\n * shared validator gives both sides identical encoding contracts:\n * **the framework never silently encodes — callers emit `cookie-octet`\n * bytes or get a developer-facing error.**\n *\n * Why this matters: see ONGOING_SECURITY.md H-3 (TIM-868) and\n * design/29-cookies.md §\"Cookie Name and Value Validation\".\n */\n\n// ─── RFC 6265 cookie-octet ────────────────────────────────────────────────\n//\n// cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E\n//\n// Equivalently: US-ASCII printable bytes excluding whitespace, `\"` (0x22),\n// `,` (0x2C), `;` (0x3B), `\\` (0x5C), and DEL (0x7F).\n\n/**\n * RFC 7230 §3.2.6 token characters used as the cookie-name production in\n * RFC 6265 §4.1.1. Anchored, non-empty match.\n *\n * Allowed: US-ASCII letters, digits, and `!#$%&'*+-.^_` `` ` `` `|~`.\n * Disallowed: whitespace, controls, delimiters\n * (`(` `)` `<` `>` `@` `,` `;` `:` `\\` `\"` `/` `[` `]` `?` `=` `{` `}`).\n */\nconst COOKIE_NAME_RE = /^[!#$%&'*+\\-.0-9A-Z^_`a-z|~]+$/;\n\n/**\n * Format a single byte for a clear error message — escape control bytes\n * and non-ASCII as `0x..` and quote printables.\n */\nfunction describeChar(value: string, index: number): string {\n const code = value.charCodeAt(index);\n if (code < 0x20 || code === 0x7f || code > 0x7e) {\n return `0x${code.toString(16).padStart(2, '0')}`;\n }\n return JSON.stringify(value[index]);\n}\n\n/**\n * Validate a cookie name against RFC 6265 §4.1.1 / RFC 7230 token rules.\n * Throws a developer-facing error naming the offending byte and its index.\n */\nexport function assertValidCookieName(name: string): void {\n if (typeof name !== 'string' || name.length === 0) {\n throw new Error('[timber] cookie name must be a non-empty string.');\n }\n if (!COOKIE_NAME_RE.test(name)) {\n for (let i = 0; i < name.length; i++) {\n if (!COOKIE_NAME_RE.test(name[i])) {\n throw new Error(\n `[timber] invalid cookie name ${JSON.stringify(name)} — ` +\n `disallowed character ${describeChar(name, i)} at index ${i}. ` +\n `Cookie names must match the RFC 6265 §4.1.1 token grammar (US-ASCII letters, digits, ` +\n `and \\`!#$%&'*+-.^_\\`|~\\`; no whitespace, controls, or delimiters).`\n );\n }\n }\n throw new Error(\n `[timber] invalid cookie name ${JSON.stringify(name)} — ` +\n `must match RFC 6265 §4.1.1 token grammar.`\n );\n }\n}\n\n/** Single-byte test for RFC 6265 §4.1.1 cookie-octet. */\nfunction isCookieOctet(code: number): boolean {\n return (\n code === 0x21 ||\n (code >= 0x23 && code <= 0x2b) ||\n (code >= 0x2d && code <= 0x3a) ||\n (code >= 0x3c && code <= 0x5b) ||\n (code >= 0x5d && code <= 0x7e)\n );\n}\n\n/**\n * Validate a cookie value against the RFC 6265 §4.1.1 `cookie-value`\n * production:\n *\n * cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )\n * cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E\n *\n * Both the bare `*cookie-octet` form and the DQUOTE-wrapped form are\n * valid. The wrapped form is common in upstream `Set-Cookie` headers\n * (e.g. `sid=\"abc\"; Path=/`) and must be forwardable through\n * `setFromHeaders` verbatim — stripping or rejecting the surrounding\n * quotes would corrupt the on-the-wire bytes and break value semantics\n * for the upstream service.\n *\n * Excluded bytes (in either form): CTL, whitespace (CR/LF/TAB/SP),\n * interior DQUOTE, comma, semicolon, backslash, DEL, and anything\n * non-ASCII. The smuggling primitive (`;`) is rejected regardless of\n * wrapping.\n *\n * Throws a developer-facing error naming the offending byte and its\n * index. Callers passing user-controlled data through the default\n * `set()` path don't need to call this — `set()` auto-encodes via\n * `encodeURIComponent`, whose output is always valid cookie-octet.\n * This validator is for the `{ raw: true }` opt-out and for\n * `setFromHeaders` forwarding.\n */\nexport function assertValidCookieValue(name: string, value: string): void {\n if (typeof value !== 'string') {\n throw new Error(`[timber] cookie ${JSON.stringify(name)}: cookie value must be a string.`);\n }\n // Detect the DQUOTE-wrapped form: at least two characters and both\n // endpoints are `\"`. The interior must be pure cookie-octet (no\n // interior DQUOTEs allowed — the grammar says *cookie-octet, and\n // cookie-octet excludes 0x22).\n const wrapped =\n value.length >= 2 &&\n value.charCodeAt(0) === 0x22 &&\n value.charCodeAt(value.length - 1) === 0x22;\n const start = wrapped ? 1 : 0;\n const end = wrapped ? value.length - 1 : value.length;\n for (let i = start; i < end; i++) {\n const code = value.charCodeAt(i);\n if (!isCookieOctet(code)) {\n throw new Error(\n `[timber] cookie ${JSON.stringify(name)}: invalid cookie value — ` +\n `disallowed character ${describeChar(value, i)} at index ${i}. ` +\n `Cookie values must contain only RFC 6265 §4.1.1 cookie-octet bytes ` +\n `(US-ASCII printable, excluding whitespace, \\`\"\\`, \\`,\\`, \\`;\\`, and \\`\\\\\\`), ` +\n `optionally enclosed in a single DQUOTE pair. ` +\n `Encode the value (e.g. encodeURIComponent or jsonCookieCodec) before storing it.`\n );\n }\n }\n}\n","/**\n * Shared utility for merging preserved search params into a target URL.\n *\n * Used by both <Link> (client) and redirect() (server). Extracted to a shared\n * module to avoid importing client code ('use client') from server modules.\n */\n\n/**\n * Merge preserved search params from the current URL into a target href.\n *\n * When `preserve` is `true`, all current search params are merged.\n * When `preserve` is a `string[]`, only the named params are merged.\n *\n * The target href's own search params take precedence — preserved params\n * are only added if the target doesn't already define them.\n *\n * @param targetHref - The resolved target href (may already contain query string)\n * @param currentSearch - The current URL's search string (e.g. \"?private=access&page=2\")\n * @param preserve - `true` to preserve all, or `string[]` to preserve specific params\n * @returns The target href with preserved search params merged in\n */\nexport function mergePreservedSearchParams(\n targetHref: string,\n currentSearch: string,\n preserve: true | string[]\n): string {\n const currentParams = new URLSearchParams(currentSearch);\n if (currentParams.size === 0) return targetHref;\n\n // Split hash fragment from target before processing query params.\n // Hash must come after query string: /path?query=value#hash\n const hashIdx = targetHref.indexOf('#');\n const hrefWithoutHash = hashIdx >= 0 ? targetHref.slice(0, hashIdx) : targetHref;\n const hash = hashIdx >= 0 ? targetHref.slice(hashIdx) : '';\n\n // Split target into path and existing query\n const qIdx = hrefWithoutHash.indexOf('?');\n const targetPath = qIdx >= 0 ? hrefWithoutHash.slice(0, qIdx) : hrefWithoutHash;\n const targetParams = new URLSearchParams(qIdx >= 0 ? hrefWithoutHash.slice(qIdx + 1) : '');\n\n // Collect params to preserve (that aren't already in the target)\n const merged = new URLSearchParams(targetParams);\n for (const [key, value] of currentParams) {\n // Only preserve if: (a) not already in target, and (b) included in preserve list\n if (!targetParams.has(key)) {\n if (preserve === true || preserve.includes(key)) {\n merged.append(key, value);\n }\n }\n }\n\n const qs = merged.toString();\n const pathWithQuery = qs ? `${targetPath}?${qs}` : targetPath;\n return pathWithQuery + hash;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA6BA,IAAM,iBAAiB;;;;;AAMvB,SAAS,aAAa,OAAe,OAAuB;CAC1D,MAAM,OAAO,MAAM,WAAW,MAAM;AACpC,KAAI,OAAO,MAAQ,SAAS,OAAQ,OAAO,IACzC,QAAO,KAAK,KAAK,SAAS,GAAG,CAAC,SAAS,GAAG,IAAI;AAEhD,QAAO,KAAK,UAAU,MAAM,OAAO;;;;;;AAOrC,SAAgB,sBAAsB,MAAoB;AACxD,KAAI,OAAO,SAAS,YAAY,KAAK,WAAW,EAC9C,OAAM,IAAI,MAAM,mDAAmD;AAErE,KAAI,CAAC,eAAe,KAAK,KAAK,EAAE;AAC9B,OAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,IAC/B,KAAI,CAAC,eAAe,KAAK,KAAK,GAAG,CAC/B,OAAM,IAAI,MACR,gCAAgC,KAAK,UAAU,KAAK,CAAC,0BAC3B,aAAa,MAAM,EAAE,CAAC,YAAY,EAAE,2JAG/D;AAGL,QAAM,IAAI,MACR,gCAAgC,KAAK,UAAU,KAAK,CAAC,8CAEtD;;;;AAKL,SAAS,cAAc,MAAuB;AAC5C,QACE,SAAS,MACR,QAAQ,MAAQ,QAAQ,MACxB,QAAQ,MAAQ,QAAQ,MACxB,QAAQ,MAAQ,QAAQ,MACxB,QAAQ,MAAQ,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8B7B,SAAgB,uBAAuB,MAAc,OAAqB;AACxE,KAAI,OAAO,UAAU,SACnB,OAAM,IAAI,MAAM,mBAAmB,KAAK,UAAU,KAAK,CAAC,kCAAkC;CAM5F,MAAM,UACJ,MAAM,UAAU,KAChB,MAAM,WAAW,EAAE,KAAK,MACxB,MAAM,WAAW,MAAM,SAAS,EAAE,KAAK;CACzC,MAAM,QAAQ,UAAU,IAAI;CAC5B,MAAM,MAAM,UAAU,MAAM,SAAS,IAAI,MAAM;AAC/C,MAAK,IAAI,IAAI,OAAO,IAAI,KAAK,IAE3B,KAAI,CAAC,cADQ,MAAM,WAAW,EAAE,CACR,CACtB,OAAM,IAAI,MACR,mBAAmB,KAAK,UAAU,KAAK,CAAC,gDACd,aAAa,OAAO,EAAE,CAAC,YAAY,EAAE,iRAKhE;;;;;;;;;;;;;;;;;;;;;;;;AC7GP,SAAgB,2BACd,YACA,eACA,UACQ;CACR,MAAM,gBAAgB,IAAI,gBAAgB,cAAc;AACxD,KAAI,cAAc,SAAS,EAAG,QAAO;CAIrC,MAAM,UAAU,WAAW,QAAQ,IAAI;CACvC,MAAM,kBAAkB,WAAW,IAAI,WAAW,MAAM,GAAG,QAAQ,GAAG;CACtE,MAAM,OAAO,WAAW,IAAI,WAAW,MAAM,QAAQ,GAAG;CAGxD,MAAM,OAAO,gBAAgB,QAAQ,IAAI;CACzC,MAAM,aAAa,QAAQ,IAAI,gBAAgB,MAAM,GAAG,KAAK,GAAG;CAChE,MAAM,eAAe,IAAI,gBAAgB,QAAQ,IAAI,gBAAgB,MAAM,OAAO,EAAE,GAAG,GAAG;CAG1F,MAAM,SAAS,IAAI,gBAAgB,aAAa;AAChD,MAAK,MAAM,CAAC,KAAK,UAAU,cAEzB,KAAI,CAAC,aAAa,IAAI,IAAI;MACpB,aAAa,QAAQ,SAAS,SAAS,IAAI,CAC7C,QAAO,OAAO,KAAK,MAAM;;CAK/B,MAAM,KAAK,OAAO,UAAU;AAE5B,SADsB,KAAK,GAAG,WAAW,GAAG,OAAO,cAC5B"}
@@ -1,26 +0,0 @@
1
- import type { Plugin } from 'vite';
2
- /**
3
- * Rewrite 'use server' module exports to bypass RSC plugin AST validation.
4
- *
5
- * The RSC plugin's client/SSR proxy transform (transformProxyExport) requires
6
- * all `export const` initializers to be `async ArrowFunctionExpression`. But
7
- * `createActionClient().action()` and `validated()` return async functions via
8
- * CallExpressions — valid at runtime but rejected by the static AST check.
9
- *
10
- * This plugin rewrites non-function-expression exports from:
11
- *
12
- * export const foo = someCall();
13
- *
14
- * to:
15
- *
16
- * const foo = someCall();
17
- * export { foo };
18
- *
19
- * The `export { name }` form bypasses the RSC plugin's validation without
20
- * changing runtime semantics. Function expression exports (async arrows,
21
- * async function expressions) are left untouched.
22
- *
23
- * See design/08-forms-and-actions.md §"Middleware for Server Actions"
24
- */
25
- export declare function timberServerActionExports(): Plugin;
26
- //# sourceMappingURL=server-action-exports.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"server-action-exports.d.ts","sourceRoot":"","sources":["../../src/plugins/server-action-exports.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAOnC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,CAelD"}
@@ -1,34 +0,0 @@
1
- /**
2
- * Lint status files and error.tsx for missing 'use client' directive.
3
- *
4
- * Status files (error.tsx, 404.tsx, 4xx.tsx, 5xx.tsx, NNN.tsx) and legacy
5
- * compat files (not-found.tsx, forbidden.tsx, unauthorized.tsx) are passed
6
- * as fallbackComponent to TimberErrorBoundary — a 'use client' component.
7
- * RSC forbids passing server component functions as props to client
8
- * components, causing a hard-to-debug runtime error.
9
- *
10
- * This module provides a build/dev-time check that warns when these files
11
- * are missing the 'use client' directive.
12
- *
13
- * See design/10-error-handling.md §"Status-Code Files".
14
- */
15
- import type { RouteTree } from './types.js';
16
- export interface StatusFileLintWarning {
17
- filePath: string;
18
- fileType: string;
19
- }
20
- /**
21
- * Walk the route tree and check all status files and error files for
22
- * the 'use client' directive. Returns an array of warnings for files
23
- * that are missing it.
24
- *
25
- * MDX and JSON status files are excluded — MDX files are server components
26
- * by design (pre-rendered as elements via fallbackElement, see TIM-503),
27
- * and JSON files are data, not components.
28
- */
29
- export declare function lintStatusFileDirectives(tree: RouteTree): StatusFileLintWarning[];
30
- /**
31
- * Format warnings into human-readable console output.
32
- */
33
- export declare function formatStatusFileLintWarnings(warnings: StatusFileLintWarning[]): string;
34
- //# sourceMappingURL=status-file-lint.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"status-file-lint.d.ts","sourceRoot":"","sources":["../../src/routing/status-file-lint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,YAAY,CAAC;AAOzD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,SAAS,GAAG,qBAAqB,EAAE,CAIjF;AAqDD;;GAEG;AACH,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,qBAAqB,EAAE,GAAG,MAAM,CAiBtF"}
@@ -1,28 +0,0 @@
1
- /**
2
- * AST-based directive detection for 'use client' and 'use server'.
3
- *
4
- * Uses acorn to parse source code and detect directives properly,
5
- * avoiding false positives from regex matching inside string literals,
6
- * comments, or template expressions.
7
- *
8
- * @module
9
- */
10
- export interface FileDirective {
11
- /** The directive value, e.g. 'use client', 'use server' */
12
- directive: string;
13
- /** 1-based line number where the directive appears */
14
- line: number;
15
- }
16
- /**
17
- * Detect a file-level directive ('use client', 'use server', etc.).
18
- *
19
- * Per the ECMAScript spec, directives are string literal expression
20
- * statements at the start of a program body (before any non-directive
21
- * statements). This function checks the AST `Program.body` for
22
- * `ExpressionStatement` nodes whose expression is a `Literal` string
23
- * matching a known directive.
24
- *
25
- * Returns the first matching directive, or null if none found.
26
- */
27
- export declare function detectFileDirective(code: string, directives?: string[]): FileDirective | null;
28
- //# sourceMappingURL=directive-parser.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"directive-parser.d.ts","sourceRoot":"","sources":["../../src/utils/directive-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAYH,MAAM,WAAW,aAAa;IAC5B,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;CACd;AAMD;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,UAAU,GAAE,MAAM,EAAiC,GAClD,aAAa,GAAG,IAAI,CAmCtB"}
@@ -1,220 +0,0 @@
1
- import type { Plugin } from 'vite';
2
- import { Parser } from 'acorn';
3
- import acornJsx from 'acorn-jsx';
4
- import { detectFileDirective } from '../utils/directive-parser.js';
5
-
6
- const jsxParser = Parser.extend(acornJsx());
7
-
8
- /**
9
- * Rewrite 'use server' module exports to bypass RSC plugin AST validation.
10
- *
11
- * The RSC plugin's client/SSR proxy transform (transformProxyExport) requires
12
- * all `export const` initializers to be `async ArrowFunctionExpression`. But
13
- * `createActionClient().action()` and `validated()` return async functions via
14
- * CallExpressions — valid at runtime but rejected by the static AST check.
15
- *
16
- * This plugin rewrites non-function-expression exports from:
17
- *
18
- * export const foo = someCall();
19
- *
20
- * to:
21
- *
22
- * const foo = someCall();
23
- * export { foo };
24
- *
25
- * The `export { name }` form bypasses the RSC plugin's validation without
26
- * changing runtime semantics. Function expression exports (async arrows,
27
- * async function expressions) are left untouched.
28
- *
29
- * See design/08-forms-and-actions.md §"Middleware for Server Actions"
30
- */
31
- export function timberServerActionExports(): Plugin {
32
- return {
33
- name: 'timber-server-action-exports',
34
- transform(code, id) {
35
- // Skip non-JS/TS files
36
- if (!/\.[jt]sx?$/.test(id)) return;
37
- // Quick bail-out
38
- if (!code.includes('use server')) return;
39
- // Check for file-level directive
40
- const directive = detectFileDirective(code, ['use server']);
41
- if (!directive) return;
42
-
43
- return rewriteServerActionExports(code);
44
- },
45
- };
46
- }
47
-
48
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
- type AcornNode = any;
50
-
51
- /**
52
- * Check if an AST node is an async function expression (arrow or regular).
53
- * These are handled correctly by the RSC plugin and don't need rewriting.
54
- */
55
- function isAsyncFunctionExpr(node: AcornNode): boolean {
56
- if (!node) return false;
57
- return (
58
- (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') && node.async
59
- );
60
- }
61
-
62
- /**
63
- * Extract identifier names from a declarator's id pattern.
64
- * Handles simple identifiers and destructuring patterns.
65
- */
66
- function extractDeclNames(id: AcornNode): string[] {
67
- if (id.type === 'Identifier') return [id.name];
68
-
69
- // ObjectPattern: const { a, b } = ...
70
- if (id.type === 'ObjectPattern') {
71
- return id.properties.flatMap((p: AcornNode) => extractDeclNames(p.value ?? p.argument));
72
- }
73
-
74
- // ArrayPattern: const [a, b] = ...
75
- if (id.type === 'ArrayPattern') {
76
- return id.elements.filter(Boolean).flatMap((e: AcornNode) => extractDeclNames(e));
77
- }
78
-
79
- // RestElement: const [...rest] = ...
80
- if (id.type === 'RestElement') {
81
- return extractDeclNames(id.argument);
82
- }
83
-
84
- // AssignmentPattern: const { a = 1 } = ...
85
- if (id.type === 'AssignmentPattern') {
86
- return extractDeclNames(id.left);
87
- }
88
-
89
- return [];
90
- }
91
-
92
- interface RewriteTarget {
93
- /** Start offset of the `export` keyword */
94
- exportStart: number;
95
- /** Start offset of the `const/let/var` keyword (right after `export `) */
96
- declStart: number;
97
- /** Names to re-export */
98
- names: string[];
99
- }
100
-
101
- function rewriteServerActionExports(code: string): { code: string; map: null } | undefined {
102
- let ast: AcornNode;
103
- try {
104
- ast = jsxParser.parse(code, {
105
- ecmaVersion: 'latest',
106
- sourceType: 'module',
107
- });
108
- } catch {
109
- // TypeScript that esbuild hasn't stripped yet — use regex fallback
110
- return rewriteServerActionExportsFallback(code);
111
- }
112
-
113
- const targets: RewriteTarget[] = [];
114
-
115
- for (const node of ast.body) {
116
- // Case 1: export const/let/var name = <non-function-expression>
117
- if (
118
- node.type === 'ExportNamedDeclaration' &&
119
- node.declaration?.type === 'VariableDeclaration'
120
- ) {
121
- const hasNonFunctionInit = node.declaration.declarations.some(
122
- (d: AcornNode) => d.init && !isAsyncFunctionExpr(d.init)
123
- );
124
- if (!hasNonFunctionInit) continue;
125
-
126
- const names = node.declaration.declarations.flatMap((d: AcornNode) => extractDeclNames(d.id));
127
- if (names.length === 0) continue;
128
-
129
- targets.push({
130
- exportStart: node.start,
131
- declStart: node.declaration.start,
132
- names,
133
- });
134
- }
135
-
136
- // Case 2: export default <non-function-expression>
137
- if (node.type === 'ExportDefaultDeclaration') {
138
- const decl = node.declaration;
139
- if (
140
- decl.type !== 'FunctionDeclaration' &&
141
- decl.type !== 'Identifier' &&
142
- !isAsyncFunctionExpr(decl)
143
- ) {
144
- // export default someCall() → const $$default = someCall(); export default $$default;
145
- targets.push({
146
- exportStart: node.start,
147
- declStart: -1, // sentinel for default export
148
- names: ['default'],
149
- });
150
- }
151
- }
152
- }
153
-
154
- if (targets.length === 0) return undefined;
155
-
156
- // Process from end to start to preserve character positions
157
- let result = code;
158
- const reExports: string[] = [];
159
-
160
- for (let i = targets.length - 1; i >= 0; i--) {
161
- const target = targets[i];
162
-
163
- if (target.declStart === -1) {
164
- // export default <expr> → const $$default = <expr>;\nexport default $$default;
165
- const node = ast.body.find(
166
- (n: AcornNode) => n.type === 'ExportDefaultDeclaration' && n.start === target.exportStart
167
- );
168
- if (node) {
169
- const exprCode = code.slice(node.declaration.start, node.declaration.end);
170
- const replacement = `const $$default = ${exprCode};\nexport default $$default;`;
171
- result = result.slice(0, node.start) + replacement + result.slice(node.end);
172
- }
173
- continue;
174
- }
175
-
176
- // Remove 'export ' from the declaration
177
- result = result.slice(0, target.exportStart) + result.slice(target.declStart);
178
- reExports.push(...target.names);
179
- }
180
-
181
- if (reExports.length > 0) {
182
- result += '\nexport { ' + reExports.join(', ') + ' };\n';
183
- }
184
-
185
- return { code: result, map: null };
186
- }
187
-
188
- /**
189
- * Regex fallback for TypeScript files that acorn cannot parse.
190
- *
191
- * Matches `export const/let/var name = <expr>` where the initializer does
192
- * NOT start with `async function` or `async (` (i.e., not an async
193
- * function expression or async arrow function).
194
- */
195
- function rewriteServerActionExportsFallback(code: string): { code: string; map: null } | undefined {
196
- // Match: export const/let/var <name> = <non-async-function-expr>
197
- const pattern =
198
- /^(export\s+)((?:const|let|var)\s+(\w+)\s*=\s*)(?!async\s+(?:function[\s(]|\())/gm;
199
-
200
- const names: string[] = [];
201
- let result = code;
202
- let offset = 0;
203
- let match;
204
-
205
- while ((match = pattern.exec(code)) !== null) {
206
- const exportKeyword = match[1]; // 'export ' (with trailing space)
207
- const name = match[3];
208
-
209
- // Remove 'export ' at this position (adjusted for prior removals)
210
- const pos = match.index + offset;
211
- result = result.slice(0, pos) + result.slice(pos + exportKeyword.length);
212
- offset -= exportKeyword.length;
213
- names.push(name);
214
- }
215
-
216
- if (names.length === 0) return undefined;
217
-
218
- result += '\nexport { ' + names.join(', ') + ' };\n';
219
- return { code: result, map: null };
220
- }
@@ -1,115 +0,0 @@
1
- /**
2
- * Lint status files and error.tsx for missing 'use client' directive.
3
- *
4
- * Status files (error.tsx, 404.tsx, 4xx.tsx, 5xx.tsx, NNN.tsx) and legacy
5
- * compat files (not-found.tsx, forbidden.tsx, unauthorized.tsx) are passed
6
- * as fallbackComponent to TimberErrorBoundary — a 'use client' component.
7
- * RSC forbids passing server component functions as props to client
8
- * components, causing a hard-to-debug runtime error.
9
- *
10
- * This module provides a build/dev-time check that warns when these files
11
- * are missing the 'use client' directive.
12
- *
13
- * See design/10-error-handling.md §"Status-Code Files".
14
- */
15
-
16
- import { readFileSync } from 'node:fs';
17
- import type { RouteTree, SegmentNode } from './types.js';
18
- import { detectFileDirective } from '../utils/directive-parser.js';
19
- import { swallow } from '../server/logger.js';
20
-
21
- /** Extensions that require 'use client' (component files, not MDX/JSON). */
22
- const CLIENT_REQUIRED_EXTENSIONS = new Set(['tsx', 'jsx', 'ts', 'js']);
23
-
24
- export interface StatusFileLintWarning {
25
- filePath: string;
26
- fileType: string;
27
- }
28
-
29
- /**
30
- * Walk the route tree and check all status files and error files for
31
- * the 'use client' directive. Returns an array of warnings for files
32
- * that are missing it.
33
- *
34
- * MDX and JSON status files are excluded — MDX files are server components
35
- * by design (pre-rendered as elements via fallbackElement, see TIM-503),
36
- * and JSON files are data, not components.
37
- */
38
- export function lintStatusFileDirectives(tree: RouteTree): StatusFileLintWarning[] {
39
- const warnings: StatusFileLintWarning[] = [];
40
- walkNode(tree.root, warnings);
41
- return warnings;
42
- }
43
-
44
- function walkNode(node: SegmentNode, warnings: StatusFileLintWarning[]): void {
45
- // Check error.tsx
46
- if (node.error) {
47
- checkFile(node.error.filePath, node.error.extension, 'error', warnings);
48
- }
49
-
50
- // Check status-code files (404.tsx, 4xx.tsx, 5xx.tsx, etc.)
51
- if (node.statusFiles) {
52
- for (const [code, file] of Object.entries(node.statusFiles)) {
53
- checkFile(file.filePath, file.extension, code, warnings);
54
- }
55
- }
56
-
57
- // Check legacy compat files (not-found.tsx, forbidden.tsx, unauthorized.tsx)
58
- if (node.legacyStatusFiles) {
59
- for (const [name, file] of Object.entries(node.legacyStatusFiles)) {
60
- checkFile(file.filePath, file.extension, name, warnings);
61
- }
62
- }
63
-
64
- // Recurse into children and slots
65
- for (const child of node.children) {
66
- walkNode(child, warnings);
67
- }
68
- for (const slotNode of Object.values(node.slots)) {
69
- walkNode(slotNode, warnings);
70
- }
71
- }
72
-
73
- function checkFile(
74
- filePath: string,
75
- extension: string,
76
- fileType: string,
77
- warnings: StatusFileLintWarning[]
78
- ): void {
79
- if (!CLIENT_REQUIRED_EXTENSIONS.has(extension)) return;
80
-
81
- let code: string;
82
- try {
83
- code = readFileSync(filePath, 'utf-8');
84
- } catch (err) {
85
- swallow(err, `status-file-lint: unreadable file ${filePath}`);
86
- return;
87
- }
88
-
89
- const directive = detectFileDirective(code, ['use client']);
90
- if (!directive) {
91
- warnings.push({ filePath, fileType });
92
- }
93
- }
94
-
95
- /**
96
- * Format warnings into human-readable console output.
97
- */
98
- export function formatStatusFileLintWarnings(warnings: StatusFileLintWarning[]): string {
99
- const lines = [
100
- `[timber] ${warnings.length} status/error file${warnings.length > 1 ? 's' : ''} missing 'use client' directive:`,
101
- '',
102
- ];
103
-
104
- for (const w of warnings) {
105
- lines.push(` ${w.filePath}`);
106
- }
107
-
108
- lines.push('');
109
- lines.push(
110
- " Status files and error.tsx are rendered inside TimberErrorBoundary (a 'use client' component)."
111
- );
112
- lines.push(" Add 'use client' as the first line of each file to avoid a runtime error.");
113
-
114
- return lines.join('\n');
115
- }
@@ -1,140 +0,0 @@
1
- /**
2
- * AST-based directive detection for 'use client' and 'use server'.
3
- *
4
- * Uses acorn to parse source code and detect directives properly,
5
- * avoiding false positives from regex matching inside string literals,
6
- * comments, or template expressions.
7
- *
8
- * @module
9
- */
10
-
11
- import { Parser } from 'acorn';
12
- import acornJsx from 'acorn-jsx';
13
-
14
- // acorn parser with JSX support
15
- const jsxParser = Parser.extend(acornJsx());
16
-
17
- // ---------------------------------------------------------------------------
18
- // Types
19
- // ---------------------------------------------------------------------------
20
-
21
- export interface FileDirective {
22
- /** The directive value, e.g. 'use client', 'use server' */
23
- directive: string;
24
- /** 1-based line number where the directive appears */
25
- line: number;
26
- }
27
-
28
- // ---------------------------------------------------------------------------
29
- // File-level directive detection
30
- // ---------------------------------------------------------------------------
31
-
32
- /**
33
- * Detect a file-level directive ('use client', 'use server', etc.).
34
- *
35
- * Per the ECMAScript spec, directives are string literal expression
36
- * statements at the start of a program body (before any non-directive
37
- * statements). This function checks the AST `Program.body` for
38
- * `ExpressionStatement` nodes whose expression is a `Literal` string
39
- * matching a known directive.
40
- *
41
- * Returns the first matching directive, or null if none found.
42
- */
43
- export function detectFileDirective(
44
- code: string,
45
- directives: string[] = ['use client', 'use server']
46
- ): FileDirective | null {
47
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
- let ast: any;
49
- try {
50
- ast = jsxParser.parse(code, {
51
- ecmaVersion: 'latest',
52
- sourceType: 'module',
53
- locations: true,
54
- });
55
- } catch {
56
- // If the file fails to parse (e.g. TypeScript syntax), fall back to
57
- // a safe line-by-line check that only considers lines before any
58
- // non-comment, non-whitespace, non-directive content.
59
- return detectFileDirectiveFallback(code, directives);
60
- }
61
-
62
- for (const node of ast.body) {
63
- if (
64
- node.type === 'ExpressionStatement' &&
65
- node.expression.type === 'Literal' &&
66
- typeof node.expression.value === 'string'
67
- ) {
68
- if (directives.includes(node.expression.value)) {
69
- return {
70
- directive: node.expression.value,
71
- line: node.loc.start.line,
72
- };
73
- }
74
- } else {
75
- // Directives must appear before any non-directive statements
76
- break;
77
- }
78
- }
79
-
80
- return null;
81
- }
82
-
83
- /**
84
- * Fallback for TypeScript files that acorn cannot parse.
85
- *
86
- * Scans lines from the top of the file. Skips blank lines and comments.
87
- * Checks if the first real statement is a directive string literal.
88
- */
89
- function detectFileDirectiveFallback(code: string, directives: string[]): FileDirective | null {
90
- const lines = code.split('\n');
91
- let inBlockComment = false;
92
-
93
- for (let i = 0; i < lines.length; i++) {
94
- let line = lines[i].trim();
95
-
96
- // Handle block comments
97
- if (inBlockComment) {
98
- const endIdx = line.indexOf('*/');
99
- if (endIdx === -1) continue;
100
- line = line.slice(endIdx + 2).trim();
101
- inBlockComment = false;
102
- if (!line) continue;
103
- }
104
-
105
- // Skip blank lines
106
- if (!line) continue;
107
-
108
- // Skip line comments
109
- if (line.startsWith('//')) continue;
110
-
111
- // Skip block comment start
112
- if (line.startsWith('/*')) {
113
- const endIdx = line.indexOf('*/', 2);
114
- if (endIdx === -1) {
115
- inBlockComment = true;
116
- continue;
117
- }
118
- line = line.slice(endIdx + 2).trim();
119
- if (!line) continue;
120
- }
121
-
122
- // Check for directive
123
- for (const dir of directives) {
124
- // Match 'directive' or "directive" optionally followed by ;
125
- if (
126
- line === `'${dir}'` ||
127
- line === `'${dir}';` ||
128
- line === `"${dir}"` ||
129
- line === `"${dir}";`
130
- ) {
131
- return { directive: dir, line: i + 1 };
132
- }
133
- }
134
-
135
- // First non-comment, non-blank line is not a directive — stop
136
- break;
137
- }
138
-
139
- return null;
140
- }