@timber-js/app 0.2.0-alpha.14 → 0.2.0-alpha.16
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/adapters/nitro.d.ts +17 -1
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +5 -5
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/package.json +7 -6
- package/src/adapters/nitro.ts +25 -4
- package/src/cli.ts +0 -0
- package/src/index.ts +7 -2
- package/src/server/html-injectors.ts +30 -10
- package/LICENSE +0 -8
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html-injectors.d.ts","sourceRoot":"","sources":["../../src/server/html-injectors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAiEH;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,QAAQ,EAAE,MAAM,GACf,cAAc,CAAC,UAAU,CAAC,CAE5B;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,WAAW,EAAE,MAAM,GAClB,cAAc,CAAC,UAAU,CAAC,CAE5B;AAkBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,GACpC,cAAc,CAAC,UAAU,CAAC,CA4B5B;
|
|
1
|
+
{"version":3,"file":"html-injectors.d.ts","sourceRoot":"","sources":["../../src/server/html-injectors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAiEH;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,QAAQ,EAAE,MAAM,GACf,cAAc,CAAC,UAAU,CAAC,CAE5B;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,EAClC,WAAW,EAAE,MAAM,GAClB,cAAc,CAAC,UAAU,CAAC,CAE5B;AAkBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,GACpC,cAAc,CAAC,UAAU,CAAC,CA4B5B;AA6JD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,cAAc,CAAC,UAAU,CAAC,EACtC,SAAS,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,SAAS,GAChD,cAAc,CAAC,UAAU,CAAC,CAS5B;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,qBAAqB;IACpC,sBAAsB,EAAE,MAAM,CAAC;IAC/B,YAAY,EAAE,MAAM,CAAC;CACtB;AAqBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAAC,aAAa,EAAE;IAChD,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,cAAc,EAAE,OAAO,CAAA;KAAE,CAAC;IACjE,GAAG,EAAE,OAAO,CAAC;IACb,aAAa,CAAC,EAAE,OAAO,qBAAqB,EAAE,aAAa,CAAC;CAC7D,GAAG,qBAAqB,CA8DxB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timber-js/app",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.16",
|
|
4
4
|
"description": "Vite-native React framework for Cloudflare Workers — correct HTTP semantics, real status codes, pages that work without JavaScript",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare-workers",
|
|
@@ -79,6 +79,11 @@
|
|
|
79
79
|
"publishConfig": {
|
|
80
80
|
"access": "public"
|
|
81
81
|
},
|
|
82
|
+
"scripts": {
|
|
83
|
+
"build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist",
|
|
84
|
+
"typecheck": "tsgo --noEmit",
|
|
85
|
+
"prepublishOnly": "pnpm run build"
|
|
86
|
+
},
|
|
82
87
|
"dependencies": {
|
|
83
88
|
"@opentelemetry/api": "^1.9.0",
|
|
84
89
|
"@opentelemetry/context-async-hooks": "^2.6.0",
|
|
@@ -117,9 +122,5 @@
|
|
|
117
122
|
},
|
|
118
123
|
"engines": {
|
|
119
124
|
"node": ">=20.0.0"
|
|
120
|
-
},
|
|
121
|
-
"scripts": {
|
|
122
|
-
"build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist",
|
|
123
|
-
"typecheck": "tsgo --noEmit"
|
|
124
125
|
}
|
|
125
|
-
}
|
|
126
|
+
}
|
package/src/adapters/nitro.ts
CHANGED
|
@@ -149,6 +149,23 @@ export interface NitroAdapterOptions {
|
|
|
149
149
|
*/
|
|
150
150
|
preset?: NitroPreset;
|
|
151
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Enable application-level gzip compression for HTML and RSC responses.
|
|
154
|
+
*
|
|
155
|
+
* When `true` (default), the origin compresses responses using the Web
|
|
156
|
+
* `CompressionStream` API. This is useful for self-hosted deployments
|
|
157
|
+
* where no reverse proxy or CDN handles compression.
|
|
158
|
+
*
|
|
159
|
+
* Set to `false` when deploying behind a reverse proxy (nginx, caddy)
|
|
160
|
+
* or CDN (Cloudflare, Fastly, Vercel) that compresses at the edge.
|
|
161
|
+
* Disabling origin compression saves CPU on the Node.js event loop —
|
|
162
|
+
* compressing 1MB+ streaming HTML responses takes 10-15ms of main
|
|
163
|
+
* thread time per request, directly reducing throughput under load.
|
|
164
|
+
*
|
|
165
|
+
* @default true
|
|
166
|
+
*/
|
|
167
|
+
compress?: boolean;
|
|
168
|
+
|
|
152
169
|
/**
|
|
153
170
|
* Additional Nitro configuration to merge into the generated config.
|
|
154
171
|
* Overrides default values for the selected preset.
|
|
@@ -176,6 +193,7 @@ export interface NitroAdapterOptions {
|
|
|
176
193
|
*/
|
|
177
194
|
export function nitro(options: NitroAdapterOptions = {}): TimberPlatformAdapter {
|
|
178
195
|
const preset = options.preset ?? 'node-server';
|
|
196
|
+
const compress = options.compress ?? true;
|
|
179
197
|
const presetConfig = PRESET_CONFIGS[preset];
|
|
180
198
|
const pendingPromises: Promise<unknown>[] = [];
|
|
181
199
|
|
|
@@ -229,7 +247,7 @@ export function nitro(options: NitroAdapterOptions = {}): TimberPlatformAdapter
|
|
|
229
247
|
}
|
|
230
248
|
|
|
231
249
|
// Generate the Nitro entry point (imports from ./rsc/ within nitro dir)
|
|
232
|
-
const entry = generateNitroEntry(buildDir, outDir, preset);
|
|
250
|
+
const entry = generateNitroEntry(buildDir, outDir, preset, compress);
|
|
233
251
|
await writeFile(join(outDir, 'entry.ts'), entry);
|
|
234
252
|
|
|
235
253
|
// Run the Nitro build to produce a production-ready server bundle.
|
|
@@ -273,6 +291,7 @@ export function generateNitroEntry(
|
|
|
273
291
|
buildDir: string,
|
|
274
292
|
outDir: string,
|
|
275
293
|
preset: NitroPreset,
|
|
294
|
+
compress = true,
|
|
276
295
|
hasManifestInit = false
|
|
277
296
|
): string {
|
|
278
297
|
// The RSC entry is the main request handler — it exports the fetch handler as default.
|
|
@@ -338,12 +357,14 @@ export function generateNitroEntry(
|
|
|
338
357
|
// Import runWithWaitUntil only when the preset supports it.
|
|
339
358
|
const waitUntilImport = supportsWaitUntil ? ', runWithWaitUntil' : '';
|
|
340
359
|
|
|
360
|
+
const compressImport = compress ? "import { compressResponse } from './_compress.mjs'\n" : '';
|
|
361
|
+
const compressCall = compress ? 'compressResponse(webRequest, webResponse)' : 'webResponse';
|
|
362
|
+
|
|
341
363
|
return `// Generated by @timber-js/app/adapters/nitro
|
|
342
364
|
// Do not edit — this file is regenerated on each build.
|
|
343
365
|
|
|
344
366
|
${manifestImport}import handler, { runWithEarlyHintsSender${waitUntilImport} } from '${serverEntryRelative}'
|
|
345
|
-
|
|
346
|
-
|
|
367
|
+
${compressImport}
|
|
347
368
|
// Set TIMBER_RUNTIME for instrumentation.ts conditional SDK initialization.
|
|
348
369
|
// See design/25-production-deployments.md §"TIMBER_RUNTIME".
|
|
349
370
|
process.env.TIMBER_RUNTIME = '${runtimeName}'
|
|
@@ -357,7 +378,7 @@ export default async function timberHandler(event) {
|
|
|
357
378
|
// h3 v2: event.req is the Web Request
|
|
358
379
|
const webRequest = event.req
|
|
359
380
|
${handlerCall}
|
|
360
|
-
return
|
|
381
|
+
return ${compressCall}
|
|
361
382
|
}
|
|
362
383
|
`;
|
|
363
384
|
}
|
package/src/cli.ts
CHANGED
|
File without changes
|
package/src/index.ts
CHANGED
|
@@ -428,8 +428,13 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
|
|
|
428
428
|
ssr: 'virtual:timber-ssr-entry',
|
|
429
429
|
client: 'virtual:timber-browser-entry',
|
|
430
430
|
},
|
|
431
|
-
//
|
|
432
|
-
//
|
|
431
|
+
// Group all client reference wrappers into a single chunk instead of
|
|
432
|
+
// creating one tiny file per "use client" module. Without this, each
|
|
433
|
+
// server chunk's client references become a separate entry point,
|
|
434
|
+
// producing many sub-500B wrapper files (e.g., 30-byte re-exports).
|
|
435
|
+
// A single group eliminates 10+ unnecessary HTTP requests.
|
|
436
|
+
// See design/27-chunking-strategy.md and TIM-440.
|
|
437
|
+
clientChunks: () => 'client-refs',
|
|
433
438
|
});
|
|
434
439
|
}
|
|
435
440
|
);
|
|
@@ -196,15 +196,25 @@ function createFlightInjectionTransform(
|
|
|
196
196
|
// Once the suffix is stripped, all content is body-level and
|
|
197
197
|
// scripts can safely be drained after any HTML chunk.
|
|
198
198
|
let foundSuffix = false;
|
|
199
|
+
// Set to true in flush() — once all HTML chunks have been emitted,
|
|
200
|
+
// there's no need to yield between RSC reads. This eliminates
|
|
201
|
+
// ~36 macrotask yields per request (18 chunks × 2 yields each)
|
|
202
|
+
// that were the primary source of SSR overhead vs Next.js.
|
|
203
|
+
let htmlStreamFinished = false;
|
|
199
204
|
|
|
200
205
|
// RSC script chunks waiting to be injected at the body level.
|
|
201
206
|
const pending: Uint8Array[] = [];
|
|
202
207
|
|
|
203
208
|
async function pullLoop(): Promise<void> {
|
|
204
|
-
//
|
|
205
|
-
// transform()
|
|
206
|
-
//
|
|
207
|
-
|
|
209
|
+
// Yield once so the first HTML shell chunk flows through
|
|
210
|
+
// transform() before we start reading RSC data. Uses
|
|
211
|
+
// setImmediate (check phase — end of current event loop
|
|
212
|
+
// iteration) instead of setTimeout(0) (timer phase — next
|
|
213
|
+
// iteration). Under concurrency, setTimeout(0) yields to
|
|
214
|
+
// ALL pending timer callbacks from other requests, adding
|
|
215
|
+
// 1-4ms per yield. setImmediate fires before timers.
|
|
216
|
+
// Available on both Node.js and Cloudflare Workers.
|
|
217
|
+
await new Promise<void>((r) => setImmediate(r));
|
|
208
218
|
|
|
209
219
|
try {
|
|
210
220
|
for (;;) {
|
|
@@ -215,10 +225,12 @@ function createFlightInjectionTransform(
|
|
|
215
225
|
}
|
|
216
226
|
pending.push(value);
|
|
217
227
|
// Yield between reads so HTML chunks get a chance to flow
|
|
218
|
-
// through transform() first
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
|
|
228
|
+
// through transform() first — but only while HTML is still
|
|
229
|
+
// streaming. Once flush() fires (all HTML emitted), drain
|
|
230
|
+
// remaining RSC chunks without yielding.
|
|
231
|
+
if (!htmlStreamFinished) {
|
|
232
|
+
await new Promise<void>((r) => setImmediate(r));
|
|
233
|
+
}
|
|
222
234
|
}
|
|
223
235
|
} catch (err) {
|
|
224
236
|
pullError = err;
|
|
@@ -240,7 +252,11 @@ function createFlightInjectionTransform(
|
|
|
240
252
|
|
|
241
253
|
return new TransformStream<Uint8Array, Uint8Array>({
|
|
242
254
|
transform(chunk, controller) {
|
|
243
|
-
//
|
|
255
|
+
// Pull-based start: don't begin reading RSC until the first
|
|
256
|
+
// HTML chunk flows through. This matches Next.js's approach
|
|
257
|
+
// and ensures the shell HTML is enqueued before any RSC
|
|
258
|
+
// script tags. Without this, the pull loop starts eagerly
|
|
259
|
+
// and may read RSC data before the browser has any HTML.
|
|
244
260
|
if (!pullPromise) {
|
|
245
261
|
pullPromise = pullLoop();
|
|
246
262
|
}
|
|
@@ -274,7 +290,11 @@ function createFlightInjectionTransform(
|
|
|
274
290
|
}
|
|
275
291
|
},
|
|
276
292
|
flush(controller) {
|
|
277
|
-
// HTML
|
|
293
|
+
// All HTML chunks have been emitted. Signal the pull loop to
|
|
294
|
+
// stop yielding between RSC reads — no more HTML to interleave.
|
|
295
|
+
htmlStreamFinished = true;
|
|
296
|
+
|
|
297
|
+
// Drain remaining RSC chunks at body level
|
|
278
298
|
const finish = () => {
|
|
279
299
|
drainPending(controller);
|
|
280
300
|
// Re-emit the suffix at the very end so HTML is well-formed
|
package/LICENSE
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
DONTFUCKINGUSE LICENSE
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Daniel Saewitz
|
|
4
|
-
|
|
5
|
-
This software may not be used, copied, modified, merged, published,
|
|
6
|
-
distributed, sublicensed, or sold by anyone other than the copyright holder.
|
|
7
|
-
|
|
8
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
|