@timber-js/app 0.2.0-alpha.152 → 0.2.0-alpha.153
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/_chunks/actions-8lJZYYGq.js +298 -0
- package/dist/_chunks/actions-8lJZYYGq.js.map +1 -0
- package/dist/_chunks/{cache-api-Co-yI-gP.js → cache-api-e1E4i3Vy.js} +3 -26
- package/dist/_chunks/cache-api-e1E4i3Vy.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync-DJDdyEae.js → cli-schema-sync-ChsRzw8o.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-DJDdyEae.js.map → cli-schema-sync-ChsRzw8o.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-D_IhHEUW.js → cli-schema-sync-D4urWkeV.js} +1 -1
- package/dist/_chunks/{actions-a0RczDnB.js → logger-DvmCWhN9.js} +665 -274
- package/dist/_chunks/logger-DvmCWhN9.js.map +1 -0
- package/dist/_chunks/plugin-context-GfDWsdd2.js.map +1 -1
- package/dist/_chunks/{walkers-DvWDj453.js → walkers-DZuFKqFX.js} +2 -2
- package/dist/_chunks/{walkers-DvWDj453.js.map → walkers-DZuFKqFX.js.map} +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cli.js +1 -1
- package/dist/client/browser-dev.d.ts +3 -0
- package/dist/client/browser-dev.d.ts.map +1 -1
- package/dist/config-types.d.ts +8 -0
- package/dist/config-types.d.ts.map +1 -1
- package/dist/config-validation.d.ts.map +1 -1
- package/dist/index.js +193 -48
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts +9 -0
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/build-report.d.ts +13 -0
- package/dist/plugins/build-report.d.ts.map +1 -1
- package/dist/plugins/chunks.d.ts +11 -4
- package/dist/plugins/chunks.d.ts.map +1 -1
- package/dist/plugins/client-chunks.d.ts +23 -22
- package/dist/plugins/client-chunks.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/server/index.js +2 -2
- package/dist/server/internal.js +65 -2
- package/dist/server/internal.js.map +1 -1
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +8 -0
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/stream-utils.d.ts +11 -0
- package/dist/server/stream-utils.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/client/browser-dev.ts +3 -0
- package/src/config-types.ts +8 -0
- package/src/config-validation.ts +16 -0
- package/src/index.ts +2 -2
- package/src/plugin-context.ts +9 -0
- package/src/plugins/build-report.ts +105 -10
- package/src/plugins/chunks.ts +74 -5
- package/src/plugins/client-chunks.ts +73 -42
- package/src/server/logger.ts +5 -0
- package/src/server/pipeline.ts +13 -0
- package/src/server/primitives.ts +29 -0
- package/src/server/stream-utils.ts +73 -0
- package/dist/_chunks/actions-a0RczDnB.js.map +0 -1
- package/dist/_chunks/cache-api-Co-yI-gP.js.map +0 -1
- package/dist/_chunks/logger-CM0clugc.js +0 -645
- package/dist/_chunks/logger-CM0clugc.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/server/logger.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/server/logger.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/server/pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/server/pipeline.ts"],"names":[],"mappings":"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"}
|
|
@@ -70,6 +70,14 @@ export declare class RedirectSignal extends Error {
|
|
|
70
70
|
readonly status: number;
|
|
71
71
|
constructor(location: string, status: number);
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Returns true if the error is a framework control-flow signal (RedirectSignal
|
|
75
|
+
* or DenySignal) rather than a genuine application error. Works both for direct
|
|
76
|
+
* instances (RSC-side) and deserialized digest errors (SSR cross-boundary).
|
|
77
|
+
*
|
|
78
|
+
* See also: isFrameworkSignalError() in client/browser-dev.ts (client-only).
|
|
79
|
+
*/
|
|
80
|
+
export declare function isControlFlowSignal(error: unknown): boolean;
|
|
73
81
|
/**
|
|
74
82
|
* Options for redirect() — alternative to passing a bare status code.
|
|
75
83
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"primitives.d.ts","sourceRoot":"","sources":["../../src/server/primitives.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"primitives.d.ts","sourceRoot":"","sources":["../../src/server/primitives.ts"],"names":[],"mappings":"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"}
|
|
@@ -33,4 +33,15 @@ export interface TeeOptions {
|
|
|
33
33
|
* The returned streams are drop-in replacements for the native tee branches.
|
|
34
34
|
*/
|
|
35
35
|
export declare function teeWithErrorPropagation(stream: ReadableStream<Uint8Array>, options?: TeeOptions): [ReadableStream<Uint8Array>, ReadableStream<Uint8Array>];
|
|
36
|
+
/**
|
|
37
|
+
* Wrap a response body with stream-lifecycle timing instrumentation.
|
|
38
|
+
*
|
|
39
|
+
* Logs firstChunk, lastChunk, close, maxGap (largest inter-chunk gap),
|
|
40
|
+
* chunk count, and byte count on stream termination (clean close OR
|
|
41
|
+
* cancellation/disconnect). Gated behind `serverTiming: 'detailed'`.
|
|
42
|
+
*
|
|
43
|
+
* Uses a manual reader pump to guarantee logging on all termination
|
|
44
|
+
* paths — TransformStream.flush() is not called on cancellation.
|
|
45
|
+
*/
|
|
46
|
+
export declare function wrapResponseWithStreamTiming(body: ReadableStream<Uint8Array>, startTime: number): ReadableStream<Uint8Array>;
|
|
36
47
|
//# sourceMappingURL=stream-utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream-utils.d.ts","sourceRoot":"","sources":["../../src/server/stream-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,MAAM,WAAW,UAAU;IACzB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,OAAO,CAAC,EAAE,UAAU,GACnB,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC,CAmK1D"}
|
|
1
|
+
{"version":3,"file":"stream-utils.d.ts","sourceRoot":"","sources":["../../src/server/stream-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,MAAM,WAAW,UAAU;IACzB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,OAAO,CAAC,EAAE,UAAU,GACnB,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,cAAc,CAAC,UAAU,CAAC,CAAC,CAmK1D;AAED;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,EAChC,SAAS,EAAE,MAAM,GAChB,cAAc,CAAC,UAAU,CAAC,CA0D5B"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timber-js/app",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.153",
|
|
4
4
|
"description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare-workers",
|
|
@@ -116,10 +116,10 @@
|
|
|
116
116
|
},
|
|
117
117
|
"dependencies": {
|
|
118
118
|
"@opentelemetry/api": "^1.9.1",
|
|
119
|
-
"@opentelemetry/context-async-hooks": "^2.
|
|
120
|
-
"@opentelemetry/sdk-trace-base": "^2.
|
|
119
|
+
"@opentelemetry/context-async-hooks": "^2.8.0",
|
|
120
|
+
"@opentelemetry/sdk-trace-base": "^2.8.0",
|
|
121
121
|
"magic-string": "^0.30.21",
|
|
122
|
-
"nitro": "3.0.
|
|
122
|
+
"nitro": "3.0.260610-beta"
|
|
123
123
|
},
|
|
124
124
|
"peerDependencies": {
|
|
125
125
|
"@content-collections/core": "^0.14.0",
|
|
@@ -279,6 +279,9 @@ export function setupClientErrorForwarding(hot: Pick<HotInterface, 'send'>): voi
|
|
|
279
279
|
* (RedirectSignal, DenySignal). These must never surface as dev overlays —
|
|
280
280
|
* they are expected during navigation and are handled by the error boundary
|
|
281
281
|
* and router. Exported for tests.
|
|
282
|
+
*
|
|
283
|
+
* See also: isControlFlowSignal() in server/primitives.ts (server-side,
|
|
284
|
+
* handles both instanceof and digest detection).
|
|
282
285
|
*/
|
|
283
286
|
export function isFrameworkSignalError(error: unknown): boolean {
|
|
284
287
|
if (!error || typeof error !== 'object') return false;
|
package/src/config-types.ts
CHANGED
|
@@ -177,6 +177,14 @@ export interface TimberUserConfig {
|
|
|
177
177
|
* logging.
|
|
178
178
|
*/
|
|
179
179
|
serverTiming?: 'detailed' | 'total' | false;
|
|
180
|
+
/**
|
|
181
|
+
* Build-time performance budgets. When exceeded, the build report emits
|
|
182
|
+
* a warning (does not fail the build).
|
|
183
|
+
*/
|
|
184
|
+
budget?: {
|
|
185
|
+
/** Warn when any route's first-load JS (gzip) exceeds this many bytes. */
|
|
186
|
+
firstLoadJs?: number;
|
|
187
|
+
};
|
|
180
188
|
/**
|
|
181
189
|
* Override the app directory location. By default, timber auto-detects
|
|
182
190
|
* `app/` at the project root, falling back to `src/app/`.
|
package/src/config-validation.ts
CHANGED
|
@@ -49,6 +49,7 @@ const KNOWN_CONFIG_KEYS = {
|
|
|
49
49
|
sitemap: true,
|
|
50
50
|
buildDir: true,
|
|
51
51
|
topLoader: true,
|
|
52
|
+
budget: true,
|
|
52
53
|
} satisfies Record<keyof TimberUserConfig, true>;
|
|
53
54
|
|
|
54
55
|
const knownKeys = new Set<string>(Object.keys(KNOWN_CONFIG_KEYS));
|
|
@@ -180,6 +181,21 @@ export function validateConfig(config: TimberUserConfig): ConfigError[] {
|
|
|
180
181
|
}
|
|
181
182
|
}
|
|
182
183
|
|
|
184
|
+
// budget
|
|
185
|
+
if (config.budget != null && typeof config.budget === 'object') {
|
|
186
|
+
if (
|
|
187
|
+
config.budget.firstLoadJs !== undefined &&
|
|
188
|
+
(typeof config.budget.firstLoadJs !== 'number' || config.budget.firstLoadJs <= 0)
|
|
189
|
+
) {
|
|
190
|
+
errors.push({
|
|
191
|
+
field: 'budget.firstLoadJs',
|
|
192
|
+
message: `budget.firstLoadJs must be a positive number in bytes (got ${JSON.stringify(config.budget.firstLoadJs)}).`,
|
|
193
|
+
value: config.budget.firstLoadJs,
|
|
194
|
+
suggestion: 'Example: budget: { firstLoadJs: 150_000 } for a ~150 kB limit.',
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
183
199
|
for (const key of Object.keys(config)) {
|
|
184
200
|
if (!knownKeys.has(key)) {
|
|
185
201
|
errors.push({
|
package/src/index.ts
CHANGED
|
@@ -156,7 +156,7 @@ function createRscOptions(
|
|
|
156
156
|
// Layout-boundary grouping is the middle ground: components under the same
|
|
157
157
|
// layout segment share a chunk. See design/27-chunking-strategy.md, TIM-440, TIM-499.
|
|
158
158
|
clientChunks: (meta: { id: string; normalizedId: string; serverChunk: string }) =>
|
|
159
|
-
clientChunkGroup(meta, ctx.appDir, ctx.root),
|
|
159
|
+
clientChunkGroup(meta, ctx.appDir, ctx.root, ctx.sharedModuleFacades),
|
|
160
160
|
};
|
|
161
161
|
|
|
162
162
|
// Bound args encryption — AES-256-GCM authenticated encryption for
|
|
@@ -538,7 +538,7 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
|
|
|
538
538
|
timberContent(ctx),
|
|
539
539
|
timberServerBundle(), // Bundle all deps in server environments for prod
|
|
540
540
|
timberCloudflareWasm(ctx), // Externalize WASM for Cloudflare Workers
|
|
541
|
-
timberChunks(),
|
|
541
|
+
timberChunks(ctx),
|
|
542
542
|
timberBuildReport(ctx), // Post-build: route table with bundle sizes
|
|
543
543
|
timberAdapterBuild(ctx), // Post-build: invoke adapter.buildOutput()
|
|
544
544
|
timberDevLogs(ctx), // Dev-only: forward server console.* to browser console
|
package/src/plugin-context.ts
CHANGED
|
@@ -94,6 +94,15 @@ export interface PluginContext {
|
|
|
94
94
|
* populate per-route modulepreload entries from clientReferenceDeps.
|
|
95
95
|
*/
|
|
96
96
|
routeClientRefKeys?: Map<string, Set<string>>;
|
|
97
|
+
/**
|
|
98
|
+
* Maps absolute module IDs of client references in non-facade RSC
|
|
99
|
+
* chunks to the absolute paths of their consuming facade modules.
|
|
100
|
+
* Built during the RSC `generateBundle` phase by timber-chunks,
|
|
101
|
+
* consumed by `clientChunkGroup` to reclassify `shared:` modules
|
|
102
|
+
* that are only used by a single route boundary.
|
|
103
|
+
* See design/27-chunking-strategy.md, TIM-1107.
|
|
104
|
+
*/
|
|
105
|
+
sharedModuleFacades?: Map<string, string[]>;
|
|
97
106
|
}
|
|
98
107
|
|
|
99
108
|
// ── App directory resolution ──────────────────────────────────────────────
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* Task: TIM-287
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { relative } from 'node:path';
|
|
16
17
|
import { gzipSync } from 'node:zlib';
|
|
17
18
|
import type { Plugin, Logger } from 'vite';
|
|
18
19
|
import type { PluginContext } from '../plugin-context.js';
|
|
@@ -160,6 +161,21 @@ export function buildRouteReport(entries: RouteEntry[], sharedSize: number): str
|
|
|
160
161
|
return lines;
|
|
161
162
|
}
|
|
162
163
|
|
|
164
|
+
/** Check route entries against a First-Load-JS budget and return warning lines. */
|
|
165
|
+
export function buildWarnings(entries: RouteEntry[], budget?: { firstLoadJs?: number }): string[] {
|
|
166
|
+
if (!budget?.firstLoadJs) return [];
|
|
167
|
+
const limit = budget.firstLoadJs;
|
|
168
|
+
const warnings: string[] = [];
|
|
169
|
+
for (const entry of entries) {
|
|
170
|
+
if (entry.firstLoadSize > limit) {
|
|
171
|
+
warnings.push(
|
|
172
|
+
`⚠ ${entry.path} first-load JS (${formatSize(entry.firstLoadSize)}) exceeds budget (${formatSize(limit)})`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return warnings;
|
|
177
|
+
}
|
|
178
|
+
|
|
163
179
|
function pad(str: string, width: number, align: 'left' | 'right' = 'right'): string {
|
|
164
180
|
const gap = Math.max(0, width - stripAnsi(str).length);
|
|
165
181
|
return align === 'left' ? ' '.repeat(gap) + str : str + ' '.repeat(gap);
|
|
@@ -180,6 +196,7 @@ interface ChunkSize {
|
|
|
180
196
|
interface OutputChunkLike {
|
|
181
197
|
type: 'chunk' | 'asset';
|
|
182
198
|
fileName: string;
|
|
199
|
+
name?: string;
|
|
183
200
|
code?: string;
|
|
184
201
|
source?: string | Uint8Array;
|
|
185
202
|
modules?: Record<string, unknown>;
|
|
@@ -219,13 +236,62 @@ export function findChunkForFile(
|
|
|
219
236
|
return null;
|
|
220
237
|
}
|
|
221
238
|
|
|
239
|
+
// ─── Route → chunk group matching ────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Derive the expected client chunk group prefix for a route.
|
|
243
|
+
*
|
|
244
|
+
* Mirrors the `clientChunkGroup` logic: extract the first non-group segment
|
|
245
|
+
* from the route's file path under appDir. Routes at the root level or
|
|
246
|
+
* without a matching segment return null (their JS is in client-shared).
|
|
247
|
+
*/
|
|
248
|
+
function routeChunkGroupPrefix(entryFilePath: string, appDir: string, root: string): string | null {
|
|
249
|
+
const appPrefix = relative(root, appDir).replace(/\\/g, '/') + '/';
|
|
250
|
+
const relPath = relative(root, entryFilePath).replace(/\\/g, '/');
|
|
251
|
+
if (!relPath.startsWith(appPrefix)) return null;
|
|
252
|
+
|
|
253
|
+
const withinApp = relPath.slice(appPrefix.length);
|
|
254
|
+
const segments = withinApp.split('/');
|
|
255
|
+
for (const seg of segments) {
|
|
256
|
+
if (seg.includes('.')) break;
|
|
257
|
+
if (seg.startsWith('(') && seg.endsWith(')')) continue;
|
|
258
|
+
return `client-${seg}`;
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Find all client bundle chunks belonging to a chunk group.
|
|
265
|
+
*
|
|
266
|
+
* Uses the chunk's `name` property (set by Rolldown from the `clientChunks`
|
|
267
|
+
* callback's return value) for exact matching. The `name` is the group name
|
|
268
|
+
* itself (e.g. `client-docs`), not the hashed filename.
|
|
269
|
+
*/
|
|
270
|
+
export function findChunksForGroup(
|
|
271
|
+
groupName: string,
|
|
272
|
+
chunkSizes: Map<string, ChunkSize>,
|
|
273
|
+
bundle?: Record<string, OutputChunkLike>
|
|
274
|
+
): string[] {
|
|
275
|
+
if (!bundle) return [];
|
|
276
|
+
const matched: string[] = [];
|
|
277
|
+
for (const [fileName, item] of Object.entries(bundle)) {
|
|
278
|
+
if (item.type !== 'chunk') continue;
|
|
279
|
+
if (item.name === groupName && chunkSizes.has(fileName)) {
|
|
280
|
+
matched.push(fileName);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return matched;
|
|
284
|
+
}
|
|
285
|
+
|
|
222
286
|
// ─── Build route entries from collected data ──────────────────────────────
|
|
223
287
|
|
|
224
288
|
function buildEntries(
|
|
225
289
|
routeTree: RouteTree,
|
|
226
290
|
chunkSizes: Map<string, ChunkSize>,
|
|
227
291
|
bundle: Record<string, OutputChunkLike>,
|
|
228
|
-
outputMode: 'server' | 'static'
|
|
292
|
+
outputMode: 'server' | 'static',
|
|
293
|
+
appDir?: string,
|
|
294
|
+
root?: string
|
|
229
295
|
): { entries: RouteEntry[]; sharedGzip: number } {
|
|
230
296
|
const routeInfos = collectRoutes(routeTree);
|
|
231
297
|
|
|
@@ -233,7 +299,7 @@ function buildEntries(
|
|
|
233
299
|
let totalGzip = 0;
|
|
234
300
|
for (const s of chunkSizes.values()) totalGzip += s.gzip;
|
|
235
301
|
|
|
236
|
-
// Per-route sizes
|
|
302
|
+
// Per-route sizes. Track route-specific chunks to compute shared size.
|
|
237
303
|
const routeChunkFiles = new Set<string>();
|
|
238
304
|
const entries: RouteEntry[] = [];
|
|
239
305
|
|
|
@@ -242,14 +308,36 @@ function buildEntries(
|
|
|
242
308
|
let gzip = 0;
|
|
243
309
|
|
|
244
310
|
if (info.entryFilePath) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
311
|
+
// Try chunk group matching first (works for server component routes
|
|
312
|
+
// whose client JS is in client-<segment> chunks, not in the route's
|
|
313
|
+
// own page.tsx chunk). Falls back to direct chunk lookup.
|
|
314
|
+
let matched = false;
|
|
315
|
+
if (appDir && root) {
|
|
316
|
+
const groupPrefix = routeChunkGroupPrefix(info.entryFilePath, appDir, root);
|
|
317
|
+
if (groupPrefix) {
|
|
318
|
+
const chunks = findChunksForGroup(groupPrefix, chunkSizes, bundle);
|
|
319
|
+
for (const chunk of chunks) {
|
|
320
|
+
const s = chunkSizes.get(chunk);
|
|
321
|
+
if (s) {
|
|
322
|
+
raw += s.raw;
|
|
323
|
+
gzip += s.gzip;
|
|
324
|
+
}
|
|
325
|
+
routeChunkFiles.add(chunk);
|
|
326
|
+
}
|
|
327
|
+
if (chunks.length > 0) matched = true;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!matched) {
|
|
332
|
+
const chunk = findChunkForFile(info.entryFilePath, bundle);
|
|
333
|
+
if (chunk) {
|
|
334
|
+
const s = chunkSizes.get(chunk);
|
|
335
|
+
if (s) {
|
|
336
|
+
raw = s.raw;
|
|
337
|
+
gzip = s.gzip;
|
|
338
|
+
}
|
|
339
|
+
routeChunkFiles.add(chunk);
|
|
251
340
|
}
|
|
252
|
-
routeChunkFiles.add(chunk);
|
|
253
341
|
}
|
|
254
342
|
}
|
|
255
343
|
|
|
@@ -348,12 +436,19 @@ export function timberBuildReport(ctx: PluginContext): Plugin {
|
|
|
348
436
|
})),
|
|
349
437
|
sharedGzip: 0,
|
|
350
438
|
}
|
|
351
|
-
: buildEntries(ctx.routeTree, chunkSizes, clientBundle, outputMode);
|
|
439
|
+
: buildEntries(ctx.routeTree, chunkSizes, clientBundle, outputMode, ctx.appDir, ctx.root);
|
|
352
440
|
const elapsed = ((performance.now() - buildStart) / 1000).toFixed(2);
|
|
353
441
|
const lines = buildRouteReport(entries, sharedGzip);
|
|
354
442
|
|
|
355
443
|
logger.info('');
|
|
356
444
|
for (const line of lines) logger.info(line);
|
|
445
|
+
|
|
446
|
+
const warnings = buildWarnings(entries, ctx.config.budget);
|
|
447
|
+
if (warnings.length > 0) {
|
|
448
|
+
logger.info('');
|
|
449
|
+
for (const w of warnings) logger.warn(w);
|
|
450
|
+
}
|
|
451
|
+
|
|
357
452
|
logger.info('');
|
|
358
453
|
logger.info(
|
|
359
454
|
`✓ built ${entries.length} routes for all three environments (rsc, ssr, client) in ${elapsed}s`
|
package/src/plugins/chunks.ts
CHANGED
|
@@ -13,10 +13,16 @@
|
|
|
13
13
|
* - Per-route chunks (page/layout components, only when route-specific)
|
|
14
14
|
* - rolldown-runtime (Rolldown's own output, unavoidable)
|
|
15
15
|
*
|
|
16
|
+
* Additionally, this plugin builds a facade ancestry map during the RSC
|
|
17
|
+
* build so that `clientChunkGroup` can reclassify `shared:` client
|
|
18
|
+
* references that are only consumed by a single route boundary.
|
|
19
|
+
* See TIM-1107, design/27-chunking-strategy.md.
|
|
20
|
+
*
|
|
16
21
|
* Design docs: 27-chunking-strategy.md
|
|
17
22
|
*/
|
|
18
23
|
|
|
19
|
-
import type { Plugin } from 'vite';
|
|
24
|
+
import type { Plugin, Rollup } from 'vite';
|
|
25
|
+
import type { PluginContext } from '../plugin-context';
|
|
20
26
|
|
|
21
27
|
/**
|
|
22
28
|
* Check if a module is part of the timber framework runtime.
|
|
@@ -36,12 +42,71 @@ export function isTimberRuntime(id: string): boolean {
|
|
|
36
42
|
);
|
|
37
43
|
}
|
|
38
44
|
|
|
45
|
+
type OutputChunk = Rollup.OutputChunk;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build a map from absolute module IDs in non-facade RSC chunks to the
|
|
49
|
+
* absolute facade module paths that transitively import them.
|
|
50
|
+
*
|
|
51
|
+
* Used by `clientChunkGroup` to reclassify `shared:` client references
|
|
52
|
+
* that are only consumed by a single route boundary (TIM-1107).
|
|
53
|
+
*/
|
|
54
|
+
function buildFacadeAncestryMap(bundle: Rollup.OutputBundle): Map<string, string[]> {
|
|
55
|
+
const chunks = new Map<string, OutputChunk>();
|
|
56
|
+
for (const [file, asset] of Object.entries(bundle)) {
|
|
57
|
+
if (asset.type === 'chunk') chunks.set(file, asset);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Reverse import graph: which chunks import this chunk file?
|
|
61
|
+
const importedBy = new Map<string, string[]>();
|
|
62
|
+
for (const [file, chunk] of chunks) {
|
|
63
|
+
for (const imp of chunk.imports) {
|
|
64
|
+
let parents = importedBy.get(imp);
|
|
65
|
+
if (!parents) {
|
|
66
|
+
parents = [];
|
|
67
|
+
importedBy.set(imp, parents);
|
|
68
|
+
}
|
|
69
|
+
parents.push(file);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Trace upward from a chunk to find all facade modules that consume it
|
|
74
|
+
function facadeAncestors(file: string, visited: Set<string>): string[] {
|
|
75
|
+
if (visited.has(file)) return [];
|
|
76
|
+
visited.add(file);
|
|
77
|
+
const chunk = chunks.get(file);
|
|
78
|
+
if (!chunk) return [];
|
|
79
|
+
if (chunk.facadeModuleId) return [chunk.facadeModuleId];
|
|
80
|
+
const parents = importedBy.get(file) ?? [];
|
|
81
|
+
const result: string[] = [];
|
|
82
|
+
for (const p of parents) {
|
|
83
|
+
for (const f of facadeAncestors(p, visited)) result.push(f);
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const moduleToFacades = new Map<string, string[]>();
|
|
89
|
+
for (const [file, chunk] of chunks) {
|
|
90
|
+
if (chunk.facadeModuleId) continue;
|
|
91
|
+
const facades = facadeAncestors(file, new Set());
|
|
92
|
+
if (facades.length === 0) continue;
|
|
93
|
+
// Deduplicate facades
|
|
94
|
+
const unique = [...new Set(facades)];
|
|
95
|
+
for (const modId of chunk.moduleIds) {
|
|
96
|
+
moduleToFacades.set(modId, unique);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return moduleToFacades;
|
|
101
|
+
}
|
|
102
|
+
|
|
39
103
|
/**
|
|
40
104
|
* Create the timber-chunks Vite plugin.
|
|
41
105
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
106
|
+
* Rolldown's default splitting produces the desired output (one main
|
|
107
|
+
* bundle + per-route chunks). This plugin's main job is building the
|
|
108
|
+
* facade ancestry map during the RSC build so `clientChunkGroup` can
|
|
109
|
+
* reclassify shared client references to their correct route boundary.
|
|
45
110
|
*
|
|
46
111
|
* NOTE: Ideally we'd use Rolldown's `format: 'app'` (module registry format)
|
|
47
112
|
* which guarantees single-copy module semantics like webpack/Turbopack.
|
|
@@ -51,8 +116,12 @@ export function isTimberRuntime(id: string): boolean {
|
|
|
51
116
|
*
|
|
52
117
|
* Design docs: 27-chunking-strategy.md
|
|
53
118
|
*/
|
|
54
|
-
export function timberChunks(): Plugin {
|
|
119
|
+
export function timberChunks(ctx: PluginContext): Plugin {
|
|
55
120
|
return {
|
|
56
121
|
name: 'timber-chunks',
|
|
122
|
+
generateBundle(_options, bundle) {
|
|
123
|
+
if (this.environment?.name !== 'rsc') return;
|
|
124
|
+
ctx.sharedModuleFacades = buildFacadeAncestryMap(bundle);
|
|
125
|
+
},
|
|
57
126
|
};
|
|
58
127
|
}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client chunk grouping strategy for @vitejs/plugin-rsc.
|
|
3
3
|
*
|
|
4
|
-
* Groups client reference modules by
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* grouping is the middle ground.
|
|
4
|
+
* Groups client reference modules by the **consuming route's** layout boundary
|
|
5
|
+
* (derived from `meta.serverChunk`), not by the module's own file location.
|
|
6
|
+
* This ensures components in shared directories (e.g. `components/`) land in
|
|
7
|
+
* the chunk of the route that uses them, not in a single catch-all chunk.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
9
|
+
* For `shared:` modules (client references in non-facade RSC chunks), the
|
|
10
|
+
* function consults a facade ancestry map built during the RSC build. If all
|
|
11
|
+
* consuming facades belong to the same route boundary, the module is
|
|
12
|
+
* reclassified to that boundary instead of falling through to `client-shared`.
|
|
13
|
+
*
|
|
14
|
+
* See design/27-chunking-strategy.md, TIM-440, TIM-499, TIM-1104, TIM-1107.
|
|
11
15
|
*/
|
|
12
16
|
|
|
13
17
|
import { relative } from 'node:path';
|
|
@@ -44,37 +48,68 @@ function findAppBoundary(
|
|
|
44
48
|
return { inApp: true, segment: null };
|
|
45
49
|
}
|
|
46
50
|
|
|
51
|
+
/**
|
|
52
|
+
* For a `shared:` client reference, check the facade ancestry map to
|
|
53
|
+
* see if all consuming facades belong to the same route boundary.
|
|
54
|
+
* Returns the boundary segment name, or null if the module is
|
|
55
|
+
* genuinely shared across multiple boundaries.
|
|
56
|
+
*/
|
|
57
|
+
function reclassifySharedModule(
|
|
58
|
+
meta: { id: string; normalizedId: string },
|
|
59
|
+
appPrefix: string,
|
|
60
|
+
root: string,
|
|
61
|
+
sharedModuleFacades: Map<string, string[]>
|
|
62
|
+
): string | null {
|
|
63
|
+
const facades = sharedModuleFacades.get(meta.id);
|
|
64
|
+
if (!facades || facades.length === 0) return null;
|
|
65
|
+
|
|
66
|
+
const boundaries = new Set<string>();
|
|
67
|
+
for (const facade of facades) {
|
|
68
|
+
const relFacade = relative(root, facade).replace(/\\/g, '/');
|
|
69
|
+
const result = findAppBoundary(relFacade, appPrefix);
|
|
70
|
+
if (result.inApp && result.segment) {
|
|
71
|
+
boundaries.add(result.segment);
|
|
72
|
+
} else if (result.inApp) {
|
|
73
|
+
// Root-level facade — module is used at root, keep shared
|
|
74
|
+
return null;
|
|
75
|
+
} else {
|
|
76
|
+
// Facade outside app/ (e.g. src/posts/foo.mdx) — can't derive a
|
|
77
|
+
// safe common boundary, so keep shared to avoid pulling outside-app
|
|
78
|
+
// deps into a route chunk they don't belong to.
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (boundaries.size === 1) return [...boundaries][0];
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
47
87
|
/**
|
|
48
88
|
* Derive a chunk group name for a client reference module.
|
|
49
89
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
90
|
+
* Always derives the group from the **consuming route** (`serverChunk`'s
|
|
91
|
+
* facade path), not from the module's own file location. This prevents
|
|
92
|
+
* components in shared directories (e.g. `app/components/`, `src/lib/`)
|
|
93
|
+
* from collapsing into a single chunk when they're only used by one route.
|
|
94
|
+
*
|
|
95
|
+
* For `shared:` modules (non-facade RSC chunks), consults the facade
|
|
96
|
+
* ancestry map to reclassify modules that are only consumed by a single
|
|
97
|
+
* route boundary. This prevents large dependencies from polluting
|
|
98
|
+
* `client-shared` when they're route-scoped (TIM-1107).
|
|
99
|
+
*
|
|
100
|
+
* Examples:
|
|
53
101
|
* - `facade:app/dashboard/settings/page.tsx` → `"client-dashboard"`
|
|
54
102
|
* - `facade:app/(group-a)/group-page-a/page.tsx` → `"client-group-page-a"`
|
|
55
103
|
* - `facade:app/layout.tsx` (root layout) → `"client-shared"`
|
|
56
|
-
* - `shared:...` (shared across chunks) → `"client-shared"`
|
|
57
|
-
*
|
|
58
|
-
* When a 'use client' module lives outside the app directory (e.g. in
|
|
59
|
-
* src/posts/ or src/components/), the function falls back to deriving
|
|
60
|
-
* the layout boundary from the serverChunk's facade path. This prevents
|
|
61
|
-
* lazily-loaded client dependencies (e.g. via import.meta.glob) from
|
|
62
|
-
* being hoisted into the shared chunk when they're only used by a
|
|
63
|
-
* specific route.
|
|
64
|
-
*
|
|
65
|
-
* This balances route-scoped code splitting with HTTP request count:
|
|
66
|
-
* fewer chunks than per-serverChunk, but still avoids downloading every
|
|
67
|
-
* client component on every page.
|
|
104
|
+
* - `shared:...` (shared across chunks) → reclassified or `"client-shared"`
|
|
68
105
|
*/
|
|
69
106
|
export function clientChunkGroup(
|
|
70
107
|
meta: { id: string; normalizedId: string; serverChunk: string },
|
|
71
108
|
appDir: string,
|
|
72
|
-
root: string
|
|
109
|
+
root: string,
|
|
110
|
+
sharedModuleFacades?: Map<string, string[]>
|
|
73
111
|
): string {
|
|
74
|
-
const {
|
|
75
|
-
|
|
76
|
-
// Shared chunks (not associated with a single route entry) get one group.
|
|
77
|
-
if (serverChunk.startsWith('shared:')) return 'client-shared';
|
|
112
|
+
const { serverChunk } = meta;
|
|
78
113
|
|
|
79
114
|
// Compute the root-relative app directory prefix once. Both normalizedId
|
|
80
115
|
// and serverChunk paths from the RSC plugin are root-relative (via
|
|
@@ -82,32 +117,28 @@ export function clientChunkGroup(
|
|
|
82
117
|
// no ambiguity with content directories that happen to share a name.
|
|
83
118
|
const appPrefix = relative(root, appDir).replace(/\\/g, '/') + '/';
|
|
84
119
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
120
|
+
if (serverChunk.startsWith('shared:')) {
|
|
121
|
+
if (sharedModuleFacades) {
|
|
122
|
+
const boundary = reclassifySharedModule(meta, appPrefix, root, sharedModuleFacades);
|
|
123
|
+
if (boundary) return `client-${boundary}`;
|
|
124
|
+
}
|
|
125
|
+
return 'client-shared';
|
|
126
|
+
}
|
|
89
127
|
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
// layout boundary. This keeps dependencies of lazily-loaded 'use client'
|
|
94
|
-
// components (e.g. from import.meta.glob) in per-route async chunks
|
|
95
|
-
// instead of hoisting them into client-shared.
|
|
128
|
+
// Derive the group from the serverChunk's facade path — the RSC entry
|
|
129
|
+
// point that references this client module. This groups by the consuming
|
|
130
|
+
// route's layout boundary, not the module's own file location (TIM-1104).
|
|
96
131
|
if (serverChunk.startsWith('facade:')) {
|
|
97
132
|
const facadePath = serverChunk.slice('facade:'.length).replace(/\\/g, '/');
|
|
98
133
|
const facadeResult = findAppBoundary(facadePath, appPrefix);
|
|
99
134
|
if (facadeResult.inApp && facadeResult.segment) return `client-${facadeResult.segment}`;
|
|
100
135
|
if (facadeResult.inApp) return 'client-shared';
|
|
101
136
|
|
|
102
|
-
//
|
|
103
|
-
// the full facade path to keep these isolated
|
|
104
|
-
// Uses the full path (minus extension) to avoid collisions between
|
|
105
|
-
// facades with the same basename (e.g. foo/index.mdx vs bar/index.mdx).
|
|
137
|
+
// Facade is outside app/ (e.g. src/posts/foo.mdx via import.meta.glob).
|
|
138
|
+
// Derive a chunk name from the full facade path to keep these isolated.
|
|
106
139
|
const facadeName = facadePath.replace(/\.[^.]+$/, '').replace(/\//g, '-');
|
|
107
140
|
if (facadeName) return `client-${facadeName}`;
|
|
108
141
|
}
|
|
109
142
|
|
|
110
|
-
// Root-level files (layout.tsx, page.tsx directly in app/) go into the
|
|
111
|
-
// shared group since the root layout is loaded on every page.
|
|
112
143
|
return 'client-shared';
|
|
113
144
|
}
|