@timber-js/app 0.2.0-alpha.71 → 0.2.0-alpha.73

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 (248) hide show
  1. package/dist/_chunks/actions-Dg-ANYHb.js +421 -0
  2. package/dist/_chunks/actions-Dg-ANYHb.js.map +1 -0
  3. package/dist/_chunks/{als-registry-BJARkOcu.js → als-registry-HS0LGUl2.js} +1 -1
  4. package/dist/_chunks/als-registry-HS0LGUl2.js.map +1 -0
  5. package/dist/_chunks/{define-Dz1bqwaS.js → define-C77ScO0m.js} +14 -14
  6. package/dist/_chunks/define-C77ScO0m.js.map +1 -0
  7. package/dist/_chunks/{define-CGuYoRHU.js → define-CZqDwhSu.js} +15 -15
  8. package/dist/_chunks/define-CZqDwhSu.js.map +1 -0
  9. package/dist/_chunks/{define-cookie-B5mewxwM.js → define-cookie-C2IkoFGN.js} +9 -8
  10. package/dist/_chunks/{define-cookie-B5mewxwM.js.map → define-cookie-C2IkoFGN.js.map} +1 -1
  11. package/dist/_chunks/{format-Rn922VH2.js → dev-warnings-DpGRGoDi.js} +4 -26
  12. package/dist/_chunks/dev-warnings-DpGRGoDi.js.map +1 -0
  13. package/dist/_chunks/format-CYBGxKtc.js +14 -0
  14. package/dist/_chunks/format-CYBGxKtc.js.map +1 -0
  15. package/dist/_chunks/{interception-CEdHHviP.js → interception-Dpn_UfAD.js} +2 -2
  16. package/dist/_chunks/{interception-CEdHHviP.js.map → interception-Dpn_UfAD.js.map} +1 -1
  17. package/dist/_chunks/{segment-context-hzuJ048X.js → merge-search-params-Cm_KIWDX.js} +2 -33
  18. package/dist/_chunks/merge-search-params-Cm_KIWDX.js.map +1 -0
  19. package/dist/_chunks/{request-context-CywiO4jV.js → request-context-qMsWgy9C.js} +72 -36
  20. package/dist/_chunks/request-context-qMsWgy9C.js.map +1 -0
  21. package/dist/_chunks/{schema-bridge-C4SwjCQD.js → schema-bridge-C3xl_vfb.js} +1 -1
  22. package/dist/_chunks/{schema-bridge-C4SwjCQD.js.map → schema-bridge-C3xl_vfb.js.map} +1 -1
  23. package/dist/_chunks/segment-context-fHFLF1PE.js +34 -0
  24. package/dist/_chunks/segment-context-fHFLF1PE.js.map +1 -0
  25. package/dist/_chunks/ssr-data-DzuI0bIV.js +88 -0
  26. package/dist/_chunks/ssr-data-DzuI0bIV.js.map +1 -0
  27. package/dist/_chunks/{stale-reload-BLUC_Pl_.js → stale-reload-C2plcNtG.js} +1 -1
  28. package/dist/_chunks/{stale-reload-BLUC_Pl_.js.map → stale-reload-C2plcNtG.js.map} +1 -1
  29. package/dist/_chunks/{handler-store-BVePM7hp.js → tracing-CCYbKn5n.js} +60 -60
  30. package/dist/_chunks/tracing-CCYbKn5n.js.map +1 -0
  31. package/dist/_chunks/use-params-B1AuhI1p.js +307 -0
  32. package/dist/_chunks/use-params-B1AuhI1p.js.map +1 -0
  33. package/dist/_chunks/{use-query-states-DAhgj8Gx.js → use-query-states-Lo_s_pw2.js} +4 -4
  34. package/dist/_chunks/use-query-states-Lo_s_pw2.js.map +1 -0
  35. package/dist/_chunks/{wrappers-LZbghvn0.js → wrappers-_DTmImGt.js} +1 -1
  36. package/dist/_chunks/{wrappers-LZbghvn0.js.map → wrappers-_DTmImGt.js.map} +1 -1
  37. package/dist/adapters/cloudflare-kv-cache.d.ts +64 -0
  38. package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -0
  39. package/dist/adapters/cloudflare-kv-cache.js +95 -0
  40. package/dist/adapters/cloudflare-kv-cache.js.map +1 -0
  41. package/dist/cache/index.d.ts +18 -4
  42. package/dist/cache/index.d.ts.map +1 -1
  43. package/dist/cache/index.js +78 -12
  44. package/dist/cache/index.js.map +1 -1
  45. package/dist/cache/sizeof.d.ts +22 -0
  46. package/dist/cache/sizeof.d.ts.map +1 -0
  47. package/dist/cli.d.ts +6 -1
  48. package/dist/cli.d.ts.map +1 -1
  49. package/dist/cli.js +6 -1
  50. package/dist/cli.js.map +1 -1
  51. package/dist/client/browser-dev.d.ts +27 -1
  52. package/dist/client/browser-dev.d.ts.map +1 -1
  53. package/dist/client/browser-entry/action-dispatch.d.ts +17 -0
  54. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -0
  55. package/dist/client/browser-entry/hmr.d.ts +21 -0
  56. package/dist/client/browser-entry/hmr.d.ts.map +1 -0
  57. package/dist/client/browser-entry/hydrate.d.ts +46 -0
  58. package/dist/client/browser-entry/hydrate.d.ts.map +1 -0
  59. package/dist/client/browser-entry/index.d.ts +30 -0
  60. package/dist/client/browser-entry/index.d.ts.map +1 -0
  61. package/dist/client/browser-entry/post-hydration.d.ts +26 -0
  62. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -0
  63. package/dist/client/browser-entry/router-init.d.ts +23 -0
  64. package/dist/client/browser-entry/router-init.d.ts.map +1 -0
  65. package/dist/client/browser-entry/rsc-stream.d.ts +24 -0
  66. package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -0
  67. package/dist/client/browser-entry/scroll.d.ts +19 -0
  68. package/dist/client/browser-entry/scroll.d.ts.map +1 -0
  69. package/dist/client/error-boundary.js +131 -1
  70. package/dist/client/error-boundary.js.map +1 -0
  71. package/dist/client/index.d.ts +4 -19
  72. package/dist/client/index.d.ts.map +1 -1
  73. package/dist/client/index.js +14 -1191
  74. package/dist/client/index.js.map +1 -1
  75. package/dist/client/internal.d.ts +18 -0
  76. package/dist/client/internal.d.ts.map +1 -0
  77. package/dist/client/internal.js +890 -0
  78. package/dist/client/internal.js.map +1 -0
  79. package/dist/client/navigation-context.d.ts.map +1 -1
  80. package/dist/client/router-ref.d.ts +1 -1
  81. package/dist/client/top-loader.d.ts +2 -2
  82. package/dist/client/use-link-status.d.ts +1 -1
  83. package/dist/client/{use-navigation-pending.d.ts → use-pending-navigation.d.ts} +4 -4
  84. package/dist/client/use-pending-navigation.d.ts.map +1 -0
  85. package/dist/client/use-router.d.ts +1 -1
  86. package/dist/codec.d.ts +10 -0
  87. package/dist/codec.d.ts.map +1 -1
  88. package/dist/codec.js +1 -1
  89. package/dist/config-types.d.ts +210 -0
  90. package/dist/config-types.d.ts.map +1 -0
  91. package/dist/content/index.d.ts +1 -10
  92. package/dist/content/index.d.ts.map +1 -1
  93. package/dist/content/index.js +0 -2
  94. package/dist/cookies/define-cookie.d.ts.map +1 -1
  95. package/dist/cookies/index.d.ts +0 -2
  96. package/dist/cookies/index.d.ts.map +1 -1
  97. package/dist/cookies/index.js +2 -3
  98. package/dist/index.d.ts +25 -288
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +261 -43
  101. package/dist/index.js.map +1 -1
  102. package/dist/plugin-context.d.ts +84 -0
  103. package/dist/plugin-context.d.ts.map +1 -0
  104. package/dist/plugins/adapter-build.d.ts +1 -1
  105. package/dist/plugins/adapter-build.d.ts.map +1 -1
  106. package/dist/plugins/build-manifest.d.ts +1 -1
  107. package/dist/plugins/build-manifest.d.ts.map +1 -1
  108. package/dist/plugins/build-report.d.ts +1 -1
  109. package/dist/plugins/build-report.d.ts.map +1 -1
  110. package/dist/plugins/content.d.ts +1 -1
  111. package/dist/plugins/content.d.ts.map +1 -1
  112. package/dist/plugins/dev-browser-logs.d.ts +1 -1
  113. package/dist/plugins/dev-browser-logs.d.ts.map +1 -1
  114. package/dist/plugins/dev-logs.d.ts +1 -1
  115. package/dist/plugins/dev-logs.d.ts.map +1 -1
  116. package/dist/plugins/dev-server.d.ts +1 -1
  117. package/dist/plugins/dev-server.d.ts.map +1 -1
  118. package/dist/plugins/entries.d.ts +1 -1
  119. package/dist/plugins/entries.d.ts.map +1 -1
  120. package/dist/plugins/fonts.d.ts +1 -1
  121. package/dist/plugins/fonts.d.ts.map +1 -1
  122. package/dist/plugins/mdx.d.ts +1 -1
  123. package/dist/plugins/mdx.d.ts.map +1 -1
  124. package/dist/plugins/routing.d.ts +1 -1
  125. package/dist/plugins/routing.d.ts.map +1 -1
  126. package/dist/plugins/shims.d.ts +1 -1
  127. package/dist/plugins/shims.d.ts.map +1 -1
  128. package/dist/plugins/static-build.d.ts +4 -4
  129. package/dist/plugins/static-build.d.ts.map +1 -1
  130. package/dist/routing/index.js +1 -1
  131. package/dist/search-params/define.d.ts +6 -6
  132. package/dist/search-params/define.d.ts.map +1 -1
  133. package/dist/search-params/index.d.ts +1 -2
  134. package/dist/search-params/index.d.ts.map +1 -1
  135. package/dist/search-params/index.js +4 -4
  136. package/dist/search-params/registry.d.ts +1 -1
  137. package/dist/search-params/registry.d.ts.map +1 -1
  138. package/dist/segment-params/define.d.ts +6 -6
  139. package/dist/segment-params/define.d.ts.map +1 -1
  140. package/dist/segment-params/index.d.ts +0 -1
  141. package/dist/segment-params/index.d.ts.map +1 -1
  142. package/dist/segment-params/index.js +3 -3
  143. package/dist/server/als-registry.d.ts +1 -1
  144. package/dist/server/dev-holding-server.d.ts +52 -0
  145. package/dist/server/dev-holding-server.d.ts.map +1 -0
  146. package/dist/server/dev-warnings.d.ts +1 -7
  147. package/dist/server/dev-warnings.d.ts.map +1 -1
  148. package/dist/server/index.d.ts +6 -45
  149. package/dist/server/index.d.ts.map +1 -1
  150. package/dist/server/index.js +7 -3272
  151. package/dist/server/index.js.map +1 -1
  152. package/dist/server/internal.d.ts +46 -0
  153. package/dist/server/internal.d.ts.map +1 -0
  154. package/dist/server/internal.js +2865 -0
  155. package/dist/server/internal.js.map +1 -0
  156. package/dist/server/pipeline.d.ts.map +1 -1
  157. package/dist/server/primitives.d.ts +41 -17
  158. package/dist/server/primitives.d.ts.map +1 -1
  159. package/dist/server/request-context.d.ts +45 -15
  160. package/dist/server/request-context.d.ts.map +1 -1
  161. package/dist/server/tracing.d.ts +4 -4
  162. package/dist/server/tracing.d.ts.map +1 -1
  163. package/dist/shims/headers.d.ts +2 -1
  164. package/dist/shims/headers.d.ts.map +1 -1
  165. package/dist/shims/navigation.d.ts +2 -1
  166. package/dist/shims/navigation.d.ts.map +1 -1
  167. package/package.json +19 -13
  168. package/src/adapters/cloudflare-kv-cache.ts +142 -0
  169. package/src/cache/handler-store.ts +2 -2
  170. package/src/cache/index.ts +74 -15
  171. package/src/cache/sizeof.ts +31 -0
  172. package/src/cli.ts +6 -1
  173. package/src/client/browser-dev.ts +128 -1
  174. package/src/client/browser-entry/action-dispatch.ts +116 -0
  175. package/src/client/browser-entry/hmr.ts +81 -0
  176. package/src/client/browser-entry/hydrate.ts +145 -0
  177. package/src/client/browser-entry/index.ts +138 -0
  178. package/src/client/browser-entry/post-hydration.ts +119 -0
  179. package/src/client/browser-entry/router-init.ts +184 -0
  180. package/src/client/browser-entry/rsc-stream.ts +157 -0
  181. package/src/client/browser-entry/scroll.ts +27 -0
  182. package/src/client/index.ts +10 -38
  183. package/src/client/internal.ts +57 -0
  184. package/src/client/navigation-context.ts +6 -2
  185. package/src/client/navigation-root.tsx +1 -1
  186. package/src/client/router-ref.ts +1 -1
  187. package/src/client/top-loader.tsx +2 -2
  188. package/src/client/use-link-status.ts +1 -1
  189. package/src/client/{use-navigation-pending.ts → use-pending-navigation.ts} +5 -5
  190. package/src/client/use-query-states.ts +2 -2
  191. package/src/client/use-router.ts +1 -1
  192. package/src/codec.ts +15 -0
  193. package/src/config-types.ts +208 -0
  194. package/src/content/index.ts +5 -13
  195. package/src/cookies/define-cookie.ts +9 -7
  196. package/src/cookies/index.ts +6 -5
  197. package/src/index.ts +84 -416
  198. package/src/plugin-context.ts +200 -0
  199. package/src/plugins/adapter-build.ts +1 -1
  200. package/src/plugins/build-manifest.ts +1 -1
  201. package/src/plugins/build-report.ts +1 -1
  202. package/src/plugins/content.ts +1 -1
  203. package/src/plugins/dev-browser-logs.ts +1 -1
  204. package/src/plugins/dev-logs.ts +1 -1
  205. package/src/plugins/dev-server.ts +16 -1
  206. package/src/plugins/entries.ts +2 -2
  207. package/src/plugins/fonts.ts +4 -3
  208. package/src/plugins/mdx.ts +1 -1
  209. package/src/plugins/routing.ts +1 -1
  210. package/src/plugins/shims.ts +53 -5
  211. package/src/plugins/static-build.ts +8 -6
  212. package/src/search-params/define.ts +22 -22
  213. package/src/search-params/index.ts +3 -3
  214. package/src/search-params/registry.ts +1 -1
  215. package/src/segment-params/define.ts +18 -18
  216. package/src/segment-params/index.ts +2 -1
  217. package/src/server/action-handler.ts +1 -1
  218. package/src/server/als-registry.ts +3 -3
  219. package/src/server/dev-holding-server.ts +185 -0
  220. package/src/server/dev-warnings.ts +2 -21
  221. package/src/server/html-injectors.ts +3 -3
  222. package/src/server/index.ts +25 -180
  223. package/src/server/internal.ts +169 -0
  224. package/src/server/pipeline.ts +12 -7
  225. package/src/server/primitives.ts +71 -30
  226. package/src/server/request-context.ts +77 -39
  227. package/src/server/route-element-builder.ts +1 -1
  228. package/src/server/rsc-entry/index.ts +2 -2
  229. package/src/server/rsc-entry/ssr-renderer.ts +1 -1
  230. package/src/server/slot-resolver.ts +1 -1
  231. package/src/server/tracing.ts +6 -6
  232. package/src/server/tree-builder.ts +1 -1
  233. package/src/shims/headers.ts +5 -1
  234. package/src/shims/navigation.ts +5 -1
  235. package/dist/_chunks/als-registry-BJARkOcu.js.map +0 -1
  236. package/dist/_chunks/define-CGuYoRHU.js.map +0 -1
  237. package/dist/_chunks/define-Dz1bqwaS.js.map +0 -1
  238. package/dist/_chunks/error-boundary-D9hzsveV.js +0 -216
  239. package/dist/_chunks/error-boundary-D9hzsveV.js.map +0 -1
  240. package/dist/_chunks/format-Rn922VH2.js.map +0 -1
  241. package/dist/_chunks/handler-store-BVePM7hp.js.map +0 -1
  242. package/dist/_chunks/request-context-CywiO4jV.js.map +0 -1
  243. package/dist/_chunks/segment-context-hzuJ048X.js.map +0 -1
  244. package/dist/_chunks/use-query-states-DAhgj8Gx.js.map +0 -1
  245. package/dist/client/browser-entry.d.ts +0 -21
  246. package/dist/client/browser-entry.d.ts.map +0 -1
  247. package/dist/client/use-navigation-pending.d.ts.map +0 -1
  248. package/src/client/browser-entry.ts +0 -846
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Plugin context — internal types and helpers for timber sub-plugins.
3
+ *
4
+ * These are NOT part of the user-facing API. They are consumed by
5
+ * sub-plugins (routing, entries, shims, etc.) via relative imports.
6
+ * The root entry point (index.ts) does not re-export them.
7
+ *
8
+ * Design doc: 18-build-system.md §"Shared Plugin Context"
9
+ */
10
+
11
+ import { existsSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+ import { createRequire } from 'node:module';
14
+ import type { RouteTree } from './routing/types';
15
+ import type { BuildManifest } from './server/build-manifest';
16
+ import type { StartupTimer } from './utils/startup-timer';
17
+ import { createStartupTimer } from './utils/startup-timer';
18
+ import type { TimberUserConfig, ClientJavascriptConfig } from './config-types.js';
19
+ import type { HoldingServer } from './server/dev-holding-server.js';
20
+
21
+ // Re-export for sub-plugin convenience — they import from plugin-context.ts
22
+ export type { TimberUserConfig, ClientJavascriptConfig } from './config-types.js';
23
+
24
+ // ── Client JavaScript config ─────────────────────────────────────────────
25
+
26
+ /** Fully resolved client JavaScript configuration (no optionals). */
27
+ export interface ResolvedClientJavascript {
28
+ disabled: boolean;
29
+ enableHMRInDev: boolean;
30
+ }
31
+
32
+ /**
33
+ * Resolve `clientJavascript` into a fully resolved config.
34
+ */
35
+ export function resolveClientJavascript(config: TimberUserConfig): ResolvedClientJavascript {
36
+ if (config.clientJavascript !== undefined) {
37
+ if (typeof config.clientJavascript === 'boolean') {
38
+ return {
39
+ disabled: !config.clientJavascript,
40
+ enableHMRInDev: !config.clientJavascript,
41
+ };
42
+ }
43
+ return {
44
+ disabled: config.clientJavascript.disabled,
45
+ enableHMRInDev: config.clientJavascript.enableHMRInDev ?? config.clientJavascript.disabled,
46
+ };
47
+ }
48
+ return { disabled: false, enableHMRInDev: false };
49
+ }
50
+
51
+ // ── Plugin Context ────────────────────────────────────────────────────────
52
+
53
+ /**
54
+ * Shared context object passed to all sub-plugins via closure.
55
+ *
56
+ * Sub-plugins communicate through this context — not through Vite's
57
+ * plugin API or global state.
58
+ * See design/18-build-system.md §"Shared Plugin Context".
59
+ */
60
+ export interface PluginContext {
61
+ config: TimberUserConfig;
62
+ /** Resolved client JavaScript configuration */
63
+ clientJavascript: ResolvedClientJavascript;
64
+ /** The scanned route tree (populated by timber-routing, consumed by timber-entries) */
65
+ routeTree: RouteTree | null;
66
+ /** Absolute path to the app/ directory */
67
+ appDir: string;
68
+ /** Absolute path to the project root */
69
+ root: string;
70
+ /** Whether the dev server is running (set by timber-root-sync in configResolved) */
71
+ dev: boolean;
72
+ /** CSS build manifest (populated by adapter after client build, null in dev) */
73
+ buildManifest: BuildManifest | null;
74
+ /** Per-build deployment ID for version skew detection (null in dev) */
75
+ deploymentId: string | null;
76
+ /** Startup timer for profiling cold start phases (active in dev, no-op in prod) */
77
+ timer: StartupTimer;
78
+ /** Holding server that binds the port during dev startup (closed in timber-dev-server) */
79
+ holdingServer?: HoldingServer | null;
80
+ }
81
+
82
+ // ── App directory resolution ──────────────────────────────────────────────
83
+
84
+ /**
85
+ * Resolve the app directory. Checks (in order):
86
+ * 1. Explicit `configAppDir` from timber.config.ts
87
+ * 2. `<root>/app`
88
+ * 3. `<root>/src/app`
89
+ *
90
+ * Throws if none exist.
91
+ */
92
+ export function resolveAppDir(root: string, configAppDir?: string): string {
93
+ if (configAppDir) {
94
+ const explicit = join(root, configAppDir);
95
+ if (!existsSync(explicit)) {
96
+ throw new Error(`[timber] Configured appDir "${configAppDir}" does not exist at ${explicit}`);
97
+ }
98
+ return explicit;
99
+ }
100
+
101
+ const rootApp = join(root, 'app');
102
+ if (existsSync(rootApp)) return rootApp;
103
+
104
+ const srcApp = join(root, 'src', 'app');
105
+ if (existsSync(srcApp)) return srcApp;
106
+
107
+ throw new Error(
108
+ `[timber] Could not find app directory. Expected "app/" or "src/app/" in ${root}. ` +
109
+ `You can set appDir in timber.config.ts to specify a custom location.`
110
+ );
111
+ }
112
+
113
+ // ── Plugin context factory ────────────────────────────────────────────────
114
+
115
+ export function createPluginContext(config?: TimberUserConfig, root?: string): PluginContext {
116
+ const projectRoot = root ?? process.cwd();
117
+ const resolvedConfig: TimberUserConfig = { ...config };
118
+ return {
119
+ config: resolvedConfig,
120
+ clientJavascript: resolveClientJavascript(resolvedConfig),
121
+ routeTree: null,
122
+ appDir: join(projectRoot, 'app'),
123
+ root: projectRoot,
124
+ dev: false,
125
+ buildManifest: null,
126
+ deploymentId: null,
127
+ timer: createStartupTimer(),
128
+ holdingServer: null,
129
+ };
130
+ }
131
+
132
+ // ── Config file loading ───────────────────────────────────────────────────
133
+
134
+ /**
135
+ * Load timber.config.ts (or .js, .mjs) from the project root.
136
+ * Returns the config object or null if no config file is found.
137
+ *
138
+ * Uses require() which works for ESM modules on Node 22.12+.
139
+ * This keeps timber() synchronous — no async config loading needed.
140
+ */
141
+ export function loadTimberConfigFile(root: string): TimberUserConfig | null {
142
+ const configNames = ['timber.config.ts', 'timber.config.js', 'timber.config.mjs'];
143
+ const req = createRequire(join(root, 'package.json'));
144
+
145
+ for (const name of configNames) {
146
+ const configPath = join(root, name);
147
+ if (existsSync(configPath)) {
148
+ const mod = req(configPath);
149
+ return (mod.default ?? mod) as TimberUserConfig;
150
+ }
151
+ }
152
+ return null;
153
+ }
154
+
155
+ /**
156
+ * Detect config keys set in both inline (vite.config.ts) and file (timber.config.ts)
157
+ * and warn the user. The `output` key is excluded because it defaults to 'server'
158
+ * in createPluginContext and would always appear as an inline key.
159
+ *
160
+ * Returns the list of conflicting key names (for testing).
161
+ */
162
+ export function warnConfigConflicts(
163
+ inline: TimberUserConfig,
164
+ fileConfig: TimberUserConfig
165
+ ): string[] {
166
+ const conflicts: string[] = [];
167
+ for (const key of Object.keys(fileConfig) as (keyof TimberUserConfig)[]) {
168
+ if (key in inline && inline[key] !== undefined) {
169
+ conflicts.push(key);
170
+ }
171
+ }
172
+ if (conflicts.length > 0) {
173
+ console.warn(
174
+ `[timber] Config conflict: ${conflicts.map((k) => `"${k}"`).join(', ')} set in both ` +
175
+ `vite.config.ts (inline) and timber.config.ts. ` +
176
+ `Move all config to timber.config.ts to avoid confusion. ` +
177
+ `The inline value from vite.config.ts will be used.`
178
+ );
179
+ }
180
+ return conflicts;
181
+ }
182
+
183
+ /**
184
+ * Merge file-based config into ctx.config. Inline config (already in ctx.config)
185
+ * takes precedence — file config only fills in missing fields.
186
+ */
187
+ export function mergeFileConfig(ctx: PluginContext, fileConfig: TimberUserConfig): void {
188
+ const inline = ctx.config;
189
+ warnConfigConflicts(inline, fileConfig);
190
+
191
+ ctx.config = {
192
+ ...fileConfig,
193
+ ...inline,
194
+ ...(fileConfig.limits && inline.limits
195
+ ? { limits: { ...fileConfig.limits, ...inline.limits } }
196
+ : {}),
197
+ ...(fileConfig.dev && inline.dev ? { dev: { ...fileConfig.dev, ...inline.dev } } : {}),
198
+ ...(fileConfig.mdx && inline.mdx ? { mdx: { ...fileConfig.mdx, ...inline.mdx } } : {}),
199
+ };
200
+ }
@@ -16,7 +16,7 @@
16
16
  import type { Plugin } from 'vite';
17
17
  import { join } from 'node:path';
18
18
  import { readFile, writeFile } from 'node:fs/promises';
19
- import type { PluginContext } from '../index.js';
19
+ import type { PluginContext } from '../plugin-context.js';
20
20
  import type { TimberPlatformAdapter, TimberConfig } from '../adapters/types.js';
21
21
 
22
22
  export function timberAdapterBuild(ctx: PluginContext): Plugin {
@@ -18,7 +18,7 @@
18
18
 
19
19
  import { randomUUID } from 'node:crypto';
20
20
  import type { Plugin, ResolvedConfig } from 'vite';
21
- import type { PluginContext } from '../index.js';
21
+ import type { PluginContext } from '../plugin-context.js';
22
22
  import type { BuildManifest } from '../server/build-manifest.js';
23
23
 
24
24
  // Rollup types used by generateBundle hook — imported from vite which re-exports them.
@@ -15,7 +15,7 @@
15
15
 
16
16
  import { gzipSync } from 'node:zlib';
17
17
  import type { Plugin, Logger } from 'vite';
18
- import type { PluginContext } from '../index.js';
18
+ import type { PluginContext } from '../plugin-context.js';
19
19
  import type { SegmentNode, RouteTree } from '../routing/types.js';
20
20
  import { formatSize } from '../utils/format.js';
21
21
 
@@ -11,7 +11,7 @@
11
11
  import type { Plugin } from 'vite';
12
12
  import { existsSync } from 'node:fs';
13
13
  import { join } from 'node:path';
14
- import type { PluginContext } from '../index.js';
14
+ import type { PluginContext } from '../plugin-context.js';
15
15
 
16
16
  const CONFIG_FILE_NAMES = [
17
17
  'content-collections.ts',
@@ -16,7 +16,7 @@
16
16
  */
17
17
 
18
18
  import type { Plugin, ViteDevServer } from 'vite';
19
- import type { PluginContext } from '../index.js';
19
+ import type { PluginContext } from '../plugin-context.js';
20
20
 
21
21
  // ─── Types ───────────────────────────────────────────────────────────────
22
22
 
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  import type { Plugin, ViteDevServer } from 'vite';
16
- import type { PluginContext } from '../index.js';
16
+ import type { PluginContext } from '../plugin-context.js';
17
17
 
18
18
  // ─── Types ───────────────────────────────────────────────────────────────
19
19
 
@@ -15,7 +15,7 @@
15
15
  import type { Plugin, ViteDevServer, DevEnvironment } from 'vite';
16
16
  import type { IncomingMessage, ServerResponse } from 'node:http';
17
17
  import { join } from 'node:path';
18
- import type { PluginContext } from '../index.js';
18
+ import type { PluginContext } from '../plugin-context.js';
19
19
  import { setViteServer } from '../server/dev-warnings.js';
20
20
  import { sendErrorToOverlay, classifyErrorPhase, parseFirstAppFrame } from './dev-error-overlay.js';
21
21
  import { compressResponse } from '../server/compress.js';
@@ -96,6 +96,21 @@ export function timberDevServer(ctx: PluginContext): Plugin {
96
96
  // Pre-hook — registers middleware before Vite's internals
97
97
  server.middlewares.use(createTimberMiddleware(server, ctx.root));
98
98
 
99
+ // Wrap server.listen() to close the holding server immediately
100
+ // before Vite binds the port — no gap where nothing is listening.
101
+ // The holding server was started in rootSync's config() hook to
102
+ // serve a loading page during initialization.
103
+ // See design/21-dev-server.md §"Startup Holding Server", TIM-665.
104
+ if (ctx.holdingServer) {
105
+ const originalListen = server.listen.bind(server);
106
+ const holdingRef = ctx.holdingServer;
107
+ ctx.holdingServer = null;
108
+ server.listen = async (port?: number, isRestart?: boolean) => {
109
+ await holdingRef.close().catch(() => {});
110
+ return originalListen(port, isRestart);
111
+ };
112
+ }
113
+
99
114
  // Log startup timing summary. configureServer runs on all plugins
100
115
  // before the server listens, so this captures the full cold start.
101
116
  ctx.timer.end('dev-server-setup');
@@ -15,7 +15,7 @@ import type { Plugin } from 'vite';
15
15
  import { resolve, dirname } from 'node:path';
16
16
  import { fileURLToPath } from 'node:url';
17
17
  import { existsSync } from 'node:fs';
18
- import type { PluginContext } from '../index.js';
18
+ import type { PluginContext } from '../plugin-context.js';
19
19
 
20
20
  const __dirname = dirname(fileURLToPath(import.meta.url));
21
21
 
@@ -49,7 +49,7 @@ const VIRTUAL_IDS = {
49
49
  const ENTRY_FILE_MAP: Record<string, string> = {
50
50
  [VIRTUAL_IDS.rscEntry]: resolve(SRC_DIR, 'server', 'rsc-entry', 'index.ts'),
51
51
  [VIRTUAL_IDS.ssrEntry]: resolve(SRC_DIR, 'server', 'ssr-entry.ts'),
52
- [VIRTUAL_IDS.browserEntry]: resolve(SRC_DIR, 'client', 'browser-entry.ts'),
52
+ [VIRTUAL_IDS.browserEntry]: resolve(SRC_DIR, 'client', 'browser-entry', 'index.ts'),
53
53
  };
54
54
 
55
55
  /** The \0-prefixed resolved ID for virtual:timber-config */
@@ -17,7 +17,7 @@
17
17
  import type { Plugin, ViteDevServer } from 'vite';
18
18
  import { readFileSync, existsSync } from 'node:fs';
19
19
  import { resolve, normalize } from 'node:path';
20
- import type { PluginContext } from '../index.js';
20
+ import type { PluginContext } from '../plugin-context.js';
21
21
  import type { ExtractedFont, GoogleFontConfig } from '../fonts/types.js';
22
22
  import type { ManifestFontEntry } from '../server/build-manifest.js';
23
23
  import { generateVariableClass, generateFontFamilyClass, generateFontFaces } from '../fonts/css.js';
@@ -470,11 +470,12 @@ export function timberFonts(ctx: PluginContext): Plugin {
470
470
  googleFontFacesMap.set(font.id, faces);
471
471
  } catch (e) {
472
472
  // In dev mode, fail gracefully — fonts fall back to system fonts.
473
+ // Don't cache the failure so transient errors (network blips,
474
+ // rate limits) are retried on the next request. See TIM-636.
473
475
  const msg = e instanceof Error ? e.message : String(e);
474
476
  console.warn(
475
- `[timber-fonts] Failed to resolve Google font "${font.family}": ${msg}`
477
+ `[timber-fonts] Failed to resolve Google font "${font.family}": ${msg}. Will retry on next request.`
476
478
  );
477
- googleFontFacesMap.set(font.id, []);
478
479
  }
479
480
  }
480
481
  }
@@ -11,7 +11,7 @@
11
11
  import type { Plugin } from 'vite';
12
12
  import { existsSync } from 'node:fs';
13
13
  import { join } from 'node:path';
14
- import type { PluginContext } from '../index.js';
14
+ import type { PluginContext } from '../plugin-context.js';
15
15
 
16
16
  const MDX_EXTENSIONS = ['mdx', 'md'];
17
17
 
@@ -19,7 +19,7 @@ import {
19
19
  formatStatusFileLintWarnings,
20
20
  } from '../routing/status-file-lint.js';
21
21
  import type { RouteTree, SegmentNode, RouteFile } from '../routing/types.js';
22
- import type { PluginContext } from '../index.js';
22
+ import type { PluginContext } from '../plugin-context.js';
23
23
 
24
24
  const VIRTUAL_MODULE_ID = 'virtual:timber-route-manifest';
25
25
  const RESOLVED_VIRTUAL_ID = `\0${VIRTUAL_MODULE_ID}`;
@@ -17,7 +17,7 @@
17
17
  import type { Plugin } from 'vite';
18
18
  import { resolve, dirname } from 'node:path';
19
19
  import { fileURLToPath } from 'node:url';
20
- import type { PluginContext } from '../index.js';
20
+ import type { PluginContext } from '../plugin-context.js';
21
21
 
22
22
  const __dirname = dirname(fileURLToPath(import.meta.url));
23
23
  // Detect whether we're running from source (src/plugins/) or dist (dist/).
@@ -48,11 +48,9 @@ const CLIENT_ONLY_VIRTUAL = '\0timber:client-only';
48
48
  */
49
49
  const SUBPATH_SRC_MAP: Record<string, string> = {
50
50
  'cache': 'cache/index.ts',
51
- 'content': 'content/index.ts',
52
51
  'cookies': 'cookies/index.ts',
53
- 'segment-params': 'segment-params/index.ts',
54
52
  'search-params': 'search-params/index.ts',
55
- 'routing': 'routing/index.ts',
53
+
56
54
  'adapters/cloudflare': 'adapters/cloudflare.ts',
57
55
  'adapters/cloudflare/dev': 'adapters/cloudflare-dev.ts',
58
56
  'adapters/nitro': 'adapters/nitro.ts',
@@ -163,6 +161,50 @@ export function timberShims(_ctx: PluginContext): Plugin {
163
161
  // In the client (browser) environment, all imports resolve to dist/
164
162
  // via package.json exports (no remapping needed).
165
163
 
164
+ // ── #imports resolution for package-internal modules ─────────────
165
+ //
166
+ // #server-internal and #client-internal are Node package imports
167
+ // (package.json "imports" field). They are only resolvable from
168
+ // within this package — external consumers get module-not-found.
169
+ //
170
+ // In Vite, we intercept them here to resolve to src/ for dev/build
171
+ // correctness (same rationale as the @timber-js/app/* remap above).
172
+ if (cleanId === '#server-internal') {
173
+ const envName = (this as unknown as { environment?: { name?: string } }).environment?.name;
174
+ if (envName === 'client') {
175
+ return '\0timber:server-empty';
176
+ }
177
+ return resolve(PKG_ROOT, 'src', 'server', 'internal.ts');
178
+ }
179
+
180
+ if (cleanId === '#client-internal') {
181
+ // Always resolve to src/ — framework internals only,
182
+ // no 'use client' directives to preserve.
183
+ return resolve(PKG_ROOT, 'src', 'client', 'internal.ts');
184
+ }
185
+
186
+ if (cleanId === '#routing') {
187
+ return resolve(PKG_ROOT, 'src', 'routing', 'index.ts');
188
+ }
189
+
190
+ // Legacy aliases — vitest and monorepo configs may still reference
191
+ // the old @timber-js/app/server/internal and @timber-js/app/client/internal
192
+ // specifiers via resolve.alias. When those aliases resolve to the
193
+ // absolute src/ path, they bypass this plugin entirely. But if the
194
+ // specifier reaches us unresolved (e.g., in a Vite build without
195
+ // aliases), handle it the same way as the #imports above.
196
+ if (cleanId === '@timber-js/app/server/internal') {
197
+ const envName = (this as unknown as { environment?: { name?: string } }).environment?.name;
198
+ if (envName === 'client') {
199
+ return '\0timber:server-empty';
200
+ }
201
+ return resolve(PKG_ROOT, 'src', 'server', 'internal.ts');
202
+ }
203
+
204
+ if (cleanId === '@timber-js/app/client/internal') {
205
+ return resolve(PKG_ROOT, 'src', 'client', 'internal.ts');
206
+ }
207
+
166
208
  if (cleanId === '@timber-js/app/server') {
167
209
  const envName = (this as unknown as { environment?: { name?: string } }).environment?.name;
168
210
  if (envName === 'client') {
@@ -245,6 +287,12 @@ const msg = "[timber] @timber-js/app/server was imported from client code. " +
245
287
  function stub() { throw new Error(msg); }
246
288
  export const headers = stub;
247
289
  export const cookies = stub;
290
+ export const getHeaders = stub;
291
+ export const getHeader = stub;
292
+ export const getCookies = stub;
293
+ export const getCookie = stub;
294
+ export const getSearchParams = stub;
295
+ export const getSegmentParams = stub;
248
296
  export const searchParams = stub;
249
297
  export const deny = stub;
250
298
  export const notFound = stub;
@@ -252,7 +300,7 @@ export const redirect = stub;
252
300
  export const permanentRedirect = stub;
253
301
  export const redirectExternal = stub;
254
302
  export const waitUntil = stub;
255
- export const RenderError = stub;
303
+ export const DenyOptions = {};
256
304
  export const RedirectType = {};
257
305
  export const DenySignal = stub;
258
306
  export const RedirectSignal = stub;
@@ -2,7 +2,7 @@
2
2
  * timber-static-build — Vite sub-plugin for static output mode.
3
3
  *
4
4
  * When `output: 'static'` is set in timber.config.ts, this plugin:
5
- * 1. Validates that no dynamic APIs (cookies(), headers()) are used
5
+ * 1. Validates that no dynamic APIs (getCookies(), getHeaders()) are used
6
6
  * 2. When client JavaScript is disabled, rejects 'use client' and 'use server' directives
7
7
  * 3. Coordinates build-time rendering of all pages
8
8
  *
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import type { Plugin } from 'vite';
13
- import type { PluginContext } from '../index.js';
13
+ import type { PluginContext } from '../plugin-context.js';
14
14
  import { detectFileDirective } from '../utils/directive-parser.js';
15
15
 
16
16
  // ---------------------------------------------------------------------------
@@ -29,7 +29,7 @@ export interface StaticOptions {
29
29
  }
30
30
 
31
31
  // ---------------------------------------------------------------------------
32
- // Detection: dynamic APIs (cookies, headers)
32
+ // Detection: dynamic APIs (getCookies, getHeaders, and legacy cookies/headers)
33
33
  // ---------------------------------------------------------------------------
34
34
 
35
35
  /**
@@ -39,12 +39,14 @@ export interface StaticOptions {
39
39
  * We detect both import-level and call-level usage.
40
40
  */
41
41
  const DYNAMIC_API_PATTERNS: Array<{ pattern: RegExp; name: string }> = [
42
+ { pattern: /\bgetCookies\s*\(/, name: 'getCookies()' },
43
+ { pattern: /\bgetHeaders\s*\(/, name: 'getHeaders()' },
42
44
  { pattern: /\bcookies\s*\(/, name: 'cookies()' },
43
45
  { pattern: /\bheaders\s*\(/, name: 'headers()' },
44
46
  ];
45
47
 
46
48
  /**
47
- * Detect usage of dynamic per-request APIs (cookies(), headers())
49
+ * Detect usage of dynamic per-request APIs (getCookies(), getHeaders())
48
50
  * that cannot work at build time in static mode.
49
51
  *
50
52
  * Returns an array of validation errors.
@@ -134,7 +136,7 @@ export function detectDirectives(
134
136
  * Run all static mode validations on a source file.
135
137
  *
136
138
  * Combines:
137
- * - Dynamic API detection (cookies, headers) — always in static mode
139
+ * - Dynamic API detection (getCookies, getHeaders) — always in static mode
138
140
  * - Directive detection ('use client', 'use server') — only when client JS is disabled
139
141
  */
140
142
  export function validateStaticMode(
@@ -170,7 +172,7 @@ export function timberStaticBuild(ctx: PluginContext): Plugin {
170
172
  * Validate source files during transform.
171
173
  *
172
174
  * In static mode, we check every app/ file for:
173
- * - Dynamic API usage (cookies(), headers()) → build error
175
+ * - Dynamic API usage (getCookies(), getHeaders()) → build error
174
176
  * - When client JS disabled: 'use client' / 'use server' directives → build error
175
177
  */
176
178
  transform(code: string, id: string) {
@@ -15,33 +15,33 @@ import { fromSchema, isStandardSchema, isCodec } from '../schema-bridge.js';
15
15
  import type { StandardSchemaV1 } from '../schema-bridge.js';
16
16
  import type { Codec } from '../codec.js';
17
17
 
18
- // Server-only reference for .load() — avoids pulling server ALS into client bundles.
19
- // In client environments, .load() throws before reaching this code path.
18
+ // Server-only reference for .get() — avoids pulling server ALS into client bundles.
19
+ // In client environments, .get() throws before reaching this code path.
20
20
  //
21
- // IMPORTANT: This is set eagerly via _setRawSearchParamsFn() at server startup
21
+ // IMPORTANT: This is set eagerly via _setGetSearchParamsFn() at server startup
22
22
  // (called from request-context.ts module initialization). It must NOT use
23
23
  // dynamic `await import()` at call time because the async microtask from the
24
24
  // dynamic import loses AsyncLocalStorage context in React's RSC Flight renderer,
25
- // breaking rawSearchParams() in parallel slot pages. See TIM-523.
26
- let _rawSearchParams: (() => Promise<URLSearchParams>) | undefined;
25
+ // breaking getSearchParams() in parallel slot pages. See TIM-523.
26
+ let _getSearchParamsFn: (() => Promise<URLSearchParams>) | undefined;
27
27
 
28
28
  /**
29
- * Register the rawSearchParams function. Called once at module load time
29
+ * Register the getSearchParams function. Called once at module load time
30
30
  * from request-context.ts to avoid dynamic import at call time.
31
31
  * @internal
32
32
  */
33
- export function _setRawSearchParamsFn(fn: () => Promise<URLSearchParams>): void {
34
- _rawSearchParams = fn;
33
+ export function _setGetSearchParamsFn(fn: () => Promise<URLSearchParams>): void {
34
+ _getSearchParamsFn = fn;
35
35
  }
36
36
 
37
- function getRawSearchParams(): Promise<URLSearchParams> {
38
- if (!_rawSearchParams) {
37
+ function getSearchParamsFromAls(): Promise<URLSearchParams> {
38
+ if (!_getSearchParamsFn) {
39
39
  throw new Error(
40
- '[timber] searchParams.load() is only available on the server. ' +
40
+ '[timber] searchParams.get() is only available on the server. ' +
41
41
  'Use searchParams.useQueryStates() on the client.'
42
42
  );
43
43
  }
44
- return _rawSearchParams();
44
+ return _getSearchParamsFn();
45
45
  }
46
46
 
47
47
  // ---------------------------------------------------------------------------
@@ -109,9 +109,9 @@ export interface SearchParamsDefinition<T extends Record<string, unknown>> {
109
109
  parse(raw: Promise<URLSearchParams | Record<string, string | string[] | undefined>>): Promise<T>;
110
110
 
111
111
  /**
112
- * Load typed search params from the current request context (ALS-backed).
112
+ * Get typed search params from the current request context (ALS-backed).
113
113
  *
114
- * Server-only — reads rawSearchParams() from ALS and parses through codecs.
114
+ * Server-only — reads getSearchParams() from ALS and parses through codecs.
115
115
  * Throws on client. Eliminates the naming conflict between the definition
116
116
  * export and the server helper.
117
117
  *
@@ -119,11 +119,11 @@ export interface SearchParamsDefinition<T extends Record<string, unknown>> {
119
119
  * // app/products/page.tsx
120
120
  * import { searchParams } from './params'
121
121
  * export default async function Page() {
122
- * const { page, category } = await searchParams.load()
122
+ * const { page, category } = await searchParams.get()
123
123
  * }
124
124
  * ```
125
125
  */
126
- load(): Promise<T>;
126
+ get(): Promise<T>;
127
127
 
128
128
  /** Client hook — reads current URL params and returns typed values + setter. */
129
129
  useQueryStates(options?: QueryStatesOptions): [T, SetParams<T>];
@@ -451,23 +451,23 @@ function buildDefinition<T extends Record<string, unknown>>(
451
451
  ];
452
452
  }
453
453
 
454
- // ---- load ----
455
- // ALS-backed: reads rawSearchParams() from the current request context
454
+ // ---- get ----
455
+ // ALS-backed: reads getSearchParams() from the current request context
456
456
  // and parses through codecs. Server-only — throws on client.
457
- async function load(): Promise<T> {
457
+ async function get(): Promise<T> {
458
458
  if (typeof window !== 'undefined') {
459
459
  throw new Error(
460
- '[timber] searchParams.load() is server-only. ' +
460
+ '[timber] searchParams.get() is server-only. ' +
461
461
  'Use searchParams.useQueryStates() on the client.'
462
462
  );
463
463
  }
464
- const raw = await getRawSearchParams();
464
+ const raw = await getSearchParamsFromAls();
465
465
  return parseSync(raw);
466
466
  }
467
467
 
468
468
  const definition: SearchParamsDefinition<T> = {
469
469
  parse,
470
- load,
470
+ get,
471
471
  useQueryStates,
472
472
  extend,
473
473
  pick,
@@ -1,7 +1,7 @@
1
1
  // @timber-js/app/search-params — Typed search params
2
2
 
3
- // Shared codec protocol
4
- export type { Codec } from '../codec.js';
3
+ // Codec is the canonical home for the Codec type — import from
4
+ // @timber-js/app/codec. Re-export removed per TIM-721.
5
5
 
6
6
  // Core types and factory
7
7
  export type {
@@ -25,4 +25,4 @@ export { defineSearchParams } from './define.js';
25
25
  export { withDefault, withUrlKey } from './wrappers.js';
26
26
 
27
27
  // Runtime registry (route-scoped useQueryStates)
28
- export { registerSearchParams, getSearchParams } from './registry.js';
28
+ export { registerSearchParams, getSearchParamsDefinition } from './registry.js';
@@ -26,6 +26,6 @@ export function registerSearchParams(route: string, definition: SearchParamsDefi
26
26
  * Returns undefined if the route hasn't been loaded yet.
27
27
  */
28
28
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
- export function getSearchParams(route: string): SearchParamsDefinition<any> | undefined {
29
+ export function getSearchParamsDefinition(route: string): SearchParamsDefinition<any> | undefined {
30
30
  return registry.get(route);
31
31
  }