@timber-js/app 0.1.0 → 0.1.2

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 (141) hide show
  1. package/dist/index.js +5 -2
  2. package/dist/index.js.map +1 -1
  3. package/dist/plugins/entries.d.ts.map +1 -1
  4. package/package.json +43 -58
  5. package/src/adapters/cloudflare.ts +325 -0
  6. package/src/adapters/nitro.ts +366 -0
  7. package/src/adapters/types.ts +63 -0
  8. package/src/cache/index.ts +91 -0
  9. package/src/cache/redis-handler.ts +91 -0
  10. package/src/cache/register-cached-function.ts +99 -0
  11. package/src/cache/singleflight.ts +26 -0
  12. package/src/cache/stable-stringify.ts +21 -0
  13. package/src/cache/timber-cache.ts +116 -0
  14. package/src/cli.ts +201 -0
  15. package/src/client/browser-entry.ts +663 -0
  16. package/src/client/error-boundary.tsx +209 -0
  17. package/src/client/form.tsx +200 -0
  18. package/src/client/head.ts +61 -0
  19. package/src/client/history.ts +46 -0
  20. package/src/client/index.ts +60 -0
  21. package/src/client/link-navigate-interceptor.tsx +62 -0
  22. package/src/client/link-status-provider.tsx +40 -0
  23. package/src/client/link.tsx +310 -0
  24. package/src/client/nuqs-adapter.tsx +117 -0
  25. package/src/client/router-ref.ts +25 -0
  26. package/src/client/router.ts +563 -0
  27. package/src/client/segment-cache.ts +194 -0
  28. package/src/client/segment-context.ts +57 -0
  29. package/src/client/ssr-data.ts +95 -0
  30. package/src/client/types.ts +4 -0
  31. package/src/client/unload-guard.ts +34 -0
  32. package/src/client/use-cookie.ts +122 -0
  33. package/src/client/use-link-status.ts +46 -0
  34. package/src/client/use-navigation-pending.ts +47 -0
  35. package/src/client/use-params.ts +71 -0
  36. package/src/client/use-pathname.ts +43 -0
  37. package/src/client/use-query-states.ts +133 -0
  38. package/src/client/use-router.ts +77 -0
  39. package/src/client/use-search-params.ts +74 -0
  40. package/src/client/use-selected-layout-segment.ts +110 -0
  41. package/src/content/index.ts +13 -0
  42. package/src/cookies/define-cookie.ts +137 -0
  43. package/src/cookies/index.ts +9 -0
  44. package/src/fonts/ast.ts +359 -0
  45. package/src/fonts/css.ts +68 -0
  46. package/src/fonts/fallbacks.ts +248 -0
  47. package/src/fonts/google.ts +332 -0
  48. package/src/fonts/local.ts +177 -0
  49. package/src/fonts/types.ts +88 -0
  50. package/src/index.ts +413 -0
  51. package/src/plugins/adapter-build.ts +118 -0
  52. package/src/plugins/build-manifest.ts +323 -0
  53. package/src/plugins/build-report.ts +353 -0
  54. package/src/plugins/cache-transform.ts +199 -0
  55. package/src/plugins/chunks.ts +90 -0
  56. package/src/plugins/content.ts +136 -0
  57. package/src/plugins/dev-error-overlay.ts +230 -0
  58. package/src/plugins/dev-logs.ts +280 -0
  59. package/src/plugins/dev-server.ts +389 -0
  60. package/src/plugins/dynamic-transform.ts +161 -0
  61. package/src/plugins/entries.ts +207 -0
  62. package/src/plugins/fonts.ts +581 -0
  63. package/src/plugins/mdx.ts +179 -0
  64. package/src/plugins/react-prod.ts +56 -0
  65. package/src/plugins/routing.ts +419 -0
  66. package/src/plugins/server-action-exports.ts +220 -0
  67. package/src/plugins/server-bundle.ts +113 -0
  68. package/src/plugins/shims.ts +168 -0
  69. package/src/plugins/static-build.ts +207 -0
  70. package/src/routing/codegen.ts +396 -0
  71. package/src/routing/index.ts +14 -0
  72. package/src/routing/interception.ts +173 -0
  73. package/src/routing/scanner.ts +487 -0
  74. package/src/routing/status-file-lint.ts +114 -0
  75. package/src/routing/types.ts +100 -0
  76. package/src/search-params/analyze.ts +192 -0
  77. package/src/search-params/codecs.ts +153 -0
  78. package/src/search-params/create.ts +314 -0
  79. package/src/search-params/index.ts +23 -0
  80. package/src/search-params/registry.ts +31 -0
  81. package/src/server/access-gate.tsx +142 -0
  82. package/src/server/action-client.ts +473 -0
  83. package/src/server/action-handler.ts +325 -0
  84. package/src/server/actions.ts +236 -0
  85. package/src/server/asset-headers.ts +81 -0
  86. package/src/server/body-limits.ts +102 -0
  87. package/src/server/build-manifest.ts +234 -0
  88. package/src/server/canonicalize.ts +90 -0
  89. package/src/server/client-module-map.ts +58 -0
  90. package/src/server/csrf.ts +79 -0
  91. package/src/server/deny-renderer.ts +302 -0
  92. package/src/server/dev-logger.ts +419 -0
  93. package/src/server/dev-span-processor.ts +78 -0
  94. package/src/server/dev-warnings.ts +282 -0
  95. package/src/server/early-hints-sender.ts +55 -0
  96. package/src/server/early-hints.ts +142 -0
  97. package/src/server/error-boundary-wrapper.ts +69 -0
  98. package/src/server/error-formatter.ts +184 -0
  99. package/src/server/flush.ts +182 -0
  100. package/src/server/form-data.ts +176 -0
  101. package/src/server/form-flash.ts +93 -0
  102. package/src/server/html-injectors.ts +445 -0
  103. package/src/server/index.ts +222 -0
  104. package/src/server/instrumentation.ts +136 -0
  105. package/src/server/logger.ts +145 -0
  106. package/src/server/manifest-status-resolver.ts +215 -0
  107. package/src/server/metadata-render.ts +527 -0
  108. package/src/server/metadata-routes.ts +189 -0
  109. package/src/server/metadata.ts +263 -0
  110. package/src/server/middleware-runner.ts +32 -0
  111. package/src/server/nuqs-ssr-provider.tsx +63 -0
  112. package/src/server/pipeline.ts +555 -0
  113. package/src/server/prerender.ts +139 -0
  114. package/src/server/primitives.ts +264 -0
  115. package/src/server/proxy.ts +43 -0
  116. package/src/server/request-context.ts +554 -0
  117. package/src/server/route-element-builder.ts +395 -0
  118. package/src/server/route-handler.ts +153 -0
  119. package/src/server/route-matcher.ts +316 -0
  120. package/src/server/rsc-entry/api-handler.ts +112 -0
  121. package/src/server/rsc-entry/error-renderer.ts +177 -0
  122. package/src/server/rsc-entry/helpers.ts +147 -0
  123. package/src/server/rsc-entry/index.ts +688 -0
  124. package/src/server/rsc-entry/ssr-bridge.ts +18 -0
  125. package/src/server/slot-resolver.ts +359 -0
  126. package/src/server/ssr-entry.ts +161 -0
  127. package/src/server/ssr-render.ts +200 -0
  128. package/src/server/status-code-resolver.ts +282 -0
  129. package/src/server/tracing.ts +281 -0
  130. package/src/server/tree-builder.ts +354 -0
  131. package/src/server/types.ts +150 -0
  132. package/src/shims/font-google.ts +67 -0
  133. package/src/shims/headers.ts +11 -0
  134. package/src/shims/image.ts +48 -0
  135. package/src/shims/link.ts +9 -0
  136. package/src/shims/navigation-client.ts +52 -0
  137. package/src/shims/navigation.ts +31 -0
  138. package/src/shims/server-only-noop.js +5 -0
  139. package/src/utils/directive-parser.ts +529 -0
  140. package/src/utils/format.ts +10 -0
  141. package/src/utils/startup-timer.ts +102 -0
