@timber-js/app 0.1.56 → 0.2.0-alpha.1

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.
@@ -1,108 +1,33 @@
1
1
  /**
2
- * timber-chunks — Vite sub-plugin for intelligent client chunk splitting.
2
+ * timber-chunks — Vite sub-plugin for client chunk configuration.
3
3
  *
4
- * Splits client bundles into cache tiers based on update frequency:
4
+ * Previously implemented a 5-tier cache strategy (vendor-react, vendor-timber,
5
+ * vendor-app, shared-app, shared-client) via manualChunks. This was removed
6
+ * in LOCAL-337 because the cache-tier separation added HTTP requests,
7
+ * complicated the architecture, and caused module duplication across chunks
8
+ * that required globalThis + Symbol.for workarounds.
5
9
  *
6
- * Tier 1: vendor-react — react, react-dom, scheduler (changes rarely)
7
- * Tier 2: vendor-timber — timber runtime, RSC runtime (changes per framework update)
8
- * Tier 3: vendor-app — user node_modules (changes on dependency updates)
9
- * Tier 4: shared-app — small shared app utilities/components (< 5KB source)
10
- * Tier 5: [route]-* — per-route page/layout chunks (default Rollup splitting)
11
- *
12
- * The shared-app tier prevents tiny utility modules (constants, helpers,
13
- * small UI components) from becoming individual chunks when shared across
14
- * routes. Without this, Rolldown creates per-module chunks for any code
15
- * shared between two or more entry points, producing many sub-1KB chunks.
16
- *
17
- * Server environments (RSC, SSR) are left to Vite's default chunking since
18
- * Cloudflare Workers load all code from a single deployment bundle with no
19
- * benefit from cache-tier separation.
10
+ * Current strategy: let Rolldown handle natural code splitting.
11
+ * Output is typically:
12
+ * - 1 main bundle (React + timber runtime + shared app code + user vendors)
13
+ * - Per-route chunks (page/layout components, only when route-specific)
14
+ * - rolldown-runtime (Rolldown's own output, unavoidable)
20
15
  *
21
16
  * Design docs: 27-chunking-strategy.md
22
17
  */
23
18
 
24
- import { statSync } from 'node:fs';
25
19
  import type { Plugin } from 'vite';
26
20
 
27
21
  /**
28
- * Source file size threshold for the shared-app chunk.
29
- * Modules under this size that aren't route files get merged into shared-app
30
- * instead of getting their own tiny chunks.
31
- */
32
- const SMALL_MODULE_THRESHOLD = 5 * 1024; // 5KB
33
-
34
- /**
35
- * Route convention file basenames (without extension).
36
- * These files define route segments and must stay in per-route chunks
37
- * to preserve route-based code splitting.
38
- */
39
- const ROUTE_FILE_BASENAMES = new Set([
40
- 'page',
41
- 'layout',
42
- 'loading',
43
- 'error',
44
- 'not-found',
45
- 'template',
46
- 'access',
47
- 'middleware',
48
- 'default',
49
- 'route',
50
- ]);
51
-
52
- /**
53
- * Cache for source file sizes to avoid repeated statSync calls.
54
- * Populated lazily during the build.
55
- */
56
- const sizeCache = new Map<string, number>();
57
-
58
- /**
59
- * Get the source file size, with caching.
60
- * Returns Infinity for virtual modules or files that can't be stat'd.
61
- */
62
- function getSourceSize(id: string): number {
63
- const cached = sizeCache.get(id);
64
- if (cached !== undefined) return cached;
65
-
66
- try {
67
- const size = statSync(id).size;
68
- sizeCache.set(id, size);
69
- return size;
70
- } catch {
71
- sizeCache.set(id, Infinity);
72
- return Infinity;
73
- }
74
- }
75
-
76
- /**
77
- * Extract the basename without extension from a module ID.
78
- * e.g. '/project/app/dashboard/page.tsx' → 'page'
79
- */
80
- function getBasename(id: string): string {
81
- const lastSlash = id.lastIndexOf('/');
82
- const filename = lastSlash >= 0 ? id.substring(lastSlash + 1) : id;
83
- const dotIndex = filename.indexOf('.');
84
- return dotIndex >= 0 ? filename.substring(0, dotIndex) : filename;
85
- }
86
-
87
- /**
88
- * Check if a module is a React ecosystem package (tier 1).
89
- */
90
- function isReactVendor(id: string): boolean {
91
- return (
92
- id.includes('node_modules/react-dom') ||
93
- id.includes('node_modules/react/') ||
94
- id.includes('node_modules/scheduler')
95
- );
96
- }
97
-
98
- /**
99
- * Check if a module is part of the timber framework runtime (tier 2).
22
+ * Check if a module is part of the timber framework runtime.
100
23
  *
101
24
  * Matches both monorepo paths (packages/timber-app/...) and consumer
102
- * project paths (node_modules/@timber-js/app/...) so the chunk
103
- * assignment is consistent regardless of how timber is installed.
25
+ * project paths (node_modules/@timber-js/app/...) so identification
26
+ * is consistent regardless of how timber is installed.
27
+ *
28
+ * Exported for use in tests (singleton-audit, etc.).
104
29
  */
