@timber-js/app 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -7
- package/dist/index.js.map +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/package.json +5 -4
- package/src/adapters/cloudflare.ts +325 -0
- package/src/adapters/nitro.ts +366 -0
- package/src/adapters/types.ts +63 -0
- package/src/cache/index.ts +91 -0
- package/src/cache/redis-handler.ts +91 -0
- package/src/cache/register-cached-function.ts +99 -0
- package/src/cache/singleflight.ts +26 -0
- package/src/cache/stable-stringify.ts +21 -0
- package/src/cache/timber-cache.ts +116 -0
- package/src/cli.ts +201 -0
- package/src/client/browser-entry.ts +663 -0
- package/src/client/error-boundary.tsx +209 -0
- package/src/client/form.tsx +200 -0
- package/src/client/head.ts +61 -0
- package/src/client/history.ts +46 -0
- package/src/client/index.ts +60 -0
- package/src/client/link-navigate-interceptor.tsx +62 -0
- package/src/client/link-status-provider.tsx +40 -0
- package/src/client/link.tsx +310 -0
- package/src/client/nuqs-adapter.tsx +117 -0
- package/src/client/router-ref.ts +25 -0
- package/src/client/router.ts +563 -0
- package/src/client/segment-cache.ts +194 -0
- package/src/client/segment-context.ts +57 -0
- package/src/client/ssr-data.ts +95 -0
- package/src/client/types.ts +4 -0
- package/src/client/unload-guard.ts +34 -0
- package/src/client/use-cookie.ts +122 -0
- package/src/client/use-link-status.ts +46 -0
- package/src/client/use-navigation-pending.ts +47 -0
- package/src/client/use-params.ts +71 -0
- package/src/client/use-pathname.ts +43 -0
- package/src/client/use-query-states.ts +133 -0
- package/src/client/use-router.ts +77 -0
- package/src/client/use-search-params.ts +74 -0
- package/src/client/use-selected-layout-segment.ts +110 -0
- package/src/content/index.ts +13 -0
- package/src/cookies/define-cookie.ts +137 -0
- package/src/cookies/index.ts +9 -0
- package/src/fonts/ast.ts +359 -0
- package/src/fonts/css.ts +68 -0
- package/src/fonts/fallbacks.ts +248 -0
- package/src/fonts/google.ts +332 -0
- package/src/fonts/local.ts +177 -0
- package/src/fonts/types.ts +88 -0
- package/src/index.ts +420 -0
- package/src/plugins/adapter-build.ts +118 -0
- package/src/plugins/build-manifest.ts +323 -0
- package/src/plugins/build-report.ts +353 -0
- package/src/plugins/cache-transform.ts +199 -0
- package/src/plugins/chunks.ts +90 -0
- package/src/plugins/content.ts +136 -0
- package/src/plugins/dev-error-overlay.ts +230 -0
- package/src/plugins/dev-logs.ts +280 -0
- package/src/plugins/dev-server.ts +391 -0
- package/src/plugins/dynamic-transform.ts +161 -0
- package/src/plugins/entries.ts +214 -0
- package/src/plugins/fonts.ts +581 -0
- package/src/plugins/mdx.ts +179 -0
- package/src/plugins/react-prod.ts +56 -0
- package/src/plugins/routing.ts +419 -0
- package/src/plugins/server-action-exports.ts +220 -0
- package/src/plugins/server-bundle.ts +113 -0
- package/src/plugins/shims.ts +168 -0
- package/src/plugins/static-build.ts +207 -0
- package/src/routing/codegen.ts +396 -0
- package/src/routing/index.ts +14 -0
- package/src/routing/interception.ts +173 -0
- package/src/routing/scanner.ts +487 -0
- package/src/routing/status-file-lint.ts +114 -0
- package/src/routing/types.ts +100 -0
- package/src/search-params/analyze.ts +192 -0
- package/src/search-params/codecs.ts +153 -0
- package/src/search-params/create.ts +314 -0
- package/src/search-params/index.ts +23 -0
- package/src/search-params/registry.ts +31 -0
- package/src/server/access-gate.tsx +142 -0
- package/src/server/action-client.ts +473 -0
- package/src/server/action-handler.ts +325 -0
- package/src/server/actions.ts +236 -0
- package/src/server/asset-headers.ts +81 -0
- package/src/server/body-limits.ts +102 -0
- package/src/server/build-manifest.ts +234 -0
- package/src/server/canonicalize.ts +90 -0
- package/src/server/client-module-map.ts +58 -0
- package/src/server/csrf.ts +79 -0
- package/src/server/deny-renderer.ts +302 -0
- package/src/server/dev-logger.ts +419 -0
- package/src/server/dev-span-processor.ts +78 -0
- package/src/server/dev-warnings.ts +282 -0
- package/src/server/early-hints-sender.ts +55 -0
- package/src/server/early-hints.ts +142 -0
- package/src/server/error-boundary-wrapper.ts +69 -0
- package/src/server/error-formatter.ts +184 -0
- package/src/server/flush.ts +182 -0
- package/src/server/form-data.ts +176 -0
- package/src/server/form-flash.ts +93 -0
- package/src/server/html-injectors.ts +445 -0
- package/src/server/index.ts +222 -0
- package/src/server/instrumentation.ts +136 -0
- package/src/server/logger.ts +145 -0
- package/src/server/manifest-status-resolver.ts +215 -0
- package/src/server/metadata-render.ts +527 -0
- package/src/server/metadata-routes.ts +189 -0
- package/src/server/metadata.ts +263 -0
- package/src/server/middleware-runner.ts +32 -0
- package/src/server/nuqs-ssr-provider.tsx +63 -0
- package/src/server/pipeline.ts +555 -0
- package/src/server/prerender.ts +139 -0
- package/src/server/primitives.ts +264 -0
- package/src/server/proxy.ts +43 -0
- package/src/server/request-context.ts +554 -0
- package/src/server/route-element-builder.ts +395 -0
- package/src/server/route-handler.ts +153 -0
- package/src/server/route-matcher.ts +316 -0
- package/src/server/rsc-entry/api-handler.ts +112 -0
- package/src/server/rsc-entry/error-renderer.ts +177 -0
- package/src/server/rsc-entry/helpers.ts +147 -0
- package/src/server/rsc-entry/index.ts +688 -0
- package/src/server/rsc-entry/ssr-bridge.ts +18 -0
- package/src/server/slot-resolver.ts +359 -0
- package/src/server/ssr-entry.ts +161 -0
- package/src/server/ssr-render.ts +200 -0
- package/src/server/status-code-resolver.ts +282 -0
- package/src/server/tracing.ts +281 -0
- package/src/server/tree-builder.ts +354 -0
- package/src/server/types.ts +150 -0
- package/src/shims/font-google.ts +67 -0
- package/src/shims/headers.ts +11 -0
- package/src/shims/image.ts +48 -0
- package/src/shims/link.ts +9 -0
- package/src/shims/navigation-client.ts +52 -0
- package/src/shims/navigation.ts +31 -0
- package/src/shims/server-only-noop.js +5 -0
- package/src/utils/directive-parser.ts +529 -0
- package/src/utils/format.ts +10 -0
- package/src/utils/startup-timer.ts +102 -0
|
@@ -0,0 +1,220 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* timber-server-bundle — Bundle all dependencies for server environments.
|
|
3
|
+
*
|
|
4
|
+
* In production builds, sets `resolve.noExternal: true` for the rsc and ssr
|
|
5
|
+
* environments. This ensures all npm dependencies are bundled into the output,
|
|
6
|
+
* which is required for platforms like Cloudflare Workers that don't have
|
|
7
|
+
* access to node_modules at runtime.
|
|
8
|
+
*
|
|
9
|
+
* In dev mode, Vite's default externalization is preserved for fast HMR.
|
|
10
|
+
*
|
|
11
|
+
* Design docs: design/11-platform.md, design/25-production-deployments.md
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Plugin } from 'vite';
|
|
15
|
+
|
|
16
|
+
export function timberServerBundle(): Plugin[] {
|
|
17
|
+
const bundlePlugin: Plugin = {
|
|
18
|
+
name: 'timber-server-bundle',
|
|
19
|
+
|
|
20
|
+
config(_cfg, { command }) {
|
|
21
|
+
// In dev mode, Vite externalizes node_modules by default for fast HMR.
|
|
22
|
+
// But server-only/client-only must NOT be externalized — they need to
|
|
23
|
+
// go through the timber-shims plugin so it can replace them with no-op
|
|
24
|
+
// virtual modules. Without this, deps like `bright` that import
|
|
25
|
+
// `server-only` get the real CJS package loaded via Node's require(),
|
|
26
|
+
// which throws in the SSR environment.
|
|
27
|
+
if (command === 'serve') {
|
|
28
|
+
// In dev, Vite externalizes node_modules and loads them via Node's
|
|
29
|
+
// native require(). Deps that import `server-only` (like `bright`)
|
|
30
|
+
// hit the real CJS package which throws at runtime. We force these
|
|
31
|
+
// poison-pill packages to be non-external so they go through Vite's
|
|
32
|
+
// module pipeline, where the timber-shims plugin intercepts them
|
|
33
|
+
// and serves no-op virtual modules for server environments.
|
|
34
|
+
return {
|
|
35
|
+
environments: {
|
|
36
|
+
rsc: {
|
|
37
|
+
resolve: {
|
|
38
|
+
noExternal: ['server-only', 'client-only'],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
ssr: {
|
|
42
|
+
resolve: {
|
|
43
|
+
noExternal: ['server-only', 'client-only'],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Bundle all dependencies in server environments for production.
|
|
51
|
+
// Without this, bare imports like 'nuqs/adapters/custom' are left
|
|
52
|
+
// as external imports in the output, which fails on platforms
|
|
53
|
+
// without node_modules (Cloudflare Workers, edge runtimes).
|
|
54
|
+
// Define process.env.NODE_ENV in server environments so that
|
|
55
|
+
// dead-code branches (dev-only tracing, React dev checks) are
|
|
56
|
+
// eliminated by Rollup's tree-shaking. Without this, the runtime
|
|
57
|
+
// check falls through on platforms where process.env is empty
|
|
58
|
+
// (e.g. Cloudflare Workers), causing dev code to run in production.
|
|
59
|
+
const serverDefine = {
|
|
60
|
+
'process.env.NODE_ENV': JSON.stringify('production'),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
// Target webworker so Rolldown doesn't emit createRequire(import.meta.url)
|
|
65
|
+
// shims that fail in Cloudflare Workers where import.meta.url is undefined
|
|
66
|
+
// for non-entry modules. See design/11-platform.md.
|
|
67
|
+
ssr: { target: 'webworker' },
|
|
68
|
+
environments: {
|
|
69
|
+
rsc: {
|
|
70
|
+
resolve: { noExternal: true },
|
|
71
|
+
define: serverDefine,
|
|
72
|
+
},
|
|
73
|
+
ssr: {
|
|
74
|
+
resolve: { noExternal: true },
|
|
75
|
+
define: serverDefine,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Fix Rolldown's broken `__esmMin` lazy initializers in server bundles.
|
|
83
|
+
//
|
|
84
|
+
// Rolldown wraps ESM module initialization in `__esmMin` lazy functions.
|
|
85
|
+
// For packages with `sideEffects: false` (e.g. nuqs), Rolldown drops
|
|
86
|
+
// the variable assignment of the init function — so the module's React
|
|
87
|
+
// imports, context creation, etc. never execute.
|
|
88
|
+
//
|
|
89
|
+
// The fix: patch the `__esmMin` runtime definition to eagerly execute
|
|
90
|
+
// the init callback while still returning the lazy wrapper. This makes
|
|
91
|
+
// all ESM module inits run at load time (standard ESM behavior) instead
|
|
92
|
+
// of lazily, which is functionally correct and avoids the dropped-init bug.
|
|
93
|
+
const esmInitFixPlugin: Plugin = {
|
|
94
|
+
name: 'timber-esm-init-fix',
|
|
95
|
+
applyToEnvironment(environment) {
|
|
96
|
+
return environment.name === 'rsc' || environment.name === 'ssr';
|
|
97
|
+
},
|
|
98
|
+
renderChunk(code) {
|
|
99
|
+
const lazy = 'var __esmMin = (fn, res) => () => (fn && (res = fn(fn = 0)), res);';
|
|
100
|
+
if (!code.includes(lazy)) return null;
|
|
101
|
+
|
|
102
|
+
// Replace with eager-then-lazy: execute init immediately, then
|
|
103
|
+
// return the lazy wrapper for any subsequent calls (which are
|
|
104
|
+
// idempotent since fn is set to 0 after first execution).
|
|
105
|
+
const eager =
|
|
106
|
+
'var __esmMin = (fn, res) => { var l = () => (fn && (res = fn(fn = 0)), res); l(); return l; };';
|
|
107
|
+
|
|
108
|
+
return { code: code.replace(lazy, eager), map: null };
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return [bundlePlugin, esmInitFixPlugin];
|
|
113
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* timber-shims — Vite sub-plugin for next/* → timber shim resolution.
|
|
3
|
+
*
|
|
4
|
+
* Intercepts imports of next/* modules and redirects them to timber.js
|
|
5
|
+
* shim implementations. This enables Next.js-compatible libraries
|
|
6
|
+
* (nuqs, next-intl, etc.) to work unmodified.
|
|
7
|
+
*
|
|
8
|
+
* Design doc: 18-build-system.md §"Shim Map"
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Plugin } from 'vite';
|
|
12
|
+
import { resolve, dirname } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import type { PluginContext } from '#/index.js';
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const SHIMS_DIR = resolve(__dirname, '..', 'shims');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Virtual module IDs for server-only and client-only poison pills.
|
|
21
|
+
*
|
|
22
|
+
* These packages cause build errors when imported in the wrong environment:
|
|
23
|
+
* - `server-only` errors when imported in a client component
|
|
24
|
+
* - `client-only` errors when imported in a server component
|
|
25
|
+
*/
|
|
26
|
+
const SERVER_ONLY_VIRTUAL = '\0timber:server-only';
|
|
27
|
+
const CLIENT_ONLY_VIRTUAL = '\0timber:client-only';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Map from next/* import specifiers to shim file paths.
|
|
31
|
+
*
|
|
32
|
+
* The shim map is a separate data structure (not embedded in the plugin)
|
|
33
|
+
* per the task's approach constraints.
|
|
34
|
+
*/
|
|
35
|
+
const SHIM_MAP: Record<string, string> = {
|
|
36
|
+
'next/link': resolve(SHIMS_DIR, 'link.ts'),
|
|
37
|
+
'next/image': resolve(SHIMS_DIR, 'image.ts'),
|
|
38
|
+
'next/navigation': resolve(SHIMS_DIR, 'navigation.ts'),
|
|
39
|
+
'next/headers': resolve(SHIMS_DIR, 'headers.ts'),
|
|
40
|
+
// next/font/* redirects to the timber-fonts virtual modules.
|
|
41
|
+
// The fonts plugin's load hook serves the actual module code.
|
|
42
|
+
'next/font/google': '\0@timber/fonts/google',
|
|
43
|
+
'next/font/local': '\0@timber/fonts/local',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Client-only shim overrides for the browser environment.
|
|
48
|
+
*
|
|
49
|
+
* next/navigation in the client environment resolves to navigation-client.ts
|
|
50
|
+
* which only re-exports client hooks — not server functions like redirect()
|
|
51
|
+
* and deny(). This prevents server/primitives.ts from being pulled into the
|
|
52
|
+
* browser bundle via tree-shaking-resistant imports.
|
|
53
|
+
*/
|
|
54
|
+
const CLIENT_SHIM_OVERRIDES: Record<string, string> = {
|
|
55
|
+
'next/navigation': resolve(SHIMS_DIR, 'navigation-client.ts'),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Map from @timber/app/* subpath imports to real source files.
|
|
60
|
+
*
|
|
61
|
+
* These resolve subpath imports like `@timber/app/server` to the
|
|
62
|
+
* real entry files in the package source.
|
|
63
|
+
*/
|
|
64
|
+
const TIMBER_SUBPATH_MAP: Record<string, string> = {
|
|
65
|
+
'@timber/app/server': resolve(__dirname, '..', 'server', 'index.ts'),
|
|
66
|
+
'@timber/app/client': resolve(__dirname, '..', 'client', 'index.ts'),
|
|
67
|
+
'@timber/app/cache': resolve(__dirname, '..', 'cache', 'index.ts'),
|
|
68
|
+
'@timber/app/search-params': resolve(__dirname, '..', 'search-params', 'index.ts'),
|
|
69
|
+
'@timber/app/routing': resolve(__dirname, '..', 'routing', 'index.ts'),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Strip .js extension from an import specifier.
|
|
74
|
+
*
|
|
75
|
+
* Libraries like nuqs import `next/navigation.js` with an explicit
|
|
76
|
+
* extension. We strip it before matching against the shim map.
|
|
77
|
+
*/
|
|
78
|
+
function stripJsExtension(id: string): string {
|
|
79
|
+
return id.endsWith('.js') ? id.slice(0, -3) : id;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Create the timber-shims Vite plugin.
|
|
84
|
+
*
|
|
85
|
+
* Hooks: resolveId, load
|
|
86
|
+
*/
|
|
87
|
+
export function timberShims(_ctx: PluginContext): Plugin {
|
|
88
|
+
return {
|
|
89
|
+
name: 'timber-shims',
|
|
90
|
+
// Must run before Vite's built-in resolution so that server-only/client-only
|
|
91
|
+
// poison pills are intercepted even when imported from node_modules deps
|
|
92
|
+
// (e.g. bright, next-intl). Without this, the dep optimizer resolves to the
|
|
93
|
+
// real CJS package which throws at runtime in the SSR environment.
|
|
94
|
+
enforce: 'pre',
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve next/* and @timber/app/* imports to shim/source files.
|
|
98
|
+
*
|
|
99
|
+
* Resolution order:
|
|
100
|
+
* 1. Check server-only / client-only poison pill packages
|
|
101
|
+
* 2. Strip .js extension from the import specifier
|
|
102
|
+
* 3. Check next/* shim map
|
|
103
|
+
* 4. Check @timber/app/* subpath map
|
|
104
|
+
* 5. Return null (pass through) for unrecognized imports
|
|
105
|
+
*/
|
|
106
|
+
resolveId(id: string) {
|
|
107
|
+
// Poison pill packages — resolve to virtual modules handled by load()
|
|
108
|
+
if (id === 'server-only') return SERVER_ONLY_VIRTUAL;
|
|
109
|
+
if (id === 'client-only') return CLIENT_ONLY_VIRTUAL;
|
|
110
|
+
|
|
111
|
+
const cleanId = stripJsExtension(id);
|
|
112
|
+
|
|
113
|
+
// Check next/* shim map.
|
|
114
|
+
// In the client (browser) environment, use client-only shim overrides
|
|
115
|
+
// to avoid pulling server code (primitives.ts) into the browser bundle.
|
|
116
|
+
if (cleanId in SHIM_MAP) {
|
|
117
|
+
const envName = (this as unknown as { environment?: { name?: string } }).environment?.name;
|
|
118
|
+
if (envName === 'client' && cleanId in CLIENT_SHIM_OVERRIDES) {
|
|
119
|
+
return CLIENT_SHIM_OVERRIDES[cleanId];
|
|
120
|
+
}
|
|
121
|
+
return SHIM_MAP[cleanId];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check @timber/app/* subpath map
|
|
125
|
+
if (cleanId in TIMBER_SUBPATH_MAP) {
|
|
126
|
+
return TIMBER_SUBPATH_MAP[cleanId];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return null;
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Serve virtual modules for server-only / client-only poison pills.
|
|
134
|
+
*
|
|
135
|
+
* In the correct environment, the module is a no-op (empty export).
|
|
136
|
+
* In the wrong environment, it throws a build-time error message that
|
|
137
|
+
* clearly identifies the boundary violation.
|
|
138
|
+
*/
|
|
139
|
+
load(id: string) {
|
|
140
|
+
const envName = (this as unknown as { environment?: { name?: string } }).environment?.name;
|
|
141
|
+
const isClient = envName === 'client';
|
|
142
|
+
|
|
143
|
+
if (id === SERVER_ONLY_VIRTUAL) {
|
|
144
|
+
if (isClient) {
|
|
145
|
+
return `throw new Error(
|
|
146
|
+
"This module cannot be imported from a Client Component module. " +
|
|
147
|
+
"It should only be used from a Server Component."
|
|
148
|
+
);`;
|
|
149
|
+
}
|
|
150
|
+
// No-op in server environments (rsc, ssr)
|
|
151
|
+
return 'export {};';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (id === CLIENT_ONLY_VIRTUAL) {
|
|
155
|
+
if (!isClient) {
|
|
156
|
+
return `throw new Error(
|
|
157
|
+
"This module cannot be imported from a Server Component module. " +
|
|
158
|
+
"It should only be used from a Client Component."
|
|
159
|
+
);`;
|
|
160
|
+
}
|
|
161
|
+
// No-op in client environment
|
|
162
|
+
return 'export {};';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return null;
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|