@@ -0,0 +1,161 @@
1
+ /**
2
+ * timber-dynamic-transform — Vite sub-plugin for 'use dynamic' directive.
3
+ *
4
+ * Detects `'use dynamic'` directives in server component function bodies
5
+ * and transforms them into `markDynamic()` runtime calls. The directive
6
+ * declares a dynamic boundary — the component and its subtree opt out of
7
+ * the pre-rendered shell and render per-request.
8
+ *
9
+ * - In `output: 'static'` mode, `'use dynamic'` is a build error.
10
+ * - In standard SSR routes (no prerender.ts), the directive is a no-op
11
+ * (everything is already per-request), but the transform still runs
12
+ * so the runtime can skip unnecessary work.
13
+ *
14
+ * Design doc: design/15-future-prerendering.md §"'use dynamic'"
15
+ */
16
+
17
+ import type { Plugin } from 'vite';
18
+ import type { PluginContext } from '#/index.js';
19
+ import { findFunctionsWithDirective, containsDirective } from '#/utils/directive-parser.js';
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Detection
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /**
26
+ * Quick check: does this source file contain 'use dynamic' anywhere?
27
+ * Used as a fast bail-out before doing expensive AST parsing.
28
+ */
29
+ export function containsUseDynamic(code: string): boolean {
30
+ return containsDirective(code, 'use dynamic');
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Transform
35
+ // ---------------------------------------------------------------------------
36
+
37
+ interface TransformResult {
38
+ code: string;
39
+ map?: null;
40
+ }
41
+
42
+ /**
43
+ * Find function declarations/expressions containing 'use dynamic' and
44
+ * transform them into markDynamic() calls.
45
+ *
46
+ * Input:
47
+ * ```tsx
48
+ * export default async function AddToCartButton({ productId }) {
49
+ * 'use dynamic'
50
+ * const user = await getUser()
51
+ * return <button>Add to cart</button>
52
+ * }
53
+ * ```
54
+ *
55
+ * Output:
56
+ * ```tsx
57
+ * import { markDynamic as __markDynamic } from '@timber/app/runtime';
58
+ * export default async function AddToCartButton({ productId }) {
59
+ * __markDynamic();
60
+ * const user = await getUser()
61
+ * return <button>Add to cart</button>
62
+ * }
63
+ * ```
64
+ *
65
+ * The markDynamic() call registers the component boundary as dynamic
66
+ * at render time. The pre-render pass uses this to know which subtrees
67
+ * to skip and leave as holes for per-request rendering.
68
+ */
69
+ export function transformUseDynamic(code: string): TransformResult | null {
70
+ if (!containsUseDynamic(code)) return null;
71
+
72
+ const functions = findFunctionsWithDirective(code, 'use dynamic');
73
+ if (functions.length === 0) return null;
74
+
75
+ // Replace directive strings with __markDynamic() calls, processing
76
+ // from end to start to preserve source offsets
77
+ let result = code;
78
+ for (const fn of functions) {
79
+ // Replace the directive in the body content
80
+ const cleanBody = fn.bodyContent.replace(/['"]use dynamic['"];?/, '__markDynamic();');
81
+ // Reconstruct: replace the body content between braces
82
+ result = result.slice(0, fn.bodyStart) + cleanBody + result.slice(fn.bodyEnd);
83
+ }
84
+
85
+ // Add the import at the top
86
+ result = `import { markDynamic as __markDynamic } from '@timber/app/runtime';\n` + result;
87
+
88
+ return { code: result, map: null };
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Static mode validation
93
+ // ---------------------------------------------------------------------------
94
+
95
+ /**
96
+ * In `output: 'static'` mode, `'use dynamic'` is a build error.
97
+ * Static mode renders everything at build time — there is no per-request
98
+ * rendering to opt into.
99
+ */
100
+ export function validateNoDynamicInStaticMode(
101
+ code: string
102
+ ): { message: string; line?: number } | null {
103
+ if (!containsUseDynamic(code)) return null;
104
+
105
+ const functions = findFunctionsWithDirective(code, 'use dynamic');
106
+ if (functions.length === 0) return null;
107
+
108
+ return {
109
+ message:
110
+ `'use dynamic' cannot be used in static mode (output: 'static'). ` +
111
+ `Static mode renders all content at build time — there is no per-request rendering. ` +
112
+ `Remove the directive or switch to output: 'server'.`,
113
+ line: functions[functions.length - 1].directiveLine, // First occurrence (sorted descending)
114
+ };
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // Vite Plugin
119
+ // ---------------------------------------------------------------------------
120
+
121
+ /**
122
+ * Create the timber-dynamic-transform Vite plugin.
123
+ *
124
+ * In server mode: transforms 'use dynamic' into markDynamic() calls.
125
+ * In static mode: rejects 'use dynamic' as a build error.
126
+ */
127
+ export function timberDynamicTransform(ctx: PluginContext): Plugin {
128
+ const isStatic = ctx.config.output === 'static';
129
+
130
+ return {
131
+ name: 'timber-dynamic-transform',
132
+
133
+ transform(code: string, id: string) {
134
+ // Skip node_modules
135
+ if (id.includes('node_modules')) return null;
136
+
137
+ // Only check files in the app directory
138
+ if (!id.includes('/app/') && !id.startsWith('app/')) return null;
139
+
140
+ // Only check JS/TS files
141
+ if (!/\.[jt]sx?$/.test(id)) return null;
142
+
143
+ // Quick bail-out
144
+ if (!containsUseDynamic(code)) return null;
145
+
146
+ // In static mode, 'use dynamic' is a build error
147
+ if (isStatic) {
148
+ const error = validateNoDynamicInStaticMode(code);
149
+ if (error) {
150
+ this.error(
151
+ `[timber] Static mode error in ${id}${error.line ? `:${error.line}` : ''}: ${error.message}`
152
+ );
153
+ }
154
+ return null;
155
+ }
156
+
157
+ // In server mode, transform the directive
158
+ return transformUseDynamic(code);
159
+ },
160
+ };
161
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * timber-entries — Vite sub-plugin for entry virtual module resolution.
3
+ *
4
+ * Resolves virtual:timber-rsc-entry, virtual:timber-ssr-entry,
5
+ * virtual:timber-browser-entry to real TypeScript files, and generates
6
+ * virtual:timber-config as serialized runtime config.
7
+ *
8
+ * Entry modules are real .ts files — NOT codegen strings. The only
9
+ * generated code is virtual:timber-config (serialized runtime config).
10
+ *
11
+ * Design docs: 18-build-system.md §"Entry Generation", §"Virtual Modules"
12
+ */
13
+
14
+ import type { Plugin } from 'vite';
15
+ import { resolve, dirname } from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+ import type { PluginContext } from '#/index.js';
18
+
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+ const SRC_DIR = resolve(__dirname, '..');
21
+
22
+ // ─── Virtual Module IDs ──────────────────────────────────────────────────
23
+
24
+ const VIRTUAL_IDS = {
25
+ rscEntry: 'virtual:timber-rsc-entry',
26
+ ssrEntry: 'virtual:timber-ssr-entry',
27
+ browserEntry: 'virtual:timber-browser-entry',
28
+ config: 'virtual:timber-config',
29
+ } as const;
30
+
31
+ /**
32
+ * Map from virtual module IDs to their resolved file paths.
33
+ *
34
+ * These point to .ts source files that Vite transpiles at runtime.
35
+ * They import virtual modules (virtual:timber-config, etc.) so they
36
+ * cannot be pre-compiled — they must be processed by Vite's pipeline
37
+ * with timber's plugin active. The .ts files are included in the
38
+ * published package via the `files` field.
39
+ */
40
+ const ENTRY_FILE_MAP: Record<string, string> = {
41
+ [VIRTUAL_IDS.rscEntry]: resolve(SRC_DIR, 'server', 'rsc-entry', 'index.ts'),
42
+ [VIRTUAL_IDS.ssrEntry]: resolve(SRC_DIR, 'server', 'ssr-entry.ts'),
43
+ [VIRTUAL_IDS.browserEntry]: resolve(SRC_DIR, 'client', 'browser-entry.ts'),
44
+ };
45
+
46
+ /** The \0-prefixed resolved ID for virtual:timber-config */
47
+ const RESOLVED_CONFIG_ID = `\0${VIRTUAL_IDS.config}`;
48
+
49
+ /**
50
+ * Strip the \0 prefix from a module ID.
51
+ *
52
+ * The RSC plugin generates browser entry imports using already-resolved
53
+ * \0-prefixed IDs. Vite's import-analysis cannot resolve these.
54
+ * See 18-build-system.md §"Resolution Quirks".
55
+ */
56
+ function stripNullPrefix(id: string): string {
57
+ return id.startsWith('\0') ? id.slice(1) : id;
58
+ }
59
+
60
+ /**
61
+ * Strip a root directory prefix from a module ID.
62
+ *
63
+ * Vite prefixes virtual module IDs with the project root when resolving
64
+ * SSR build entries. We need to handle both `virtual:timber-rsc-entry`
65
+ * and `<root>/virtual:timber-rsc-entry`.
66
+ * See 18-build-system.md §"Resolution Quirks".
67
+ */
68
+ function stripRootPrefix(id: string, root: string): string {
69
+ if (id.startsWith(root)) {
70
+ // Remove root + path separator
71
+ const stripped = id.slice(root.length);
72
+ // Handle both /virtual:... and \virtual:... (Windows)
73
+ if (stripped.startsWith('/') || stripped.startsWith('\\')) {
74
+ return stripped.slice(1);
75
+ }
76
+ return stripped;
77
+ }
78
+ return id;
79
+ }
80
+
81
+ /**
82
+ * Generate the virtual:timber-config module source.
83
+ *
84
+ * Serializes output mode and feature flags for runtime consumption.
85
+ */
86
+ function generateConfigModule(ctx: PluginContext): string {
87
+ // Resolve cookie secrets: `secret` shorthand expands to `secrets: [secret]`
88
+ const cookieSecrets = ctx.config.cookies?.secrets ??
89
+ (ctx.config.cookies?.secret ? [ctx.config.cookies.secret] : undefined);
90
+
91
+ const runtimeConfig = {
92
+ output: ctx.config.output ?? 'server',
93
+ csrf: ctx.config.csrf ?? true,
94
+ allowedOrigins: ctx.config.allowedOrigins,
95
+ clientJavascript: ctx.clientJavascript,
96
+ dev: ctx.dev ?? false,
97
+ slowPhaseMs: ctx.config.dev?.slowPhaseMs ?? 200,
98
+ cookieSecrets,
99
+ };
100
+
101
+ return [
102
+ '// Auto-generated runtime config — do not edit.',
103
+ '// Generated by timber-entries plugin.',
104
+ '',
105
+ `const config = ${JSON.stringify(runtimeConfig, null, 2)};`,
106
+ '',
107
+ 'export default config;',
108
+ ].join('\n');
109
+ }
110
+
111
+ /**
112
+ * Create the timber-entries Vite plugin.
113
+ *
114
+ * Hooks: resolveId, load
115
+ */
116
+ export function timberEntries(ctx: PluginContext): Plugin {
117
+ return {
118
+ name: 'timber-entries',
119
+
120
+ /**
121
+ * Resolve virtual entry and config module IDs.
122
+ *
123
+ * Handles:
124
+ * - virtual:timber-rsc-entry → real file path
125
+ * - virtual:timber-ssr-entry → real file path
126
+ * - virtual:timber-browser-entry → real file path
127
+ * - virtual:timber-config → \0-prefixed virtual ID
128
+ * - \0 prefix stripping (RSC plugin re-imports)
129
+ * - Root prefix stripping (SSR build entries)
130
+ */
131
+ resolveId(id: string) {
132
+ // Step 1: Strip \0 prefix if present
133
+ let cleanId = stripNullPrefix(id);
134
+
135
+ // Step 2: Strip root prefix if present
136
+ cleanId = stripRootPrefix(cleanId, ctx.root);
137
+
138
+ // Check entry file map (real files)
139
+ if (cleanId in ENTRY_FILE_MAP) {
140
+ return ENTRY_FILE_MAP[cleanId];
141
+ }
142
+
143
+ // Check config virtual module
144
+ if (cleanId === VIRTUAL_IDS.config) {
145
+ return RESOLVED_CONFIG_ID;
146
+ }
147
+
148
+ return null;
149
+ },
150
+
151
+ /**
152
+ * Load the virtual:timber-config module.
153
+ *
154
+ * Entry files (rsc/ssr/browser) are real TypeScript files that Vite
155
+ * processes normally. Only virtual:timber-config needs generated code.
156
+ */
157
+ load(id: string) {
158
+ if (id === RESOLVED_CONFIG_ID) {
159
+ return generateConfigModule(ctx);
160
+ }
161
+ return null;
162
+ },
163
+
164
+ /**
165
+ * Rename "rsc-entry" chunks in the client build to "rsc-client-entry".
166
+ *
167
+ * The RSC plugin creates client reference facades named after the RSC
168
+ * entry (virtual:timber-rsc-entry), producing chunks like "rsc-entry-XYZ.js"
169
+ * in the client output. This is confusing since those chunks contain client
170
+ * components, not server code. Renaming clarifies their purpose.
171
+ */
172
+ generateBundle(_options, bundle) {
173
+ if ((this as any).environment?.name !== 'client') return;
174
+
175
+ for (const [fileName, chunk] of Object.entries(bundle)) {
176
+ if (chunk.type !== 'chunk') continue;
177
+ if (!chunk.name?.startsWith('rsc-entry')) continue;
178
+
179
+ const newFileName = fileName.replace('rsc-entry', 'rsc-client-entry');
180
+ // Extract just the basename for matching code references like "./rsc-entry-XYZ.js"
181
+ const oldBase = fileName.split('/').pop()!;
182
+ const newBase = newFileName.split('/').pop()!;
183
+
184
+ chunk.fileName = newFileName;
185
+ chunk.name = chunk.name.replace('rsc-entry', 'rsc-client-entry');
186
+ bundle[newFileName] = chunk;
187
+ delete bundle[fileName];
188
+
189
+ // Update import references in other chunks
190
+ for (const other of Object.values(bundle)) {
191
+ if (other.type !== 'chunk') continue;
192
+ if (other.code.includes(oldBase)) {
193
+ other.code = other.code.replaceAll(oldBase, newBase);
194
+ }
195
+ if (other.imports) {
196
+ other.imports = other.imports.map((i) => (i === fileName ? newFileName : i));
197
+ }
198
+ if (other.dynamicImports) {
199
+ other.dynamicImports = other.dynamicImports.map((i) =>
200
+ i === fileName ? newFileName : i
201
+ );
202
+ }
203
+ }
204
+ }
205
+ },
206
+ };
207
+ }