105
- function isTimberRuntime(id: string): boolean {
30
+ export function isTimberRuntime(id: string): boolean {
106
31
  return (
107
32
  id.includes('/timber-app/') ||
108
33
  id.includes('/@timber-js/app/') ||
@@ -111,115 +36,15 @@ function isTimberRuntime(id: string): boolean {
111
36
  );
112
37
  }
113
38
 
114
- /**
115
- * Check if a module is a user-installed node_modules dependency (tier 3).
116
- * Excludes React ecosystem and timber runtime packages which have their own tiers.
117
- */
118
- function isUserVendor(id: string): boolean {
119
- return id.includes('node_modules/') && !isReactVendor(id) && !isTimberRuntime(id);
120
- }
121
-
122
- /**
123
- * Check if a module is a route convention file that should stay per-route.
124
- */
125
- function isRouteFile(id: string): boolean {
126
- return ROUTE_FILE_BASENAMES.has(getBasename(id));
127
- }
128
-
129
- /**
130
- * Categorize a module ID into a cache tier chunk name.
131
- *
132
- * Returns a chunk name for vendor modules and small shared app code,
133
- * or undefined to let Rollup's default splitting handle route code.
134
- */
135
- export function assignChunk(id: string): string | undefined {
136
- // Tier 1: React ecosystem — changes on version bumps only
137
- if (isReactVendor(id)) {
138
- return 'vendor-react';
139
- }
140
-
141
- // Tier 2: timber framework runtime — changes on framework updates
142
- if (isTimberRuntime(id)) {
143
- return 'vendor-timber';
144
- }
145
-
146
- // Tier 3: User vendor libraries — changes on dependency updates
147
- if (isUserVendor(id)) {
148
- return 'vendor-app';
149
- }
150
-
151
- // Tier 4: Small shared app modules — prevents tiny per-module chunks
152
- // Skip route files (page, layout, etc.) to preserve route-based splitting.
153
- // Skip virtual modules (contain \0 or don't start with /) as they have no
154
- // meaningful source size.
155
- if (!id.includes('\0') && id.startsWith('/') && !isRouteFile(id)) {
156
- const size = getSourceSize(id);
157
- if (size < SMALL_MODULE_THRESHOLD) {
158
- return 'shared-app';
159
- }
160
- }
161
-
162
- // Tier 5: Rollup's default splitting (per-route page/layout chunks, large shared modules)
163
- }
164
-
165
- /**
166
- * Group timber's internal 'use client' modules into the vendor-timber chunk.
167
- *
168
- * The RSC plugin creates separate entry points for each 'use client' module,
169
- * which manualChunks can't merge. This function is passed as the RSC plugin's
170
- * `clientChunks` callback to group timber internals into a single chunk.
171
- *
172
- * User client components that are small (< 5KB) are grouped into shared-client
173
- * to prevent thin facade wrappers from becoming individual chunks. This handles
174
- * the RSC client reference facade problem where each 'use client' module gets
175
- * a ~100-300 byte re-export wrapper chunk.
176
- */
177
- export function assignClientChunk(meta: {
178
- id: string;
179
- normalizedId: string;
180
- serverChunk: string;
181
- }): string | undefined {
182
- // Timber framework client modules → vendor-timber
183
- // Match both monorepo paths (/timber-app/) and consumer paths (/@timber-js/app/)
184
- if (meta.id.includes('/timber-app/') || meta.id.includes('/@timber-js/app/'))
185
- return 'vendor-timber';
186
-
187
- // Small user client components → shared-client (prevents facade micro-chunks)
188
- if (!meta.id.includes('\0') && meta.id.startsWith('/')) {
189
- const size = getSourceSize(meta.id);
190
- if (size < SMALL_MODULE_THRESHOLD) {
191
- return 'shared-client';
192
- }
193
- }
194
-
195
- // Large user/third-party client components → default per-route splitting
196
- }
197
-
198
39
  /**
199
40
  * Create the timber-chunks Vite plugin.
200
41
  *
201
- * Uses Vite's per-environment config to apply manualChunks only to
202
- * the client build. The config hook runs before environments are
203
- * created, so we use `environments.client` to target the client.
42
+ * Currently a no-op — Rolldown's default splitting produces the desired
43
+ * output (one main bundle + per-route chunks). The plugin is retained as
44
+ * a hook point for future chunking adjustments if needed.
204
45
  */
205
46
  export function timberChunks(): Plugin {
206
47
  return {
207
48
  name: 'timber-chunks',
208
-
209
- config() {
210
- return {
211
- environments: {
212
- client: {
213
- build: {
214
- rollupOptions: {
215
- output: {
216
- manualChunks: assignChunk,
217
- },
218
- },
219
- },
220
- },
221
- },
222
- };
223
- },
224
49
  };
225
50
  }