@timber-js/app 0.2.0-alpha.166 → 0.2.0-alpha.168
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-CDPfMp_I.js → actions-O_LsyCE4.js} +3 -3
- package/dist/_chunks/{actions-CDPfMp_I.js.map → actions-O_LsyCE4.js.map} +1 -1
- package/dist/_chunks/{cache-api-DygSeKCB.js → cache-api-B-lhk9p4.js} +107 -58
- package/dist/_chunks/cache-api-B-lhk9p4.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync-EXGYPhI2.js → cli-schema-sync-B73L6pMq.js} +5 -3
- package/dist/_chunks/cli-schema-sync-B73L6pMq.js.map +1 -0
- package/dist/_chunks/cloudflare-AHoWYTYr.js +1188 -0
- package/dist/_chunks/cloudflare-AHoWYTYr.js.map +1 -0
- package/dist/_chunks/json-lossy-check-ClNvBM_3.js +63 -0
- package/dist/_chunks/json-lossy-check-ClNvBM_3.js.map +1 -0
- package/dist/_chunks/{logger-t3uxAmbX.js → logger-AWfuX-KJ.js} +2 -19
- package/dist/_chunks/logger-AWfuX-KJ.js.map +1 -0
- package/dist/_chunks/{walkers-DBVzXuWc.js → walkers-CoOC8Hga.js} +2 -2
- package/dist/_chunks/{walkers-DBVzXuWc.js.map → walkers-CoOC8Hga.js.map} +1 -1
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +10 -3
- package/dist/adapters/cloudflare-kv-cache.js.map +1 -1
- package/dist/adapters/cloudflare.d.ts +12 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +2 -461
- package/dist/adapters/types.d.ts +2 -0
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/cache/cache-api.d.ts +33 -11
- package/dist/cache/cache-api.d.ts.map +1 -1
- package/dist/cache/index.d.ts +1 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +1 -1
- package/dist/cache/json-lossy-check.d.ts +11 -0
- package/dist/cache/json-lossy-check.d.ts.map +1 -0
- package/dist/cache/redis-handler.d.ts +42 -0
- package/dist/cache/redis-handler.d.ts.map +1 -1
- package/dist/cache/singleflight.d.ts.map +1 -1
- package/dist/cache/stores/cloudflare-kv.d.ts +1 -1
- package/dist/cache/stores/cloudflare-kv.d.ts.map +1 -1
- package/dist/cache/stores/memory.d.ts +1 -1
- package/dist/cache/stores/memory.d.ts.map +1 -1
- package/dist/cache/stores/redis.d.ts +1 -1
- package/dist/cache/stores/redis.d.ts.map +1 -1
- package/dist/cache/stores/vercel.d.ts +1 -1
- package/dist/cache/stores/vercel.d.ts.map +1 -1
- package/dist/cache/tag-aware-handler.d.ts.map +1 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/cdn/cloudflare-purge.d.ts.map +1 -1
- package/dist/cdn/fastly-purge.d.ts.map +1 -1
- package/dist/cdn/workers-cache-purge.d.ts.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +3 -2
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts +1 -0
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/child-segment-context.d.ts +2 -2
- package/dist/client/child-segment-context.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/history.d.ts +10 -0
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/internal.js +141 -116
- package/dist/client/internal.js.map +1 -1
- package/dist/client/router.d.ts +6 -0
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/segment-update-context.d.ts +3 -3
- package/dist/client/segment-update-context.d.ts.map +1 -1
- package/dist/client/slot-outlet.d.ts +1 -1
- package/dist/codec.d.ts.map +1 -1
- package/dist/codec.js +2 -1
- package/dist/codec.js.map +1 -1
- package/dist/config-types.d.ts +16 -37
- package/dist/config-types.d.ts.map +1 -1
- package/dist/config-validation.d.ts.map +1 -1
- package/dist/dev-tools/instrumentation.d.ts.map +1 -1
- package/dist/fonts/pipeline.d.ts +19 -0
- package/dist/fonts/pipeline.d.ts.map +1 -1
- package/dist/fonts/transform.d.ts.map +1 -1
- package/dist/fonts/virtual-modules.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +378 -109
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/adapter-build.d.ts +1 -0
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/cache.d.ts +8 -8
- package/dist/plugins/cache.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts +27 -3
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/server-bundle.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/server/action-client.d.ts +1 -1
- package/dist/server/action-client.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +7 -0
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/body-limits.d.ts.map +1 -1
- package/dist/server/deny-boundary.d.ts +3 -1
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/form-data.d.ts.map +1 -1
- package/dist/server/html-injector-core.d.ts.map +1 -1
- package/dist/server/index.js +20 -3
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.js +28 -41
- package/dist/server/internal.js.map +1 -1
- package/dist/server/middleware-runner.d.ts +0 -17
- package/dist/server/middleware-runner.d.ts.map +1 -1
- package/dist/server/param-coercion.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts +6 -3
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/pipeline.d.ts +18 -0
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/prebuilt/capture-state.d.ts.map +1 -1
- package/dist/server/prebuilt/synthetic-store.d.ts.map +1 -1
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/render-timeout.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts +9 -0
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-middleware-runner.d.ts +14 -14
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts +1 -0
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +8 -0
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts +44 -74
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts.map +1 -1
- package/dist/server/safe-load.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/shared/redirect-type.d.ts +2 -2
- package/dist/shared/redirect-type.d.ts.map +1 -1
- package/dist/shims/font-google.d.ts.map +1 -1
- package/dist/shims/image.d.ts +120 -120
- package/dist/shims/image.d.ts.map +1 -1
- package/docs/api/32-api-cache.mdx +116 -4
- package/docs/api/34-api-config.mdx +0 -26
- package/docs/learn/09-caching.mdx +126 -10
- package/docs/learn/12-client-navigation.mdx +9 -1
- package/docs/learn/13-configuration.mdx +6 -8
- package/docs/learn/14-deploying.mdx +18 -18
- package/docs/more/03-coming-from-nextjs.mdx +2 -2
- package/docs/more/04-metadata-and-fonts.mdx +1 -1
- package/docs/more/50-ai-agent-instructions.mdx +5 -5
- package/package.json +6 -5
- package/src/adapters/cloudflare-kv-cache.ts +27 -6
- package/src/adapters/cloudflare.ts +63 -25
- package/src/adapters/types.ts +2 -0
- package/src/cache/cache-api.ts +84 -84
- package/src/cache/index.ts +1 -1
- package/src/cache/json-lossy-check.ts +75 -0
- package/src/cache/redis-handler.ts +65 -14
- package/src/cache/timber-cache.ts +10 -2
- package/src/client/browser-entry/index.ts +2 -0
- package/src/client/browser-entry/post-hydration.ts +16 -9
- package/src/client/browser-entry/router-init.ts +2 -0
- package/src/client/history.ts +11 -0
- package/src/client/router.ts +224 -208
- package/src/codec.ts +3 -1
- package/src/config-types.ts +16 -37
- package/src/config-validation.ts +7 -5
- package/src/fonts/pipeline.ts +32 -0
- package/src/fonts/transform.ts +30 -24
- package/src/plugins/adapter-build.ts +132 -13
- package/src/plugins/cache.ts +45 -30
- package/src/plugins/entries.ts +40 -43
- package/src/plugins/fonts.ts +30 -0
- package/src/plugins/server-bundle.ts +7 -15
- package/src/routing/scanner.ts +7 -0
- package/src/server/action-client.ts +1 -1
- package/src/server/als-registry.ts +7 -0
- package/src/server/deny-boundary.ts +6 -3
- package/src/server/middleware-runner.ts +0 -44
- package/src/server/param-coercion.ts +1 -0
- package/src/server/pipeline-phases.ts +11 -13
- package/src/server/pipeline.ts +51 -3
- package/src/server/route-element-builder.ts +59 -9
- package/src/server/rsc-entry/action-middleware-runner.ts +14 -14
- package/src/server/rsc-entry/index.ts +30 -40
- package/src/server/rsc-entry/render-route.ts +6 -2
- package/src/server/rsc-entry/rsc-payload.ts +21 -4
- package/src/server/rsc-entry/rsc-stream.ts +8 -0
- package/src/server/rsc-entry/wrap-action-dispatch.ts +109 -372
- package/dist/_chunks/cache-api-DygSeKCB.js.map +0 -1
- package/dist/_chunks/cli-schema-sync-EXGYPhI2.js.map +0 -1
- package/dist/_chunks/logger-t3uxAmbX.js.map +0 -1
- package/dist/_chunks/tree-match-D2l830j2.js +0 -102
- package/dist/_chunks/tree-match-D2l830j2.js.map +0 -1
- package/dist/adapters/cloudflare.js.map +0 -1
package/src/config-validation.ts
CHANGED
|
@@ -30,12 +30,9 @@ const KNOWN_CONFIG_KEYS = {
|
|
|
30
30
|
debug: true,
|
|
31
31
|
clientJavascript: true,
|
|
32
32
|
adapter: true,
|
|
33
|
-
cacheHandler: true,
|
|
34
|
-
cdnPurge: true,
|
|
35
33
|
allowedOrigins: true,
|
|
36
34
|
csrf: true,
|
|
37
35
|
limits: true,
|
|
38
|
-
actions: true,
|
|
39
36
|
forms: true,
|
|
40
37
|
pageExtensions: true,
|
|
41
38
|
slowRequestMs: true,
|
|
@@ -49,6 +46,7 @@ const KNOWN_CONFIG_KEYS = {
|
|
|
49
46
|
reactCompiler: true,
|
|
50
47
|
sitemap: true,
|
|
51
48
|
buildDir: true,
|
|
49
|
+
clientSegmentCache: true,
|
|
52
50
|
topLoader: true,
|
|
53
51
|
budget: true,
|
|
54
52
|
} satisfies Record<keyof TimberUserConfig, true>;
|
|
@@ -197,12 +195,16 @@ export function validateConfig(config: TimberUserConfig): ConfigError[] {
|
|
|
197
195
|
}
|
|
198
196
|
}
|
|
199
197
|
|
|
198
|
+
const MOVED_TO_CACHE_FILE = new Set(['cacheHandler', 'cdnPurge']);
|
|
199
|
+
|
|
200
200
|
for (const key of Object.keys(config)) {
|
|
201
201
|
if (!knownKeys.has(key)) {
|
|
202
202
|
errors.push({
|
|
203
203
|
field: key,
|
|
204
204
|
message: `Unknown config option: "${key}".`,
|
|
205
|
-
suggestion:
|
|
205
|
+
suggestion: MOVED_TO_CACHE_FILE.has(key)
|
|
206
|
+
? `"${key}" has moved to timber.cache.ts. Export cacheHandler as default, cdnPurge as a named export.`
|
|
207
|
+
: `Check for typos. Known options: ${[...knownKeys].sort().join(', ')}.`,
|
|
206
208
|
});
|
|
207
209
|
}
|
|
208
210
|
}
|
|
@@ -248,7 +250,7 @@ const VIRTUAL_MODULE_NAMES: Record<string, string> = {
|
|
|
248
250
|
'virtual:timber-config': 'Runtime config (timber.config.ts)',
|
|
249
251
|
'virtual:timber-route-manifest': 'Route manifest (app/ file tree)',
|
|
250
252
|
'virtual:timber-instrumentation': 'Instrumentation (instrumentation.ts)',
|
|
251
|
-
'virtual:timber-cache-handler': 'Cache handler (
|
|
253
|
+
'virtual:timber-cache-handler': 'Cache handler (timber.cache.ts)',
|
|
252
254
|
'virtual:timber-build-manifest': 'Build manifest (asset mapping)',
|
|
253
255
|
};
|
|
254
256
|
|
package/src/fonts/pipeline.ts
CHANGED
|
@@ -208,6 +208,38 @@ export class FontPipeline {
|
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
/** True if at least one entry is registered under `importer`. */
|
|
212
|
+
hasImporter(importer: string): boolean {
|
|
213
|
+
for (const entry of this._entries.values()) {
|
|
214
|
+
if (entry.importer === importer) return true;
|
|
215
|
+
}
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Drop **every** registry entry that originated from `importer`,
|
|
221
|
+
* regardless of family.
|
|
222
|
+
*
|
|
223
|
+
* Called at the start of the transform hook before re-registering the
|
|
224
|
+
* current extraction set. This handles the cases `pruneFor` misses:
|
|
225
|
+
*
|
|
226
|
+
* - **Family rename** (Inter → Roboto): the old family's entries are
|
|
227
|
+
* cleared because we prune by importer, not by (importer, family).
|
|
228
|
+
* - **Font call deleted**: the old entries are cleared even though no
|
|
229
|
+
* new call provides a family to prune against.
|
|
230
|
+
*
|
|
231
|
+
* Other importers' fonts are untouched.
|
|
232
|
+
*
|
|
233
|
+
* See TIM-1087.
|
|
234
|
+
*/
|
|
235
|
+
pruneImporter(importer: string): void {
|
|
236
|
+
for (const [id, entry] of this._entries) {
|
|
237
|
+
if (entry.importer === importer) {
|
|
238
|
+
this._entries.delete(id);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
211
243
|
/** Drop every registered font. Used by tests and rebuild flows. */
|
|
212
244
|
clear(): void {
|
|
213
245
|
this._entries.clear();
|
package/src/fonts/transform.ts
CHANGED
|
@@ -213,7 +213,11 @@ export function runFontsTransform(
|
|
|
213
213
|
|
|
214
214
|
const s = new MagicString(code);
|
|
215
215
|
|
|
216
|
-
//
|
|
216
|
+
// Extract and validate all call sites before mutating the pipeline.
|
|
217
|
+
// If any extraction fails (emitError throws), the pipeline is untouched
|
|
218
|
+
// so the previous valid CSS stays intact (TIM-1087).
|
|
219
|
+
const pending: { extracted: ExtractedFont; site: (typeof analysis.callSites)[0] }[] = [];
|
|
220
|
+
|
|
217
221
|
for (const site of analysis.callSites) {
|
|
218
222
|
if (site.binding.provider === 'google') {
|
|
219
223
|
const family = site.binding.importedName.replace(/_/g, ' ');
|
|
@@ -232,24 +236,22 @@ export function runFontsTransform(
|
|
|
232
236
|
const fontStack = buildFontStack(family);
|
|
233
237
|
const display = config!.display ?? 'swap';
|
|
234
238
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
s.overwrite(site.replaceStart, site.replaceEnd, buildFontResultLiteral(extracted));
|
|
239
|
+
pending.push({
|
|
240
|
+
site,
|
|
241
|
+
extracted: {
|
|
242
|
+
id: fontId,
|
|
243
|
+
family,
|
|
244
|
+
provider: 'google',
|
|
245
|
+
weights: normalizeWeightArray(config!.weight),
|
|
246
|
+
styles: normalizeStyleArray(config!.style),
|
|
247
|
+
subsets: config!.subsets ?? ['latin'],
|
|
248
|
+
display,
|
|
249
|
+
variable: config!.variable,
|
|
250
|
+
className,
|
|
251
|
+
fontFamily: fontStack,
|
|
252
|
+
importer: id,
|
|
253
|
+
},
|
|
254
|
+
});
|
|
253
255
|
} else {
|
|
254
256
|
// local font
|
|
255
257
|
const config = extractLocalFontConfigFromNode(site.configNode);
|
|
@@ -261,14 +263,18 @@ export function runFontsTransform(
|
|
|
261
263
|
);
|
|
262
264
|
}
|
|
263
265
|
|
|
264
|
-
|
|
265
|
-
pipeline.pruneFor(id, extracted.family);
|
|
266
|
-
pipeline.register(extracted);
|
|
267
|
-
|
|
268
|
-
s.overwrite(site.replaceStart, site.replaceEnd, buildFontResultLiteral(extracted));
|
|
266
|
+
pending.push({ site, extracted: processLocalFont(config!, id) });
|
|
269
267
|
}
|
|
270
268
|
}
|
|
271
269
|
|
|
270
|
+
// All extractions succeeded — now prune stale entries and register.
|
|
271
|
+
// Handles family renames and deleted font calls (TIM-1087).
|
|
272
|
+
pipeline.pruneImporter(id);
|
|
273
|
+
for (const { extracted, site } of pending) {
|
|
274
|
+
pipeline.register(extracted);
|
|
275
|
+
s.overwrite(site.replaceStart, site.replaceEnd, buildFontResultLiteral(extracted));
|
|
276
|
+
}
|
|
277
|
+
|
|
272
278
|
// Remove font import declarations. Use a Set to avoid double-removing
|
|
273
279
|
// when multiple bindings come from the same ImportDeclaration.
|
|
274
280
|
const removedRanges = new Set<string>();
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import type { Plugin } from 'vite';
|
|
17
17
|
import { join } from 'node:path';
|
|
18
|
+
import { pathToFileURL } from 'node:url';
|
|
18
19
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
19
20
|
import type { PluginContext } from '../plugin-context.js';
|
|
20
21
|
import type { TimberPlatformAdapter, TimberConfig } from '../adapters/types.js';
|
|
@@ -83,6 +84,7 @@ export function timberAdapterBuild(ctx: PluginContext): Plugin {
|
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
const adapterConfig: TimberConfig = {
|
|
87
|
+
root: ctx.root,
|
|
86
88
|
output: ctx.config.output ?? 'server',
|
|
87
89
|
clientJavascriptDisabled: ctx.clientJavascript.disabled,
|
|
88
90
|
manifestInit,
|
|
@@ -94,6 +96,116 @@ export function timberAdapterBuild(ctx: PluginContext): Plugin {
|
|
|
94
96
|
};
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Parse the RSC assets manifest at `manifestPath`. Tries JSON.parse first
|
|
101
|
+
* (fast path for plain-string manifests); falls back to a dynamic import()
|
|
102
|
+
* for manifests that contain runtime expressions from renderBuiltUrl.
|
|
103
|
+
* Returns null if both paths fail.
|
|
104
|
+
*/
|
|
105
|
+
async function parseRscManifest(
|
|
106
|
+
manifestPath: string,
|
|
107
|
+
content: string
|
|
108
|
+
): Promise<Record<string, unknown> | null> {
|
|
109
|
+
const jsonStr = content.replace(/^export default\s*/, '').replace(/;?\s*$/, '');
|
|
110
|
+
try {
|
|
111
|
+
return JSON.parse(jsonStr);
|
|
112
|
+
} catch {
|
|
113
|
+
// Not strict JSON — manifest likely contains renderBuiltUrl runtime
|
|
114
|
+
// expressions. Fall back to evaluating the module directly.
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// When renderBuiltUrl's runtime expressions reference external helpers
|
|
118
|
+
// not defined in the manifest file, this import will fail. That's an
|
|
119
|
+
// inherent limitation: runtime-computed URLs can't be resolved at build
|
|
120
|
+
// time for <link> tag injection. The warning below provides observability.
|
|
121
|
+
try {
|
|
122
|
+
const url = pathToFileURL(manifestPath).href;
|
|
123
|
+
const mod = await import(`${url}?t=${Date.now()}`);
|
|
124
|
+
return mod.default ?? null;
|
|
125
|
+
} catch (err) {
|
|
126
|
+
swallow(err, `failed to evaluate RSC assets manifest: ${manifestPath}`, { level: 'warn' });
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Replace all `"js": [...]` array contents with `"js": []` using balanced
|
|
133
|
+
* bracket matching. Handles nested brackets from runtime expressions like
|
|
134
|
+
* `__rbu("path/to/[slug]/file.js")`.
|
|
135
|
+
*/
|
|
136
|
+
function clearJsArrays(src: string): string {
|
|
137
|
+
const pattern = /"js"\s*:\s*\[/g;
|
|
138
|
+
let result = '';
|
|
139
|
+
let lastEnd = 0;
|
|
140
|
+
let match: RegExpExecArray | null;
|
|
141
|
+
|
|
142
|
+
while ((match = pattern.exec(src)) !== null) {
|
|
143
|
+
const arrayStart = match.index + match[0].length;
|
|
144
|
+
let depth = 1;
|
|
145
|
+
let i = arrayStart;
|
|
146
|
+
while (i < src.length && depth > 0) {
|
|
147
|
+
if (src[i] === '[') depth++;
|
|
148
|
+
else if (src[i] === ']') depth--;
|
|
149
|
+
i++;
|
|
150
|
+
}
|
|
151
|
+
result += src.slice(lastEnd, match.index) + '"js": []';
|
|
152
|
+
lastEnd = i;
|
|
153
|
+
}
|
|
154
|
+
result += src.slice(lastEnd);
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Replace `"bootstrapScriptContent": <value>` with an empty string. The
|
|
160
|
+
* value may be a JS expression (e.g., concatenation with runtime helpers),
|
|
161
|
+
* not a simple string literal. Scans forward from the colon, tracking
|
|
162
|
+
* balanced parens and brackets, and stops at the next unbalanced `,` or `}`.
|
|
163
|
+
*/
|
|
164
|
+
function clearBootstrapScriptContent(src: string): string {
|
|
165
|
+
const key = '"bootstrapScriptContent"';
|
|
166
|
+
const idx = src.indexOf(key);
|
|
167
|
+
if (idx === -1) return src;
|
|
168
|
+
|
|
169
|
+
// Find the colon after the key
|
|
170
|
+
const colonIdx = src.indexOf(':', idx + key.length);
|
|
171
|
+
if (colonIdx === -1) return src;
|
|
172
|
+
|
|
173
|
+
// Skip whitespace after colon
|
|
174
|
+
let valStart = colonIdx + 1;
|
|
175
|
+
while (valStart < src.length && /\s/.test(src[valStart])) valStart++;
|
|
176
|
+
|
|
177
|
+
// Scan forward to find the end of the value expression
|
|
178
|
+
let i = valStart;
|
|
179
|
+
let depth = 0;
|
|
180
|
+
let inString = false;
|
|
181
|
+
let stringChar = '';
|
|
182
|
+
|
|
183
|
+
while (i < src.length) {
|
|
184
|
+
const ch = src[i];
|
|
185
|
+
if (inString) {
|
|
186
|
+
if (ch === '\\') {
|
|
187
|
+
i += 2;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (ch === stringChar) inString = false;
|
|
191
|
+
} else {
|
|
192
|
+
if (ch === '"' || ch === "'") {
|
|
193
|
+
inString = true;
|
|
194
|
+
stringChar = ch;
|
|
195
|
+
} else if (ch === '(' || ch === '[') {
|
|
196
|
+
depth++;
|
|
197
|
+
} else if (ch === ')' || ch === ']') {
|
|
198
|
+
depth--;
|
|
199
|
+
} else if (depth === 0 && (ch === ',' || ch === '}' || ch === '\n')) {
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
i++;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return src.slice(0, idx) + '"bootstrapScriptContent": ""' + src.slice(i);
|
|
207
|
+
}
|
|
208
|
+
|
|
97
209
|
/**
|
|
98
210
|
* Strip JS references from the RSC plugin's assets manifest files.
|
|
99
211
|
*
|
|
@@ -125,13 +237,8 @@ export async function mergeRscAssetsManifest(
|
|
|
125
237
|
return;
|
|
126
238
|
}
|
|
127
239
|
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
try {
|
|
131
|
-
manifest = JSON.parse(jsonStr);
|
|
132
|
-
} catch {
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
240
|
+
const manifest = await parseRscManifest(manifestPath, content);
|
|
241
|
+
if (!manifest) return;
|
|
135
242
|
|
|
136
243
|
// Merge serverResources CSS into buildManifest.css.
|
|
137
244
|
// Keys in serverResources are root-relative (e.g., "app/layout.tsx" or
|
|
@@ -174,7 +281,7 @@ export async function mergeRscAssetsManifest(
|
|
|
174
281
|
}
|
|
175
282
|
}
|
|
176
283
|
|
|
177
|
-
async function stripJsFromRscAssetsManifests(buildDir: string): Promise<void> {
|
|
284
|
+
export async function stripJsFromRscAssetsManifests(buildDir: string): Promise<void> {
|
|
178
285
|
const manifestName = '__vite_rsc_assets_manifest.js';
|
|
179
286
|
const paths = [join(buildDir, 'rsc', manifestName), join(buildDir, 'ssr', manifestName)];
|
|
180
287
|
|
|
@@ -186,17 +293,29 @@ async function stripJsFromRscAssetsManifests(buildDir: string): Promise<void> {
|
|
|
186
293
|
continue;
|
|
187
294
|
}
|
|
188
295
|
|
|
296
|
+
// Try JSON.parse to check if we can safely re-serialize
|
|
189
297
|
const jsonStr = content.replace(/^export default\s*/, '').replace(/;?\s*$/, '');
|
|
190
|
-
let
|
|
298
|
+
let isStrictJson = true;
|
|
299
|
+
let manifest: Record<string, unknown> | null = null;
|
|
191
300
|
try {
|
|
192
301
|
manifest = JSON.parse(jsonStr);
|
|
193
|
-
} catch
|
|
194
|
-
|
|
302
|
+
} catch {
|
|
303
|
+
isStrictJson = false;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!isStrictJson) {
|
|
307
|
+
// Manifest contains renderBuiltUrl runtime expressions — cannot
|
|
308
|
+
// safely re-serialize with JSON.stringify. Use targeted text
|
|
309
|
+
// transforms to clear JS arrays and bootstrapScriptContent.
|
|
310
|
+
let rewritten = content;
|
|
311
|
+
rewritten = clearJsArrays(rewritten);
|
|
312
|
+
rewritten = clearBootstrapScriptContent(rewritten);
|
|
313
|
+
await writeFile(path, rewritten);
|
|
195
314
|
continue;
|
|
196
315
|
}
|
|
197
316
|
|
|
198
317
|
// Clear JS from clientReferenceDeps — preserves CSS
|
|
199
|
-
const deps = manifest
|
|
318
|
+
const deps = manifest?.clientReferenceDeps as
|
|
200
319
|
| Record<string, { js: string[]; css: string[] }>
|
|
201
320
|
| undefined;
|
|
202
321
|
if (deps) {
|
|
@@ -205,7 +324,7 @@ async function stripJsFromRscAssetsManifests(buildDir: string): Promise<void> {
|
|
|
205
324
|
}
|
|
206
325
|
}
|
|
207
326
|
|
|
208
|
-
manifest.bootstrapScriptContent = '';
|
|
327
|
+
if (manifest) manifest.bootstrapScriptContent = '';
|
|
209
328
|
|
|
210
329
|
await writeFile(path, `export default ${JSON.stringify(manifest, null, 2)};\n`);
|
|
211
330
|
}
|
package/src/plugins/cache.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* timber-cache-transform — Vite plugin that injects stable callsite IDs
|
|
3
|
-
* into `cache()` calls imported from `@timber-js/app/cache`.
|
|
3
|
+
* into `cache.data()` calls imported from `@timber-js/app/cache`.
|
|
4
4
|
*
|
|
5
5
|
* Detected callsite forms (TIM-1054):
|
|
6
|
-
* - named imports, including aliases: `import { cache as c }` + `c(fn, opts)`
|
|
7
|
-
* - namespace imports: `import * as ns` + `ns.cache(fn, opts)`
|
|
6
|
+
* - named imports, including aliases: `import { cache as c }` + `c.data(fn, opts)`
|
|
7
|
+
* - namespace imports: `import * as ns` + `ns.cache.data(fn, opts)`
|
|
8
8
|
* Forms a per-module transform cannot trace — local variable aliases
|
|
9
9
|
* (`const c = cache`), user re-export wrapper modules, computed access
|
|
10
|
-
* (`
|
|
10
|
+
* (`cache['data']`) — fall back to the runtime fnId derived from
|
|
11
11
|
* fnv1a(fn.toString()) (see cache/timber-cache.ts), which is deterministic
|
|
12
12
|
* across instances and cold starts of the same build. Without either
|
|
13
13
|
* mechanism, an ordinal counter would collide across instances with a shared
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
* data to another.
|
|
16
16
|
*
|
|
17
17
|
* The plugin transforms:
|
|
18
|
-
* cache(fn, opts)
|
|
19
|
-
* ns.cache(fn, opts)
|
|
18
|
+
* cache.data(fn, opts) → cache.data(fn, opts, "stable-id")
|
|
19
|
+
* ns.cache.data(fn, opts) → ns.cache.data(fn, opts, "stable-id")
|
|
20
20
|
*
|
|
21
21
|
* where stable-id =
|
|
22
22
|
* fnv1a(relPath + ":cache:" + fnv1a(stmtText) + ":" + fnv1a(callSpanText) + ":" + occurrence)
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
* unrelated edits, while a changed call gets a fresh ID — which is exactly
|
|
35
35
|
* when invalidation is wanted.
|
|
36
36
|
*
|
|
37
|
-
* The enclosing-statement text is included so that byte-identical cache()
|
|
37
|
+
* The enclosing-statement text is included so that byte-identical cache.data()
|
|
38
38
|
* calls in different scopes — e.g. two factory helpers that each `return
|
|
39
|
-
* cache(fn, { ttl: 60 })` — get distinct, insertion-stable IDs: their
|
|
39
|
+
* cache.data(fn, { ttl: 60 })` — get distinct, insertion-stable IDs: their
|
|
40
40
|
* enclosing declarations differ by name/body even when the call spans are
|
|
41
41
|
* identical. Without it, a source-order occurrence counter alone would shift
|
|
42
42
|
* when an identical span is inserted above, reassigning IDs across deploys.
|
|
@@ -75,41 +75,56 @@ import type {
|
|
|
75
75
|
} from './callsite-ast.js';
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
|
-
* Is this callee a
|
|
79
|
-
*
|
|
80
|
-
*
|
|
78
|
+
* Is this callee a `cache.data(...)` call? Matches two forms:
|
|
79
|
+
* - Named import: `cache.data(fn, opts)` where `cache` is the imported binding
|
|
80
|
+
* - Namespace import: `ns.cache.data(fn, opts)` where `ns` is the namespace
|
|
81
|
+
* Computed access (`cache['data']`) is deliberately not matched; it falls back
|
|
81
82
|
* to the runtime fnId derivation.
|
|
82
83
|
*/
|
|
83
|
-
function
|
|
84
|
-
if (callee.type
|
|
85
|
-
|
|
84
|
+
function isCacheDataCallee(callee: PositionedNode, bindings: ImportBindings): boolean {
|
|
85
|
+
if (callee.type !== 'MemberExpression') return false;
|
|
86
|
+
const member = callee as unknown as MemberExpressionNode;
|
|
87
|
+
if (member.computed) return false;
|
|
88
|
+
if (member.property.type !== 'Identifier' || (member.property as IdentifierNode).name !== 'data')
|
|
89
|
+
return false;
|
|
90
|
+
|
|
91
|
+
const obj = member.object;
|
|
92
|
+
// Named import: `cache.data(...)` — obj is the imported `cache` identifier
|
|
93
|
+
if (obj.type === 'Identifier') {
|
|
94
|
+
return bindings.named.has((obj as IdentifierNode).name);
|
|
86
95
|
}
|
|
87
|
-
|
|
88
|
-
|
|
96
|
+
// Namespace import: `ns.cache.data(...)` — obj is `ns.cache`
|
|
97
|
+
if (obj.type === 'MemberExpression') {
|
|
98
|
+
const outer = obj as unknown as MemberExpressionNode;
|
|
89
99
|
return (
|
|
90
|
-
!
|
|
91
|
-
|
|
92
|
-
bindings.namespaces.has((
|
|
93
|
-
|
|
94
|
-
(
|
|
100
|
+
!outer.computed &&
|
|
101
|
+
outer.object.type === 'Identifier' &&
|
|
102
|
+
bindings.namespaces.has((outer.object as IdentifierNode).name) &&
|
|
103
|
+
outer.property.type === 'Identifier' &&
|
|
104
|
+
(outer.property as IdentifierNode).name === 'cache'
|
|
95
105
|
);
|
|
96
106
|
}
|
|
97
107
|
return false;
|
|
98
108
|
}
|
|
99
109
|
|
|
100
110
|
/**
|
|
101
|
-
* Extract the local binding name that a cache callee resolves to — the
|
|
102
|
-
* identifier for named imports, or the namespace object for `ns.cache()`.
|
|
111
|
+
* Extract the local binding name that a cache.data callee resolves to — the
|
|
112
|
+
* `cache` identifier for named imports, or the namespace object for `ns.cache.data()`.
|
|
103
113
|
*/
|
|
104
114
|
function calleeBindingName(callee: PositionedNode, bindings: ImportBindings): string | null {
|
|
105
|
-
if (callee.type
|
|
106
|
-
|
|
115
|
+
if (callee.type !== 'MemberExpression') return null;
|
|
116
|
+
const member = callee as unknown as MemberExpressionNode;
|
|
117
|
+
const obj = member.object;
|
|
118
|
+
// Named import: `cache.data(...)` — binding is `cache`
|
|
119
|
+
if (obj.type === 'Identifier') {
|
|
120
|
+
const name = (obj as IdentifierNode).name;
|
|
107
121
|
return bindings.named.has(name) ? name : null;
|
|
108
122
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
123
|
+
// Namespace import: `ns.cache.data(...)` — binding is `ns`
|
|
124
|
+
if (obj.type === 'MemberExpression') {
|
|
125
|
+
const outer = obj as unknown as MemberExpressionNode;
|
|
126
|
+
if (outer.object.type === 'Identifier') {
|
|
127
|
+
const name = (outer.object as IdentifierNode).name;
|
|
113
128
|
return bindings.namespaces.has(name) ? name : null;
|
|
114
129
|
}
|
|
115
130
|
}
|
|
@@ -182,7 +197,7 @@ export function timberCacheTransform(ctx: PluginContext): Plugin {
|
|
|
182
197
|
stmt,
|
|
183
198
|
(call) =>
|
|
184
199
|
call.arguments.length === 2 &&
|
|
185
|
-
|
|
200
|
+
isCacheDataCallee(call.callee, cacheBindings) &&
|
|
186
201
|
!isCalleeShadowed(call),
|
|
187
202
|
calls
|
|
188
203
|
);
|
package/src/plugins/entries.ts
CHANGED
|
@@ -154,6 +154,7 @@ function generateConfigModule(ctx: PluginContext): string {
|
|
|
154
154
|
// Per-build deployment ID for version skew detection (TIM-446).
|
|
155
155
|
// Null in dev mode — HMR handles code updates without full reloads.
|
|
156
156
|
deploymentId: ctx.deploymentId ?? null,
|
|
157
|
+
clientSegmentCache: ctx.config.clientSegmentCache ?? false,
|
|
157
158
|
// Browser log forwarding threshold — used by buildClientScripts to
|
|
158
159
|
// decide whether /@vite/client is needed when clientJavascript is
|
|
159
160
|
// disabled. See TIM-1152.
|
|
@@ -174,23 +175,39 @@ function generateConfigModule(ctx: PluginContext): string {
|
|
|
174
175
|
}
|
|
175
176
|
|
|
176
177
|
/**
|
|
177
|
-
* Detect
|
|
178
|
+
* Detect a user convention file at both root and src/.
|
|
178
179
|
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
180
|
+
* Root takes precedence over src/, extension priority: .ts > .js > .mjs.
|
|
181
|
+
* Used for instrumentation.ts, timber.cache.ts, and similar file conventions.
|
|
181
182
|
*/
|
|
182
|
-
export function
|
|
183
|
+
export function detectConventionFile(root: string, basename: string): string | null {
|
|
183
184
|
const extensions = ['.ts', '.js', '.mjs'];
|
|
184
185
|
const dirs = [root, resolve(root, 'src')];
|
|
185
186
|
for (const dir of dirs) {
|
|
186
187
|
for (const ext of extensions) {
|
|
187
|
-
const candidate = resolve(dir,
|
|
188
|
+
const candidate = resolve(dir, `${basename}${ext}`);
|
|
188
189
|
if (existsSync(candidate)) return candidate;
|
|
189
190
|
}
|
|
190
191
|
}
|
|
191
192
|
return null;
|
|
192
193
|
}
|
|
193
194
|
|
|
195
|
+
export function detectInstrumentationFile(root: string): string | null {
|
|
196
|
+
return detectConventionFile(root, 'instrumentation');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Detect the user's timber.cache file.
|
|
201
|
+
*
|
|
202
|
+
* This file holds runtime cache instances (cacheHandler, cdnPurge)
|
|
203
|
+
* that cannot be JSON-serialized. Separating them from timber.config.ts
|
|
204
|
+
* prevents the RSC build from pulling build-time deps (adapters, MDX,
|
|
205
|
+
* rollup) into the runtime bundle. See TIM-1211.
|
|
206
|
+
*/
|
|
207
|
+
export function detectCacheFile(root: string): string | null {
|
|
208
|
+
return detectConventionFile(root, 'timber.cache');
|
|
209
|
+
}
|
|
210
|
+
|
|
194
211
|
/**
|
|
195
212
|
* Generate the virtual:timber-instrumentation module source.
|
|
196
213
|
*
|
|
@@ -202,9 +219,6 @@ export function detectInstrumentationFile(root: string): string | null {
|
|
|
202
219
|
*/
|
|
203
220
|
export function generateInstrumentationModule(instrumentationPath: string | null): string {
|
|
204
221
|
if (instrumentationPath) {
|
|
205
|
-
// Use the absolute path so the bundler can resolve and include it.
|
|
206
|
-
// The dynamic import ensures the file is loaded lazily and errors
|
|
207
|
-
// in the user's code don't prevent the module from being parsed.
|
|
208
222
|
return [
|
|
209
223
|
'// Auto-generated instrumentation loader — do not edit.',
|
|
210
224
|
'// Generated by timber-entries plugin.',
|
|
@@ -226,48 +240,33 @@ export function generateInstrumentationModule(instrumentationPath: string | null
|
|
|
226
240
|
].join('\n');
|
|
227
241
|
}
|
|
228
242
|
|
|
229
|
-
/**
|
|
230
|
-
* Detect the user's timber.config file at the project root.
|
|
231
|
-
* Returns the absolute path or null if not found.
|
|
232
|
-
*/
|
|
233
|
-
function detectConfigFile(root: string): string | null {
|
|
234
|
-
const names = ['timber.config.ts', 'timber.config.js', 'timber.config.mjs'];
|
|
235
|
-
for (const name of names) {
|
|
236
|
-
const candidate = resolve(root, name);
|
|
237
|
-
if (existsSync(candidate)) return candidate;
|
|
238
|
-
}
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
243
|
/**
|
|
243
244
|
* Generate the virtual:timber-cache-handler module source.
|
|
244
245
|
*
|
|
245
|
-
* When the user's
|
|
246
|
-
* dynamically imports
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
246
|
+
* When the user's timber.cache.ts exists, generates a module that
|
|
247
|
+
* dynamically imports it and extracts cacheHandler and cdnPurge.
|
|
248
|
+
* These are class instances (e.g. RedisCacheHandler) that cannot be
|
|
249
|
+
* JSON-serialized into virtual:timber-config, so they must be loaded
|
|
250
|
+
* at runtime via dynamic import.
|
|
251
|
+
*
|
|
252
|
+
* timber.cache.ts exports: default cacheHandler, named cdnPurge.
|
|
253
|
+
*
|
|
254
|
+
* See TIM-1211, TIM-599.
|
|
250
255
|
*/
|
|
251
|
-
function generateCacheHandlerModule(
|
|
252
|
-
|
|
253
|
-
hasCacheHandler: boolean,
|
|
254
|
-
hasCdnPurge: boolean
|
|
255
|
-
): string {
|
|
256
|
-
if (configPath && (hasCacheHandler || hasCdnPurge)) {
|
|
256
|
+
export function generateCacheHandlerModule(cachePath: string | null): string {
|
|
257
|
+
if (cachePath) {
|
|
257
258
|
return [
|
|
258
259
|
'// Auto-generated cache handler loader — do not edit.',
|
|
259
260
|
'// Generated by timber-entries plugin.',
|
|
260
261
|
'',
|
|
261
262
|
`export default async function loadCacheHandler() {`,
|
|
262
|
-
` const mod = await import(${JSON.stringify(
|
|
263
|
-
`
|
|
264
|
-
` return config.cacheHandler ?? null;`,
|
|
263
|
+
` const mod = await import(${JSON.stringify(cachePath)});`,
|
|
264
|
+
` return mod.default ?? null;`,
|
|
265
265
|
`}`,
|
|
266
266
|
'',
|
|
267
267
|
`export async function loadCdnPurgeHandler() {`,
|
|
268
|
-
` const mod = await import(${JSON.stringify(
|
|
269
|
-
`
|
|
270
|
-
` return config.cdnPurge ?? null;`,
|
|
268
|
+
` const mod = await import(${JSON.stringify(cachePath)});`,
|
|
269
|
+
` return mod.cdnPurge ?? null;`,
|
|
271
270
|
`}`,
|
|
272
271
|
].join('\n');
|
|
273
272
|
}
|
|
@@ -275,7 +274,7 @@ function generateCacheHandlerModule(
|
|
|
275
274
|
return [
|
|
276
275
|
'// Auto-generated cache handler loader — do not edit.',
|
|
277
276
|
'// Generated by timber-entries plugin.',
|
|
278
|
-
'// No
|
|
277
|
+
'// No timber.cache.ts found.',
|
|
279
278
|
'',
|
|
280
279
|
`export default async function loadCacheHandler() {`,
|
|
281
280
|
` return null;`,
|
|
@@ -352,10 +351,8 @@ export function timberEntries(ctx: PluginContext): Plugin {
|
|
|
352
351
|
return generateInstrumentationModule(instrumentationPath);
|
|
353
352
|
}
|
|
354
353
|
if (id === RESOLVED_CACHE_HANDLER_ID) {
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
const hasCdnPurge = ctx.config.cdnPurge != null;
|
|
358
|
-
return generateCacheHandlerModule(configPath, hasCacheHandler, hasCdnPurge);
|
|
354
|
+
const cachePath = detectCacheFile(ctx.root);
|
|
355
|
+
return generateCacheHandlerModule(cachePath);
|
|
359
356
|
}
|
|
360
357
|
return null;
|
|
361
358
|
},
|