@timber-js/app 0.2.0-alpha.165 → 0.2.0-alpha.167
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-CSDD6x7U.js → actions-TSxpXLHJ.js} +3 -3
- package/dist/_chunks/{actions-CSDD6x7U.js.map → actions-TSxpXLHJ.js.map} +1 -1
- package/dist/_chunks/{cache-api-eb1gydM7.js → cache-api-DzpQQOEx.js} +53 -49
- package/dist/_chunks/{cache-api-eb1gydM7.js.map → cache-api-DzpQQOEx.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-mGfRbjh2.js → cli-schema-sync-wX-i90Og.js} +6 -4
- package/dist/_chunks/cli-schema-sync-wX-i90Og.js.map +1 -0
- package/dist/_chunks/cloudflare-AHoWYTYr.js +1188 -0
- package/dist/_chunks/cloudflare-AHoWYTYr.js.map +1 -0
- package/dist/_chunks/{define-CFmvb4Bt.js → define-COtkxMRT.js} +16 -28
- package/dist/_chunks/define-COtkxMRT.js.map +1 -0
- package/dist/_chunks/{define-Bssfp6ot.js → define-c4au4I9R.js} +2 -2
- package/dist/_chunks/{define-Bssfp6ot.js.map → define-c4au4I9R.js.map} +1 -1
- package/dist/_chunks/{logger-B_O6-mdJ.js → logger-t3uxAmbX.js} +3 -3
- package/dist/_chunks/logger-t3uxAmbX.js.map +1 -0
- package/dist/_chunks/{plugin-context-BnaiU_cF.js → plugin-context---kTF5v8.js} +2 -2
- package/dist/_chunks/{plugin-context-BnaiU_cF.js.map → plugin-context---kTF5v8.js.map} +1 -1
- package/dist/_chunks/{resolve-schema-3iUvBV5T.js → resolve-schema-Dz3fcFUo.js} +2 -2
- package/dist/_chunks/{resolve-schema-3iUvBV5T.js.map → resolve-schema-Dz3fcFUo.js.map} +1 -1
- package/dist/_chunks/{schema-bridge-BY3QLBL7.js → schema-bridge-DT_Tn0Xf.js} +2 -2
- package/dist/_chunks/{schema-bridge-BY3QLBL7.js.map → schema-bridge-DT_Tn0Xf.js.map} +1 -1
- package/dist/_chunks/{use-query-states-CbeQmext.js → use-query-states-DFvWd-EA.js} +65 -7
- package/dist/_chunks/use-query-states-DFvWd-EA.js.map +1 -0
- package/dist/_chunks/{walkers-BL3MCMgO.js → walkers-Cfwvl-UC.js} +3 -3
- package/dist/_chunks/{walkers-BL3MCMgO.js.map → walkers-Cfwvl-UC.js.map} +1 -1
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +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/nitro.js +1 -1
- 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.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/client/browser-entry/post-hydration.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 +139 -115
- package/dist/client/internal.js.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/use-query-states.d.ts.map +1 -1
- package/dist/codec.js +1 -1
- package/dist/cookies/index.js +1 -1
- package/dist/dev-tools/logs.d.ts +15 -1
- package/dist/dev-tools/logs.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +138 -45
- package/dist/index.js.map +1 -1
- package/dist/params/index.js +1 -1
- 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/routing/index.js +2 -2
- package/dist/schema-bridge.d.ts.map +1 -1
- package/dist/search-params/define.d.ts +35 -4
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.js +3 -7
- package/dist/search-params/index.js.map +1 -1
- package/dist/search-params/wrappers.d.ts +2 -2
- package/dist/search-params/wrappers.d.ts.map +1 -1
- package/dist/segment-params/index.js +1 -1
- package/dist/server/als-registry.d.ts +7 -0
- package/dist/server/als-registry.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/index.js +2 -2
- package/dist/server/internal.js +12 -7
- package/dist/server/internal.js.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/render-route.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/stream-utils.d.ts.map +1 -1
- package/docs/api/32-api-cache.mdx +4 -4
- package/docs/api/33-api-search-params.mdx +13 -0
- package/docs/learn/00-introduction.mdx +1 -2
- package/docs/learn/09-caching.mdx +5 -5
- package/docs/more/03-coming-from-nextjs.mdx +2 -2
- package/docs/more/50-ai-agent-instructions.mdx +5 -5
- package/package.json +10 -8
- package/src/adapters/cloudflare.ts +63 -25
- package/src/adapters/types.ts +2 -0
- package/src/cache/cache-api.ts +84 -84
- package/src/cli.ts +0 -0
- package/src/client/browser-entry/post-hydration.ts +16 -9
- package/src/client/history.ts +11 -0
- package/src/client/router.ts +212 -206
- package/src/client/use-query-states.ts +99 -7
- package/src/dev-tools/logs.ts +119 -34
- package/src/index.ts +14 -1
- package/src/plugins/adapter-build.ts +1 -0
- package/src/plugins/cache.ts +45 -30
- package/src/routing/scanner.ts +7 -0
- package/src/schema-bridge.ts +4 -3
- package/src/search-params/define.ts +57 -51
- package/src/search-params/wrappers.ts +17 -26
- package/src/server/als-registry.ts +7 -0
- package/src/server/deny-boundary.ts +6 -3
- package/src/server/route-element-builder.ts +49 -8
- package/src/server/rsc-entry/render-route.ts +3 -1
- package/src/server/rsc-entry/rsc-payload.ts +21 -4
- package/src/server/rsc-entry/rsc-stream.ts +8 -0
- package/src/server/stream-utils.ts +8 -6
- package/LICENSE +0 -8
- package/dist/_chunks/cli-schema-sync-mGfRbjh2.js.map +0 -1
- package/dist/_chunks/define-CFmvb4Bt.js.map +0 -1
- package/dist/_chunks/logger-B_O6-mdJ.js.map +0 -1
- package/dist/_chunks/use-query-states-CbeQmext.js.map +0 -1
- package/dist/adapters/cloudflare.js.map +0 -1
|
@@ -95,6 +95,11 @@ export interface CloudflareBindings {
|
|
|
95
95
|
}
|
|
96
96
|
/** Options for the Cloudflare Workers adapter. */
|
|
97
97
|
export interface CloudflareAdapterOptions {
|
|
98
|
+
/**
|
|
99
|
+
* Worker name used in the generated wrangler.jsonc.
|
|
100
|
+
* @default 'timber-app'
|
|
101
|
+
*/
|
|
102
|
+
name?: string;
|
|
98
103
|
/**
|
|
99
104
|
* Cloudflare compatibility date.
|
|
100
105
|
* @default Current date in YYYY-MM-DD format at build time.
|
|
@@ -231,8 +236,14 @@ export interface WranglerBindingsConfig {
|
|
|
231
236
|
* @internal Exported for testing.
|
|
232
237
|
*/
|
|
233
238
|
export declare function generateBindingsConfig(bindings: CloudflareBindings | undefined): WranglerBindingsConfig;
|
|
239
|
+
/**
|
|
240
|
+
* Read the user's wrangler config from the project root, if one exists.
|
|
241
|
+
* Checks wrangler.jsonc first, then wrangler.json.
|
|
242
|
+
* @internal Exported for testing.
|
|
243
|
+
*/
|
|
244
|
+
export declare function readUserWranglerConfig(root: string): Promise<Record<string, unknown> | null>;
|
|
234
245
|
/** @internal Exported for testing. */
|
|
235
|
-
export declare function generateWranglerConfig(config: TimberConfig, options: CloudflareAdapterOptions): Record<string, unknown>;
|
|
246
|
+
export declare function generateWranglerConfig(config: TimberConfig, options: CloudflareAdapterOptions, userConfig?: Record<string, unknown> | null): Record<string, unknown>;
|
|
236
247
|
/** Command descriptor for preview — testable without spawning a process. */
|
|
237
248
|
export interface PreviewCommand {
|
|
238
249
|
command: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloudflare.d.ts","sourceRoot":"","sources":["../../src/adapters/cloudflare.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cloudflare.d.ts","sourceRoot":"","sources":["../../src/adapters/cloudflare.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,461 +1,2 @@
|
|
|
1
|
-
import { t as
|
|
2
|
-
|
|
3
|
-
import { readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
4
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
|
-
import { execFile } from "node:child_process";
|
|
6
|
-
//#region src/adapters/cloudflare.ts
|
|
7
|
-
var BINDINGS_ALS_KEY = Symbol.for("timber:cf-bindings-als");
|
|
8
|
-
function getBindingsAls() {
|
|
9
|
-
let als = globalThis[BINDINGS_ALS_KEY];
|
|
10
|
-
if (!als) {
|
|
11
|
-
als = new AsyncLocalStorage();
|
|
12
|
-
globalThis[BINDINGS_ALS_KEY] = als;
|
|
13
|
-
}
|
|
14
|
-
return als;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Get Cloudflare Worker bindings for the current request.
|
|
18
|
-
*
|
|
19
|
-
* Returns the `env` object passed to the Worker's `fetch` handler,
|
|
20
|
-
* giving direct access to KV, D1, Durable Objects, R2, Queues, and
|
|
21
|
-
* any other bindings configured in `wrangler.jsonc`.
|
|
22
|
-
*
|
|
23
|
-
* Must be called within a request context (server component, middleware,
|
|
24
|
-
* server action). Throws outside a request.
|
|
25
|
-
*
|
|
26
|
-
* @example
|
|
27
|
-
* ```ts
|
|
28
|
-
* import { getCloudflareBindings } from '@timber-js/app/adapters/cloudflare'
|
|
29
|
-
*
|
|
30
|
-
* export default async function Page() {
|
|
31
|
-
* const { MY_KV, MY_DB } = getCloudflareBindings()
|
|
32
|
-
* const data = await MY_KV.get('key')
|
|
33
|
-
* return <div>{data}</div>
|
|
34
|
-
* }
|
|
35
|
-
* ```
|
|
36
|
-
*/
|
|
37
|
-
function getCloudflareBindings() {
|
|
38
|
-
const env = getBindingsAls().getStore();
|
|
39
|
-
if (!env) throw new Error("getCloudflareBindings() called outside a Cloudflare Workers request context. It can only be called from server components, middleware, or server actions when running on the Cloudflare adapter.");
|
|
40
|
-
return env;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Run a function with Cloudflare bindings available via getCloudflareBindings().
|
|
44
|
-
* @internal Used by cloudflareDevBindings() to wrap dev-server requests.
|
|
45
|
-
*/
|
|
46
|
-
function runWithBindings(env, fn) {
|
|
47
|
-
return getBindingsAls().run(env, fn);
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Create a Cloudflare Workers adapter.
|
|
51
|
-
*
|
|
52
|
-
* @example
|
|
53
|
-
* ```ts
|
|
54
|
-
* import { cloudflare } from '@timber-js/app/adapters/cloudflare'
|
|
55
|
-
*
|
|
56
|
-
* export default {
|
|
57
|
-
* output: 'server',
|
|
58
|
-
* adapter: cloudflare(),
|
|
59
|
-
* }
|
|
60
|
-
* ```
|
|
61
|
-
*/
|
|
62
|
-
function cloudflare(options = {}) {
|
|
63
|
-
return {
|
|
64
|
-
name: "cloudflare",
|
|
65
|
-
async buildOutput(config, buildDir) {
|
|
66
|
-
const outDir = join(buildDir, "cloudflare");
|
|
67
|
-
await rm(join(outDir, "prebuilt"), {
|
|
68
|
-
recursive: true,
|
|
69
|
-
force: true
|
|
70
|
-
}).catch(() => {});
|
|
71
|
-
await rm(join(outDir, "static", "prebuilt"), {
|
|
72
|
-
recursive: true,
|
|
73
|
-
force: true
|
|
74
|
-
}).catch(() => {});
|
|
75
|
-
await runSharedBuildSteps({
|
|
76
|
-
config,
|
|
77
|
-
buildDir,
|
|
78
|
-
outDir,
|
|
79
|
-
publicDirName: "static"
|
|
80
|
-
});
|
|
81
|
-
let hasWorkerHandlers = false;
|
|
82
|
-
if (options.workerHandlers) {
|
|
83
|
-
const handlersEntry = join(process.cwd(), options.workerHandlers);
|
|
84
|
-
const { build: viteBuild } = await import("vite");
|
|
85
|
-
await viteBuild({
|
|
86
|
-
configFile: false,
|
|
87
|
-
logLevel: "info",
|
|
88
|
-
build: {
|
|
89
|
-
lib: {
|
|
90
|
-
entry: handlersEntry,
|
|
91
|
-
formats: ["es"],
|
|
92
|
-
fileName: () => "_worker-handlers.js"
|
|
93
|
-
},
|
|
94
|
-
outDir,
|
|
95
|
-
emptyOutDir: false,
|
|
96
|
-
minify: false,
|
|
97
|
-
rollupOptions: { external: [/^node:/, /^cloudflare:/] }
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
hasWorkerHandlers = true;
|
|
101
|
-
}
|
|
102
|
-
const prebuiltData = await readPrebuiltData(buildDir);
|
|
103
|
-
await writeFile(join(outDir, "_timber-prebuilt-source.js"), generatePrebuiltSourceModule(prebuiltData));
|
|
104
|
-
await rm(join(outDir, "prebuilt"), {
|
|
105
|
-
recursive: true,
|
|
106
|
-
force: true
|
|
107
|
-
}).catch(() => {});
|
|
108
|
-
await writeFile(join(outDir, "_timber-runtime-init.js"), generateRuntimeInitModule());
|
|
109
|
-
const workerEntry = generateWorkerEntry(outDir, outDir, !!config.manifestInit, hasWorkerHandlers);
|
|
110
|
-
await writeFile(join(outDir, "_worker.js"), workerEntry);
|
|
111
|
-
const wranglerConfig = generateWranglerConfig(config, options);
|
|
112
|
-
await writeFile(join(outDir, "wrangler.jsonc"), JSON.stringify(wranglerConfig, null, 2));
|
|
113
|
-
},
|
|
114
|
-
async preview(_config, buildDir) {
|
|
115
|
-
const cmd = generatePreviewCommand(buildDir);
|
|
116
|
-
await spawnPreviewProcess(cmd.command, cmd.args, cmd.cwd);
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
/** @internal Exported for testing. */
|
|
121
|
-
function generateWorkerEntry(buildDir, outDir, hasManifestInit = false, hasWorkerHandlers = false) {
|
|
122
|
-
let rscEntryRelative = relative(outDir, join(buildDir, "rsc", "index.js"));
|
|
123
|
-
if (!rscEntryRelative.startsWith(".")) rscEntryRelative = "./" + rscEntryRelative;
|
|
124
|
-
return `// Generated by @timber-js/app/adapters/cloudflare
|
|
125
|
-
// Do not edit — this file is regenerated on each build.
|
|
126
|
-
|
|
127
|
-
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
128
|
-
// Side-effect imports: evaluated IN ORDER before the _worker.js body runs.
|
|
129
|
-
// _timber-runtime-init.js sets TIMBER_RUNTIME before createRequestHandler()
|
|
130
|
-
// runs (during the RSC entry's top-level await) so the auto-wire for
|
|
131
|
-
// WorkersCachePurge can detect the Cloudflare runtime (TIM-1170).
|
|
132
|
-
// _timber-prebuilt-source.js sets globalThis.__TIMBER_PREBUILT_SOURCE__
|
|
133
|
-
// for cache.component payloads.
|
|
134
|
-
import './_timber-runtime-init.js'
|
|
135
|
-
import './_timber-prebuilt-source.js'
|
|
136
|
-
${hasManifestInit ? "import './_timber-manifest-init.js'\n" : ""}import handler, { runWithWaitUntil, runWithWorkersCachePurge } from '${rscEntryRelative}'
|
|
137
|
-
${hasWorkerHandlers ? "import * as workerHandlers from './_worker-handlers.js'\n" : ""}
|
|
138
|
-
|
|
139
|
-
// Bind Cloudflare env to ALS so getCloudflareBindings() works at runtime.
|
|
140
|
-
// Uses the same Symbol.for key as getCloudflareBindings() reads from.
|
|
141
|
-
const ALS_KEY = Symbol.for('timber:cf-bindings-als')
|
|
142
|
-
let bindingsAls = globalThis[ALS_KEY]
|
|
143
|
-
if (!bindingsAls) {
|
|
144
|
-
bindingsAls = new AsyncLocalStorage()
|
|
145
|
-
globalThis[ALS_KEY] = bindingsAls
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Register timber's platform tracer so pipeline phase spans (timber.proxy,
|
|
149
|
-
// timber.middleware, timber.render, timber.access, timber.action, …) appear
|
|
150
|
-
// as native Cloudflare spans in the Observability dashboard, nested under
|
|
151
|
-
// the auto-instrumented fetch/KV/D1 spans. Requires observability.traces
|
|
152
|
-
// enabled in wrangler.jsonc (generated by default).
|
|
153
|
-
//
|
|
154
|
-
// Lazily imported and guarded: runtimes without the custom spans API
|
|
155
|
-
// (older compatibility dates, older local workerd) fall back to OTEL-only
|
|
156
|
-
// emission. withSpan() in the framework composes both channels.
|
|
157
|
-
const TRACER_KEY = Symbol.for('timber:platform-tracer')
|
|
158
|
-
let tracerInit
|
|
159
|
-
function initPlatformTracer() {
|
|
160
|
-
tracerInit ??= import('cloudflare:workers')
|
|
161
|
-
.then(({ tracing }) => {
|
|
162
|
-
if (tracing && typeof tracing.enterSpan === 'function') {
|
|
163
|
-
globalThis[TRACER_KEY] ??= {
|
|
164
|
-
enterSpan: (name, fn) => tracing.enterSpan(name, fn),
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
})
|
|
168
|
-
.catch(() => {})
|
|
169
|
-
return tracerInit
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export default {
|
|
173
|
-
${hasWorkerHandlers ? " ...workerHandlers,\n" : ""} async fetch(request, env, ctx) {
|
|
174
|
-
// One-time on first request; resolved-promise await thereafter.
|
|
175
|
-
await initPlatformTracer()
|
|
176
|
-
|
|
177
|
-
// Edge-cache GET/HEAD responses via the Cache API. RSC navigation
|
|
178
|
-
// requests (Accept: text/x-component) are excluded — they vary by
|
|
179
|
-
// X-Timber-State-Tree which the cache key doesn't capture.
|
|
180
|
-
const isRsc = request.headers.get('Accept')?.includes('text/x-component')
|
|
181
|
-
const reqCc = request.headers.get('Cache-Control') || ''
|
|
182
|
-
const bypassCache = reqCc.includes('no-cache') || reqCc.includes('max-age=0')
|
|
183
|
-
|| request.headers.has('Pragma')
|
|
184
|
-
const cacheable = !isRsc && !bypassCache
|
|
185
|
-
&& (request.method === 'GET' || request.method === 'HEAD')
|
|
186
|
-
const cache = cacheable && typeof caches !== 'undefined' ? caches.default : null
|
|
187
|
-
if (cache) {
|
|
188
|
-
const cached = await cache.match(request)
|
|
189
|
-
if (cached) return cached
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Bridge ctx.cache into the per-request ALS so WorkersCachePurge can
|
|
193
|
-
// call ctx.cache.purge({ tags }) during cache.invalidate/revalidateTag.
|
|
194
|
-
// Workers Cache is available on all Cloudflare plans — no API token needed.
|
|
195
|
-
const workersCacheCtx = ctx.cache && typeof ctx.cache.purge === 'function' ? ctx.cache : null
|
|
196
|
-
const runHandler = () =>
|
|
197
|
-
runWithWaitUntil((promise) => ctx.waitUntil(promise), () => handler(request))
|
|
198
|
-
const response = await bindingsAls.run(env, () =>
|
|
199
|
-
workersCacheCtx
|
|
200
|
-
? runWithWorkersCachePurge(workersCacheCtx, runHandler)
|
|
201
|
-
: runHandler()
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
// Cache public responses at the edge. A response is edge-cacheable if
|
|
205
|
-
// it has 'public' or 's-maxage' (a shared-cache TTL directive) and no
|
|
206
|
-
// 'no-store' or 'private'. Cloudflare's cache.put() only accepts GET
|
|
207
|
-
// requests and rejects 206, so only store GET 200s. HEAD requests
|
|
208
|
-
// benefit from cache.match() which handles HEAD→GET matching automatically.
|
|
209
|
-
// See design/25-production-deployments.md §"CDN / Edge Cache".
|
|
210
|
-
if (cache && request.method === 'GET' && response.status === 200) {
|
|
211
|
-
const cc = response.headers.get('Cache-Control')
|
|
212
|
-
const vary = response.headers.get('Vary')
|
|
213
|
-
if (cc && !cc.includes('no-store') && !cc.includes('private')
|
|
214
|
-
&& (cc.includes('public') || cc.includes('s-maxage'))
|
|
215
|
-
&& vary !== '*' && !response.headers.has('Set-Cookie')) {
|
|
216
|
-
ctx.waitUntil(cache.put(request, response.clone()))
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return response
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
`;
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Discover and read prebuilt flight payload files from the build directory.
|
|
227
|
-
* Returns null when no prebuilt artifacts exist (no cache.component usage).
|
|
228
|
-
*
|
|
229
|
-
* @internal Exported for testing.
|
|
230
|
-
*/
|
|
231
|
-
async function readPrebuiltData(buildDir) {
|
|
232
|
-
const prebuiltDir = join(buildDir, "prebuilt");
|
|
233
|
-
let manifestJson;
|
|
234
|
-
try {
|
|
235
|
-
manifestJson = await readFile(join(prebuiltDir, "manifest.json"), "utf-8");
|
|
236
|
-
} catch {
|
|
237
|
-
return null;
|
|
238
|
-
}
|
|
239
|
-
const payloads = [];
|
|
240
|
-
try {
|
|
241
|
-
const componentDirs = await readdir(prebuiltDir, { withFileTypes: true });
|
|
242
|
-
for (const dir of componentDirs) {
|
|
243
|
-
if (!dir.isDirectory()) continue;
|
|
244
|
-
const files = await readdir(join(prebuiltDir, dir.name));
|
|
245
|
-
for (const file of files) if (file.endsWith(".flight")) {
|
|
246
|
-
const filePath = `${dir.name}/${file}`;
|
|
247
|
-
const bytes = await readFile(join(prebuiltDir, filePath));
|
|
248
|
-
payloads.push({
|
|
249
|
-
file: filePath,
|
|
250
|
-
bytes
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
} catch {}
|
|
255
|
-
return {
|
|
256
|
-
manifestJson,
|
|
257
|
-
payloads
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* Generate the prebuilt payload source module for Cloudflare Workers.
|
|
262
|
-
*
|
|
263
|
-
* Inlines manifest JSON and flight payload bytes directly into the module
|
|
264
|
-
* so they're part of the worker's JS bundle — no external Text/Data module
|
|
265
|
-
* imports needed. This avoids depending on wrangler's find_additional_modules
|
|
266
|
-
* to discover non-JS files, which doesn't reliably resolve Text/Data imports.
|
|
267
|
-
*
|
|
268
|
-
* @internal Exported for testing.
|
|
269
|
-
*/
|
|
270
|
-
function generatePrebuiltSourceModule(data) {
|
|
271
|
-
if (!data) return `// Generated by @timber-js/app/adapters/cloudflare
|
|
272
|
-
// No prebuilt artifacts — all cache.component wrappers render dynamically.
|
|
273
|
-
globalThis.__TIMBER_PREBUILT_SOURCE__ = {
|
|
274
|
-
async loadManifest() { return null },
|
|
275
|
-
async readPayload() { return null },
|
|
276
|
-
}
|
|
277
|
-
`;
|
|
278
|
-
const entries = [];
|
|
279
|
-
for (const { file, bytes } of data.payloads) {
|
|
280
|
-
const b64 = bytes.toString("base64");
|
|
281
|
-
entries.push(` ${JSON.stringify(file)}: ${JSON.stringify(b64)}`);
|
|
282
|
-
}
|
|
283
|
-
return `// Generated by @timber-js/app/adapters/cloudflare
|
|
284
|
-
// Prebuilt payload source — inlines cache.component flight payloads
|
|
285
|
-
// directly into the worker bundle (TIM-1162).
|
|
286
|
-
const _manifest = ${data.manifestJson.trim()}
|
|
287
|
-
|
|
288
|
-
function _decode(b64) {
|
|
289
|
-
const bin = atob(b64)
|
|
290
|
-
const buf = new Uint8Array(bin.length)
|
|
291
|
-
for (let i = 0; i < bin.length; i++) buf[i] = bin.charCodeAt(i)
|
|
292
|
-
return buf
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const _payloads = {
|
|
296
|
-
${entries.join(",\n")}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
globalThis.__TIMBER_PREBUILT_SOURCE__ = {
|
|
300
|
-
async loadManifest() { return _manifest },
|
|
301
|
-
async readPayload(file) {
|
|
302
|
-
const b64 = _payloads[file]
|
|
303
|
-
return b64 ? _decode(b64) : null
|
|
304
|
-
},
|
|
305
|
-
}
|
|
306
|
-
`;
|
|
307
|
-
}
|
|
308
|
-
/**
|
|
309
|
-
* Generate the runtime init module that sets TIMBER_RUNTIME before the
|
|
310
|
-
* RSC entry evaluates. Imported as a side-effect module in _worker.js.
|
|
311
|
-
*
|
|
312
|
-
* @internal Exported for testing.
|
|
313
|
-
*/
|
|
314
|
-
function generateRuntimeInitModule() {
|
|
315
|
-
return `// Generated by @timber-js/app/adapters/cloudflare
|
|
316
|
-
// Sets TIMBER_RUNTIME before the RSC handler evaluates (TIM-1170).
|
|
317
|
-
// Must be a separate module imported before rsc/index.js so that ESM
|
|
318
|
-
// evaluation order guarantees the env var is set when
|
|
319
|
-
// createRequestHandler() runs its auto-wire logic.
|
|
320
|
-
globalThis.process ??= { env: {} }
|
|
321
|
-
process.env.TIMBER_RUNTIME = 'cloudflare'
|
|
322
|
-
`;
|
|
323
|
-
}
|
|
324
|
-
/**
|
|
325
|
-
* Convert declarative `CloudflareBindings` into wrangler.jsonc binding sections.
|
|
326
|
-
*
|
|
327
|
-
* Maps:
|
|
328
|
-
* - `kv` → `kv_namespaces`
|
|
329
|
-
* - `d1` → `d1_databases`
|
|
330
|
-
* - `r2` → `r2_buckets`
|
|
331
|
-
* - `queues` → `queues` (producers + consumers)
|
|
332
|
-
* - `durableObjects` → `durable_objects`
|
|
333
|
-
*
|
|
334
|
-
* Empty arrays are omitted from the output.
|
|
335
|
-
*
|
|
336
|
-
* @internal Exported for testing.
|
|
337
|
-
*/
|
|
338
|
-
function generateBindingsConfig(bindings) {
|
|
339
|
-
if (!bindings) return {};
|
|
340
|
-
const result = {};
|
|
341
|
-
if (bindings.kv && bindings.kv.length > 0) result.kv_namespaces = bindings.kv.map((kv) => ({
|
|
342
|
-
binding: kv.name,
|
|
343
|
-
id: kv.id ?? ""
|
|
344
|
-
}));
|
|
345
|
-
if (bindings.d1 && bindings.d1.length > 0) result.d1_databases = bindings.d1.map((d1) => ({
|
|
346
|
-
binding: d1.name,
|
|
347
|
-
database_id: d1.database_id
|
|
348
|
-
}));
|
|
349
|
-
if (bindings.r2 && bindings.r2.length > 0) result.r2_buckets = bindings.r2.map((r2) => ({
|
|
350
|
-
binding: r2.name,
|
|
351
|
-
bucket_name: r2.bucket_name
|
|
352
|
-
}));
|
|
353
|
-
if (bindings.queues) {
|
|
354
|
-
const queues = {};
|
|
355
|
-
if (bindings.queues.producers && bindings.queues.producers.length > 0) queues.producers = bindings.queues.producers.map((p) => ({
|
|
356
|
-
binding: p.name,
|
|
357
|
-
queue: p.queue
|
|
358
|
-
}));
|
|
359
|
-
if (bindings.queues.consumers && bindings.queues.consumers.length > 0) queues.consumers = bindings.queues.consumers.map((c) => {
|
|
360
|
-
const consumer = { queue: c.queue };
|
|
361
|
-
if (c.max_batch_size !== void 0) consumer.max_batch_size = c.max_batch_size;
|
|
362
|
-
if (c.max_retries !== void 0) consumer.max_retries = c.max_retries;
|
|
363
|
-
if (c.dead_letter_queue !== void 0) consumer.dead_letter_queue = c.dead_letter_queue;
|
|
364
|
-
return consumer;
|
|
365
|
-
});
|
|
366
|
-
if (Object.keys(queues).length > 0) result.queues = queues;
|
|
367
|
-
}
|
|
368
|
-
if (bindings.durableObjects && bindings.durableObjects.length > 0) result.durable_objects = { bindings: bindings.durableObjects.map((dobj) => {
|
|
369
|
-
const entry = {
|
|
370
|
-
name: dobj.name,
|
|
371
|
-
class_name: dobj.class_name
|
|
372
|
-
};
|
|
373
|
-
if (dobj.script_name) entry.script_name = dobj.script_name;
|
|
374
|
-
return entry;
|
|
375
|
-
}) };
|
|
376
|
-
return result;
|
|
377
|
-
}
|
|
378
|
-
/**
|
|
379
|
-
* One-level deep merge: for each key in `override`, if both the base and
|
|
380
|
-
* override values are plain objects (not arrays, not null), merge their
|
|
381
|
-
* keys with override winning on conflicts. Everything else (primitives,
|
|
382
|
-
* arrays, null) is replaced outright — matching the intuitive behavior
|
|
383
|
-
* where `kv_namespaces: [...]` replaces fully but
|
|
384
|
-
* `durable_objects: { migrations }` merges with generated `bindings`.
|
|
385
|
-
*/
|
|
386
|
-
function shallowDeepMerge(base, override) {
|
|
387
|
-
const result = { ...base };
|
|
388
|
-
for (const key of Object.keys(override)) {
|
|
389
|
-
const baseVal = result[key];
|
|
390
|
-
const overVal = override[key];
|
|
391
|
-
if (isPlainObject(baseVal) && isPlainObject(overVal)) result[key] = {
|
|
392
|
-
...baseVal,
|
|
393
|
-
...overVal
|
|
394
|
-
};
|
|
395
|
-
else result[key] = overVal;
|
|
396
|
-
}
|
|
397
|
-
return result;
|
|
398
|
-
}
|
|
399
|
-
/** True for `{}` literals — false for arrays, null, Date, etc. */
|
|
400
|
-
function isPlainObject(val) {
|
|
401
|
-
return val !== null && typeof val === "object" && !Array.isArray(val);
|
|
402
|
-
}
|
|
403
|
-
/** @internal Exported for testing. */
|
|
404
|
-
function generateWranglerConfig(config, options) {
|
|
405
|
-
const base = {
|
|
406
|
-
name: "timber-app",
|
|
407
|
-
main: "_worker.js",
|
|
408
|
-
compatibility_date: options.compatibilityDate ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
409
|
-
compatibility_flags: options.compatibilityFlags ?? ["nodejs_compat"],
|
|
410
|
-
no_bundle: true,
|
|
411
|
-
find_additional_modules: true,
|
|
412
|
-
rules: [{
|
|
413
|
-
type: "ESModule",
|
|
414
|
-
globs: ["**/*.js"]
|
|
415
|
-
}],
|
|
416
|
-
assets: {
|
|
417
|
-
directory: "./static",
|
|
418
|
-
binding: "ASSETS"
|
|
419
|
-
},
|
|
420
|
-
observability: { traces: { enabled: true } }
|
|
421
|
-
};
|
|
422
|
-
const bindingsConfig = generateBindingsConfig(options.bindings);
|
|
423
|
-
const merged = {
|
|
424
|
-
...base,
|
|
425
|
-
...bindingsConfig
|
|
426
|
-
};
|
|
427
|
-
if (options.wrangler) return shallowDeepMerge(merged, options.wrangler);
|
|
428
|
-
return merged;
|
|
429
|
-
}
|
|
430
|
-
/** @internal Exported for testing. */
|
|
431
|
-
function generatePreviewCommand(buildDir) {
|
|
432
|
-
const cfDir = join(buildDir, "cloudflare");
|
|
433
|
-
return {
|
|
434
|
-
command: "wrangler",
|
|
435
|
-
args: [
|
|
436
|
-
"dev",
|
|
437
|
-
"--local",
|
|
438
|
-
"--config",
|
|
439
|
-
join(cfDir, "wrangler.jsonc")
|
|
440
|
-
],
|
|
441
|
-
cwd: cfDir
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
/**
|
|
445
|
-
* Spawn a long-running preview process and pipe stdio to the parent.
|
|
446
|
-
* Resolves when the process exits.
|
|
447
|
-
*/
|
|
448
|
-
function spawnPreviewProcess(command, args, cwd) {
|
|
449
|
-
return new Promise((resolve, reject) => {
|
|
450
|
-
const child = execFile(command, args, { cwd }, (err) => {
|
|
451
|
-
if (err) reject(err);
|
|
452
|
-
else resolve();
|
|
453
|
-
});
|
|
454
|
-
child.stdout?.pipe(process.stdout);
|
|
455
|
-
child.stderr?.pipe(process.stderr);
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
|
-
//#endregion
|
|
459
|
-
export { cloudflare, generateBindingsConfig, generatePrebuiltSourceModule, generatePreviewCommand, generateRuntimeInitModule, generateWorkerEntry, generateWranglerConfig, getCloudflareBindings, readPrebuiltData, runWithBindings };
|
|
460
|
-
|
|
461
|
-
//# sourceMappingURL=cloudflare.js.map
|
|
1
|
+
import { a as generateRuntimeInitModule, c as getCloudflareBindings, d as runWithBindings, i as generatePreviewCommand, l as readPrebuiltData, n as generateBindingsConfig, o as generateWorkerEntry, r as generatePrebuiltSourceModule, s as generateWranglerConfig, t as cloudflare, u as readUserWranglerConfig } from "../_chunks/cloudflare-AHoWYTYr.js";
|
|
2
|
+
export { cloudflare, generateBindingsConfig, generatePrebuiltSourceModule, generatePreviewCommand, generateRuntimeInitModule, generateWorkerEntry, generateWranglerConfig, getCloudflareBindings, readPrebuiltData, readUserWranglerConfig, runWithBindings };
|
package/dist/adapters/nitro.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { n as IMMUTABLE_CACHE, t as runSharedBuildSteps } from "../_chunks/build-output-helper-DF5uScqS.js";
|
|
2
|
+
import { transformWithOxc } from "vite";
|
|
2
3
|
import { basename, dirname, join, relative } from "node:path";
|
|
3
4
|
import { cp, glob, readFile, writeFile } from "node:fs/promises";
|
|
4
|
-
import { transformWithOxc } from "vite";
|
|
5
5
|
import { execFile } from "node:child_process";
|
|
6
6
|
//#region src/server/compress.ts?raw
|
|
7
7
|
var compress_default = "// Response compression for self-hosted deployments (dev server, Nitro preview).\n//\n// Uses CompressionStream (Web Platform API) for gzip. Brotli is intentionally\n// left to CDNs and reverse proxies — at streaming-friendly quality levels its\n// ratio advantage is marginal, and node:zlib's brotli transform buffers output\n// internally, breaking streaming. Cloudflare Workers auto-compress at the edge —\n// this module is only used on Node.js/Bun.\n//\n// See design/25-production-deployments.md.\n\n// ─── Constants ────────────────────────────────────────────────────────────\n\n/**\n * MIME types that benefit from compression.\n * text/* is handled via prefix matching; these are the specific\n * application/* and image/* types that are compressible.\n */\nexport const COMPRESSIBLE_TYPES = new Set([\n 'text/html',\n 'text/css',\n 'text/plain',\n 'text/xml',\n 'text/javascript',\n 'text/x-component',\n 'application/json',\n 'application/javascript',\n 'application/xml',\n 'application/xhtml+xml',\n 'application/rss+xml',\n 'application/atom+xml',\n 'image/svg+xml',\n]);\n\n/**\n * Status codes that should never be compressed (no body or special semantics).\n */\nconst NO_COMPRESS_STATUSES = new Set([204, 304]);\n\n// ─── Encoding Negotiation ─────────────────────────────────────────────────\n\n/**\n * Parse Accept-Encoding and return the best supported encoding.\n * Returns 'gzip' if the client accepts it, null otherwise.\n *\n * Brotli (br) is intentionally not handled at the application level.\n * At the streaming-friendly quality levels (0–4), brotli's compression\n * ratio advantage over gzip is marginal, and node:zlib's brotli transform\n * buffers output internally — turning smooth streaming responses into\n * large infrequent bursts. Brotli's real wins come from offline/static\n * compression at higher quality levels (5–11), which CDNs and reverse\n * proxies (Cloudflare, nginx, Caddy) apply on cached responses.\n *\n * See design/25-production-deployments.md.\n */\nexport function negotiateEncoding(acceptEncoding: string): 'gzip' | null {\n if (!acceptEncoding) return null;\n\n // Parse tokens with quality values from the Accept-Encoding header.\n // Per RFC 9110 §12.5.3, q=0 means \"not acceptable\" — the client explicitly\n // rejects that encoding. Tokens without a q-value default to q=1.\n // e.g. \"gzip;q=1.0, br;q=0\" → gzip is acceptable, br is rejected.\n const parts = acceptEncoding.split(',');\n for (const part of parts) {\n const [token, ...params] = part.split(';');\n const name = token.trim().toLowerCase();\n if (name !== 'gzip') continue;\n\n // Check for q=0 (explicitly disabled)\n let qValue = 1; // default quality is 1\n for (const param of params) {\n const trimmed = param.trim().toLowerCase();\n if (trimmed.startsWith('q=')) {\n qValue = parseFloat(trimmed.slice(2));\n if (Number.isNaN(qValue)) qValue = 1;\n break;\n }\n }\n\n if (qValue > 0) return 'gzip';\n }\n\n return null;\n}\n\n// ─── Compressibility Check ────────────────────────────────────────────────\n\n/**\n * Determine if a response should be compressed.\n *\n * Returns false for:\n * - Responses without a body (204, 304, null body)\n * - Already-encoded responses (Content-Encoding set)\n * - Non-compressible content types (images, binary)\n * - SSE streams (text/event-stream — must not be buffered)\n */\nexport function shouldCompress(response: Response): boolean {\n // No body to compress\n if (!response.body) return false;\n if (NO_COMPRESS_STATUSES.has(response.status)) return false;\n\n // Already compressed\n if (response.headers.has('Content-Encoding')) return false;\n\n // Check content type\n const contentType = response.headers.get('Content-Type');\n if (!contentType) return false;\n\n // Extract the MIME type (strip charset and other parameters)\n const mimeType = contentType.split(';')[0].trim().toLowerCase();\n\n // SSE must not be compressed — it relies on chunk-by-chunk delivery\n if (mimeType === 'text/event-stream') return false;\n\n return COMPRESSIBLE_TYPES.has(mimeType);\n}\n\n// ─── Compression ──────────────────────────────────────────────────────────\n\n/**\n * Compress a Web Response if the client supports it and the content is compressible.\n *\n * Returns the original response unchanged if compression is not applicable.\n * Returns a new Response with the compressed body, Content-Encoding, and Vary headers.\n *\n * The body is piped through a compression stream — no buffering of the full response.\n * This preserves streaming behavior for HTML shell + deferred Suspense chunks.\n */\nexport function compressResponse(request: Request, response: Response): Response {\n // Check if response is compressible\n if (!shouldCompress(response)) return response;\n\n // Negotiate encoding with the client\n const acceptEncoding = request.headers.get('Accept-Encoding') ?? '';\n const encoding = negotiateEncoding(acceptEncoding);\n if (!encoding) return response;\n\n // Compress the body stream with gzip via the Web Platform CompressionStream API.\n const compressedBody = compressWithGzip(response.body!);\n\n // Build new headers: copy originals, add compression headers, remove Content-Length\n // (compressed size is unknown until streaming completes).\n const headers = new Headers(response.headers);\n headers.set('Content-Encoding', encoding);\n headers.delete('Content-Length');\n\n // Append to Vary header (preserve existing Vary values)\n const existingVary = headers.get('Vary');\n if (existingVary) {\n if (!existingVary.toLowerCase().includes('accept-encoding')) {\n headers.set('Vary', `${existingVary}, Accept-Encoding`);\n }\n } else {\n headers.set('Vary', 'Accept-Encoding');\n }\n\n return new Response(compressedBody, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n}\n\n// ─── Gzip (node:zlib with Z_SYNC_FLUSH) ──────────────────────────────────\n//\n// Uses node:zlib's createGzip with Z_SYNC_FLUSH so each chunk is flushed\n// to the output immediately. The Web Platform CompressionStream API buffers\n// internally and does NOT flush per-chunk — this kills streaming because\n// the browser doesn't receive the HTML shell until the gzip stream closes\n// (i.e. after all Suspense boundaries resolve).\n//\n// Z_SYNC_FLUSH adds ~2–5% size overhead vs Z_NO_FLUSH but preserves\n// correct streaming behavior: the shell renders instantly, Suspense\n// fallbacks are visible immediately, and streamed content appears\n// progressively.\n\nimport { createGzip, constants } from 'node:zlib';\nimport { Readable, pipeline } from 'node:stream';\n\n/**\n * Compress a ReadableStream with gzip, flushing each chunk immediately.\n *\n * Uses node:zlib's createGzip with Z_SYNC_FLUSH to ensure each HTML chunk\n * (shell, Suspense resolution, RSC payload) is delivered to the browser\n * as soon as it's available — preserving streaming semantics.\n */\nfunction compressWithGzip(body: ReadableStream<Uint8Array>): ReadableStream<Uint8Array> {\n const gzip = createGzip({ flush: constants.Z_SYNC_FLUSH });\n const nodeInput = Readable.fromWeb(body as import('stream/web').ReadableStream);\n\n // pipeline() instead of bare pipe(): pipe() does not forward source errors,\n // so an erroring body (streaming route handler, dropped upstream fetch)\n // would emit an unhandled 'error' event and crash the process. pipeline()\n // destroys gzip on source error — the returned web stream rejects — and\n // tears down nodeInput when the consumer cancels the web stream, which\n // cancels the source body via fromWeb. The error reaches the consumer\n // through the web stream; the callback only marks it handled at the Node\n // layer.\n pipeline(nodeInput, gzip, () => {});\n\n return Readable.toWeb(gzip) as ReadableStream<Uint8Array>;\n}\n";
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/adapters/types.ts"],"names":[],"mappings":"AAMA;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC5B,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,WAAW,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnE;;;OAGG;IACH,OAAO,CAAC,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEhE;;;;;;;;;OASG;IACH,cAAc,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CACxC"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/adapters/types.ts"],"names":[],"mappings":"AAMA;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,qDAAqD;IACrD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC5B,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,WAAW,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnE;;;OAGG;IACH,OAAO,CAAC,CAAC,MAAM,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEhE;;;;;;;;;OASG;IACH,cAAc,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CACxC"}
|
|
@@ -1,26 +1,48 @@
|
|
|
1
1
|
import type { CacheOptions } from './index';
|
|
2
2
|
import type { PrebuiltComponentOptions } from '../server/prebuilt-runtime';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Wraps an async function with cross-request caching. Uses the configured
|
|
7
|
-
* cache handler (defaults to MemoryCacheHandler, overridable via timber.config.ts).
|
|
4
|
+
* Cache namespace — `cache.data`, `cache.component`, `cache.invalidate`.
|
|
8
5
|
*
|
|
9
6
|
* ```ts
|
|
10
7
|
* import { cache } from '@timber-js/app/cache';
|
|
11
8
|
*
|
|
12
|
-
* const getUser = cache(
|
|
9
|
+
* const getUser = cache.data(
|
|
13
10
|
* async (id: string) => db.users.findUnique({ where: { id } }),
|
|
14
11
|
* { ttl: 60, tags: (id) => [`user:${id}`] }
|
|
15
12
|
* );
|
|
16
13
|
* ```
|
|
17
14
|
*/
|
|
18
|
-
export declare
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
export declare const cache: {
|
|
16
|
+
/**
|
|
17
|
+
* Wrap an async function with cross-request caching.
|
|
18
|
+
*
|
|
19
|
+
* The configured cache handler (defaults to MemoryCacheHandler, overridable
|
|
20
|
+
* via timber.config.ts) is re-resolved on every call, so module-scope
|
|
21
|
+
* wrappers created before `setCacheHandler()` still pick up the configured
|
|
22
|
+
* handler (TIM-1029).
|
|
23
|
+
*/
|
|
24
|
+
data<Fn extends (...args: any[]) => Promise<any>>(fn: Fn, opts: CacheOptions<Fn>, stableId?: string): Fn;
|
|
25
|
+
/**
|
|
26
|
+
* Invalidate cache entries by tag or key.
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* cache.invalidate({ tag: 'products' });
|
|
30
|
+
* cache.invalidate({ key: 'user:abc' });
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
invalidate(opts: {
|
|
21
34
|
key?: string;
|
|
22
35
|
tag?: string;
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
|
|
36
|
+
}): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Runtime fallback for `cache.component(...)` callsites the timber-prebuilt
|
|
39
|
+
* transform does not rewrite — nested/computed/argument-position forms (see
|
|
40
|
+
* plugins/prebuilt.ts). Only module-scope `const X = cache.component(...)`
|
|
41
|
+
* declarations are transformed and eligible for prebuilding; everything else
|
|
42
|
+
* lands here, warns once per component, and renders dynamically. Same
|
|
43
|
+
* safety-net pattern as the data cache's runtime fnId fallback (TIM-1054):
|
|
44
|
+
* an untraceable callsite degrades in performance, never in correctness.
|
|
45
|
+
*/
|
|
46
|
+
component<C extends (props: any) => unknown>(componentFn: C, _options?: PrebuiltComponentOptions): C;
|
|
47
|
+
};
|
|
26
48
|
//# sourceMappingURL=cache-api.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache-api.d.ts","sourceRoot":"","sources":["../../src/cache/cache-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"cache-api.d.ts","sourceRoot":"","sources":["../../src/cache/cache-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAS3E;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,KAAK;IAChB;;;;;;;OAOG;SAEE,EAAE,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,GAAG,CAAC,MAC1C,EAAE,QACA,YAAY,CAAC,EAAE,CAAC,aACX,MAAM,GAChB,EAAE;IAIL;;;;;;;OAOG;qBACoB;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAiCrE;;;;;;;;OAQG;cAEO,CAAC,SAAS,CAAC,KAAK,EAAE,GAAG,KAAK,OAAO,eAC5B,CAAC,aACH,wBAAwB,GAClC,CAAC;CAYL,CAAC"}
|
package/dist/cache/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { _ as setDefaultSingleflightTimeout, f as MemoryCacheHandler, g as setCacheHandler, h as getCacheHandler, m as RedisCacheHandler, p as TagAwareCacheHandler, t as cache } from "../_chunks/cache-api-
|
|
1
|
+
import { _ as setDefaultSingleflightTimeout, f as MemoryCacheHandler, g as setCacheHandler, h as getCacheHandler, m as RedisCacheHandler, p as TagAwareCacheHandler, t as cache } from "../_chunks/cache-api-DzpQQOEx.js";
|
|
2
2
|
import { t as estimateByteSize } from "../_chunks/sizeof-lXffQ1Vr.js";
|
|
3
3
|
export { MemoryCacheHandler, RedisCacheHandler, TagAwareCacheHandler, cache, estimateByteSize, getCacheHandler, setCacheHandler, setDefaultSingleflightTimeout };
|
package/dist/cli.js
CHANGED
|
@@ -79,7 +79,7 @@ function resolvePreviewStrategy(adapter) {
|
|
|
79
79
|
* Vite preview. See TIM-1066.
|
|
80
80
|
*/
|
|
81
81
|
async function runPreview(options, _deps) {
|
|
82
|
-
const { loadTimberConfigFile, resolveBuildDir } = await import("./_chunks/plugin-context
|
|
82
|
+
const { loadTimberConfigFile, resolveBuildDir } = await import("./_chunks/plugin-context---kTF5v8.js").then((n) => n.i);
|
|
83
83
|
const root = process.cwd();
|
|
84
84
|
const config = loadTimberConfigFile(root);
|
|
85
85
|
const adapter = config?.adapter;
|
|
@@ -115,7 +115,7 @@ async function runCheck(options) {
|
|
|
115
115
|
async function runSchema(args) {
|
|
116
116
|
const subcommand = args[0];
|
|
117
117
|
if (subcommand !== "sync") throw new Error(`Unknown schema subcommand: ${subcommand ?? "(none)"}. Usage: timber schema sync`);
|
|
118
|
-
const { runSchemaSync, defaultCodecForSegment } = await import("./_chunks/cli-schema-sync-
|
|
118
|
+
const { runSchemaSync, defaultCodecForSegment } = await import("./_chunks/cli-schema-sync-wX-i90Og.js").then((n) => n.t);
|
|
119
119
|
const result = runSchemaSync(process.cwd());
|
|
120
120
|
if (result.filesystemSegments.length === 0) {
|
|
121
121
|
console.log("[timber] No dynamic segments found in app/.");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"post-hydration.d.ts","sourceRoot":"","sources":["../../../src/client/browser-entry/post-hydration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAGpE,UAAU,oBAAoB;IAC5B,MAAM,EAAE,cAAc,CAAC;IACvB,gBAAgB,EAAE,uBAAuB,GAAG,IAAI,CAAC;IACjD,oEAAoE;IACpE,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,MAAM,EACN,gBAAgB,EAChB,cAAc,GACf,EAAE,oBAAoB,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"post-hydration.d.ts","sourceRoot":"","sources":["../../../src/client/browser-entry/post-hydration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAGpE,UAAU,oBAAoB;IAC5B,MAAM,EAAE,cAAc,CAAC;IACvB,gBAAgB,EAAE,uBAAuB,GAAG,IAAI,CAAC;IACjD,oEAAoE;IACpE,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,MAAM,EACN,gBAAgB,EAChB,cAAc,GACf,EAAE,oBAAoB,GAAG,IAAI,CA6E7B"}
|