@stacksjs/stx 0.2.168 → 0.2.170

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.
@@ -0,0 +1,23 @@
1
+ /** Whether this content type is worth compressing. */
2
+ export declare function isCompressible(contentType: string | null): boolean;
3
+ /**
4
+ * The best encoding both sides support, or `null` for none.
5
+ *
6
+ * Deliberately ignores the q-values in `Accept-Encoding`. Honouring them means
7
+ * parsing a header that is, in practice, one of about four fixed strings every
8
+ * real browser sends, all of which accept both encodings; the only clients
9
+ * that weight them are testing tools.
10
+ */
11
+ export declare function negotiateEncoding(acceptEncoding: string | null): 'br' | 'gzip' | null;
12
+ /**
13
+ * Return `response` compressed, or unchanged when it should not be.
14
+ *
15
+ * Never throws and never drops a body: a failed compress logs nothing and
16
+ * returns the original, because shipping bytes uncompressed is a slower
17
+ * success and a broken compress step is an outage.
18
+ */
19
+ export declare function compressResponse(request: Request, response: Response, options?: CompressionOptions): Promise<Response>;
20
+ export declare interface CompressionOptions {
21
+ threshold?: number
22
+ quality?: number
23
+ }
@@ -0,0 +1,68 @@
1
+ import zlib from "node:zlib";
2
+ const COMPRESSIBLE = [
3
+ "text/",
4
+ "application/json",
5
+ "application/javascript",
6
+ "application/xml",
7
+ "application/xhtml+xml",
8
+ "application/rss+xml",
9
+ "application/atom+xml",
10
+ "application/manifest+json",
11
+ "image/svg+xml"
12
+ ], NEVER_BUFFER = [
13
+ "text/event-stream",
14
+ "multipart/x-mixed-replace"
15
+ ], MIN_BYTES = 1024;
16
+ export function isCompressible(contentType) {
17
+ if (!contentType)
18
+ return !1;
19
+ const type = contentType.toLowerCase();
20
+ if (NEVER_BUFFER.some((prefix) => type.startsWith(prefix)))
21
+ return !1;
22
+ return COMPRESSIBLE.some((prefix) => type.startsWith(prefix));
23
+ }
24
+ export function negotiateEncoding(acceptEncoding) {
25
+ if (!acceptEncoding)
26
+ return null;
27
+ const accepted = acceptEncoding.toLowerCase();
28
+ if (accepted.includes("br"))
29
+ return "br";
30
+ if (accepted.includes("gzip"))
31
+ return "gzip";
32
+ return null;
33
+ }
34
+ export async function compressResponse(request, response, options = {}) {
35
+ if (response.headers.get("content-encoding"))
36
+ return response;
37
+ if (response.status === 204 || response.status === 304 || !response.body)
38
+ return response;
39
+ if (!isCompressible(response.headers.get("content-type")))
40
+ return response;
41
+ const encoding = negotiateEncoding(request.headers.get("accept-encoding"));
42
+ if (!encoding)
43
+ return response;
44
+ try {
45
+ const body = Buffer.from(await response.clone().arrayBuffer());
46
+ if (body.length < (options.threshold ?? MIN_BYTES))
47
+ return response;
48
+ const compressed = encoding === "br" ? zlib.brotliCompressSync(body, {
49
+ params: {
50
+ [zlib.constants.BROTLI_PARAM_QUALITY]: options.quality ?? 5,
51
+ [zlib.constants.BROTLI_PARAM_SIZE_HINT]: body.length
52
+ }
53
+ }) : zlib.gzipSync(body);
54
+ if (compressed.length >= body.length)
55
+ return response;
56
+ const headers = new Headers(response.headers);
57
+ headers.set("Content-Encoding", encoding);
58
+ headers.set("Content-Length", String(compressed.length));
59
+ headers.append("Vary", "Accept-Encoding");
60
+ return new Response(compressed, {
61
+ status: response.status,
62
+ statusText: response.statusText,
63
+ headers
64
+ });
65
+ } catch {
66
+ return response;
67
+ }
68
+ }
@@ -24,6 +24,7 @@ import {
24
24
  setupKeyboardShortcuts
25
25
  } from "./index.js";
26
26
  import { serveMarkdownFile } from "./serve-markdown.js";
27
+ import { compressResponse } from "../compression.js";
27
28
  export async function serveStxFile(filePath, options = {}) {
28
29
  const port = options.port || 3000, watch = options.watch !== !1, hotReload = options.hotReload !== !1 && watch;
29
30
  let hmrServer = null, actualHmrPort = options.hmrPort || port + 1;
@@ -135,241 +136,243 @@ export async function serveStxFile(filePath, options = {}) {
135
136
  const server = serve({
136
137
  port: actualPort,
137
138
  async fetch(request) {
138
- const url = new URL(request.url);
139
- if (customRouter)
140
- try {
141
- const routerResponse = await customRouter.handleRequest(request);
142
- if (routerResponse.status !== 404)
143
- return routerResponse;
144
- } catch {}
145
- if (apiRoutes[url.pathname])
146
- try {
147
- return await apiRoutes[url.pathname](request);
148
- } catch (err) {
149
- return new Response(JSON.stringify({ error: err.message }), {
150
- status: 500,
151
- headers: { "Content-Type": "application/json" }
152
- });
153
- }
154
- if (url.pathname === "/")
155
- return (async () => {
156
- let content = await injectCrosswindCSS(htmlContent || "");
157
- if (hotReload)
158
- content = injectHotReload(content, actualHmrPort);
159
- if (options.native) {
160
- const nativeSidebarInjection = "<style>[data-stx-sidebar],.stx-sidebar{display:none!important}</style><script>window.__craftNativeSidebar=true;document.documentElement.classList.add('has-native-sidebar')</script>";
161
- if (content.includes("<head>"))
162
- content = content.replace("<head>", `<head>${nativeSidebarInjection}`);
163
- else if (content.includes("<html"))
164
- content = content.replace(/(<html[^>]*>)/, `$1<head>${nativeSidebarInjection}</head>`);
165
- else
166
- content = `<head>${nativeSidebarInjection}</head>` + content;
167
- }
168
- if (options.titlebarHidden) {
169
- const tbInjection = "<style>html.stx-titlebar-hidden .stx-traffic-lights-dot{visibility:hidden}</style><script>document.documentElement.classList.add('stx-titlebar-hidden')</script>";
170
- if (content.includes("<head>"))
171
- content = content.replace("<head>", `<head>${tbInjection}`);
172
- else if (content.includes("<html"))
173
- content = content.replace(/(<html[^>]*>)/, `$1<head>${tbInjection}</head>`);
174
- else
175
- content = `<head>${tbInjection}</head>${content}`;
176
- }
177
- return content;
178
- })().then((content) => new Response(content, {
179
- headers: {
180
- "Content-Type": "text/html",
181
- "Cache-Control": "no-store, no-cache, must-revalidate",
182
- Pragma: "no-cache",
183
- Expires: "0"
139
+ return compressResponse(request, await (async () => {
140
+ const url = new URL(request.url);
141
+ if (customRouter)
142
+ try {
143
+ const routerResponse = await customRouter.handleRequest(request);
144
+ if (routerResponse.status !== 404)
145
+ return routerResponse;
146
+ } catch {}
147
+ if (apiRoutes[url.pathname])
148
+ try {
149
+ return await apiRoutes[url.pathname](request);
150
+ } catch (err) {
151
+ return new Response(JSON.stringify({ error: err.message }), {
152
+ status: 500,
153
+ headers: { "Content-Type": "application/json" }
154
+ });
184
155
  }
185
- }));
186
- const requestedPath = path.join(outputDir, url.pathname);
187
- if (fs.existsSync(requestedPath) && fs.statSync(requestedPath).isFile()) {
188
- const file = Bun.file(requestedPath), ext = path.extname(requestedPath).toLowerCase();
189
- let contentType = "text/plain";
190
- if (ext === ".ts" || ext === ".tsx") {
191
- const transpiler = new Bun.Transpiler({ loader: ext === ".tsx" ? "tsx" : "ts" }), code = await file.text(), js = transpiler.transformSync(code);
192
- return new Response(js, {
193
- headers: {
194
- "Content-Type": "application/javascript",
195
- "Cache-Control": "no-store, no-cache, must-revalidate",
196
- Pragma: "no-cache",
197
- Expires: "0"
156
+ if (url.pathname === "/")
157
+ return (async () => {
158
+ let content = await injectCrosswindCSS(htmlContent || "");
159
+ if (hotReload)
160
+ content = injectHotReload(content, actualHmrPort);
161
+ if (options.native) {
162
+ const nativeSidebarInjection = "<style>[data-stx-sidebar],.stx-sidebar{display:none!important}</style><script>window.__craftNativeSidebar=true;document.documentElement.classList.add('has-native-sidebar')</script>";
163
+ if (content.includes("<head>"))
164
+ content = content.replace("<head>", `<head>${nativeSidebarInjection}`);
165
+ else if (content.includes("<html"))
166
+ content = content.replace(/(<html[^>]*>)/, `$1<head>${nativeSidebarInjection}</head>`);
167
+ else
168
+ content = `<head>${nativeSidebarInjection}</head>` + content;
198
169
  }
199
- });
200
- }
201
- switch (ext) {
202
- case ".html":
203
- contentType = "text/html";
204
- break;
205
- case ".css":
206
- contentType = "text/css";
207
- break;
208
- case ".js":
209
- contentType = "text/javascript";
210
- break;
211
- case ".json":
212
- contentType = "application/json";
213
- break;
214
- case ".png":
215
- contentType = "image/png";
216
- break;
217
- case ".jpg":
218
- case ".jpeg":
219
- contentType = "image/jpeg";
220
- break;
221
- case ".gif":
222
- contentType = "image/gif";
223
- break;
224
- case ".woff":
225
- contentType = "font/woff";
226
- break;
227
- case ".woff2":
228
- contentType = "font/woff2";
229
- break;
230
- case ".ttf":
231
- contentType = "font/ttf";
232
- break;
233
- case ".otf":
234
- contentType = "font/otf";
235
- break;
236
- case ".eot":
237
- contentType = "application/vnd.ms-fontobject";
238
- break;
239
- }
240
- return new Response(file, {
241
- headers: { "Content-Type": contentType }
242
- });
243
- }
244
- const sourceDir = path.dirname(absolutePath), sourcePath = path.join(sourceDir, url.pathname);
245
- if (fs.existsSync(sourcePath) && fs.statSync(sourcePath).isFile()) {
246
- const file = Bun.file(sourcePath), ext = path.extname(sourcePath).toLowerCase();
247
- let contentType = "text/plain";
248
- if (ext === ".ts" || ext === ".tsx") {
249
- const transpiler = new Bun.Transpiler({ loader: ext === ".tsx" ? "tsx" : "ts" }), code = await file.text(), js = transpiler.transformSync(code);
250
- return new Response(js, {
170
+ if (options.titlebarHidden) {
171
+ const tbInjection = "<style>html.stx-titlebar-hidden .stx-traffic-lights-dot{visibility:hidden}</style><script>document.documentElement.classList.add('stx-titlebar-hidden')</script>";
172
+ if (content.includes("<head>"))
173
+ content = content.replace("<head>", `<head>${tbInjection}`);
174
+ else if (content.includes("<html"))
175
+ content = content.replace(/(<html[^>]*>)/, `$1<head>${tbInjection}</head>`);
176
+ else
177
+ content = `<head>${tbInjection}</head>${content}`;
178
+ }
179
+ return content;
180
+ })().then((content) => new Response(content, {
251
181
  headers: {
252
- "Content-Type": "application/javascript",
182
+ "Content-Type": "text/html",
253
183
  "Cache-Control": "no-store, no-cache, must-revalidate",
254
184
  Pragma: "no-cache",
255
185
  Expires: "0"
256
186
  }
187
+ }));
188
+ const requestedPath = path.join(outputDir, url.pathname);
189
+ if (fs.existsSync(requestedPath) && fs.statSync(requestedPath).isFile()) {
190
+ const file = Bun.file(requestedPath), ext = path.extname(requestedPath).toLowerCase();
191
+ let contentType = "text/plain";
192
+ if (ext === ".ts" || ext === ".tsx") {
193
+ const transpiler = new Bun.Transpiler({ loader: ext === ".tsx" ? "tsx" : "ts" }), code = await file.text(), js = transpiler.transformSync(code);
194
+ return new Response(js, {
195
+ headers: {
196
+ "Content-Type": "application/javascript",
197
+ "Cache-Control": "no-store, no-cache, must-revalidate",
198
+ Pragma: "no-cache",
199
+ Expires: "0"
200
+ }
201
+ });
202
+ }
203
+ switch (ext) {
204
+ case ".html":
205
+ contentType = "text/html";
206
+ break;
207
+ case ".css":
208
+ contentType = "text/css";
209
+ break;
210
+ case ".js":
211
+ contentType = "text/javascript";
212
+ break;
213
+ case ".json":
214
+ contentType = "application/json";
215
+ break;
216
+ case ".png":
217
+ contentType = "image/png";
218
+ break;
219
+ case ".jpg":
220
+ case ".jpeg":
221
+ contentType = "image/jpeg";
222
+ break;
223
+ case ".gif":
224
+ contentType = "image/gif";
225
+ break;
226
+ case ".woff":
227
+ contentType = "font/woff";
228
+ break;
229
+ case ".woff2":
230
+ contentType = "font/woff2";
231
+ break;
232
+ case ".ttf":
233
+ contentType = "font/ttf";
234
+ break;
235
+ case ".otf":
236
+ contentType = "font/otf";
237
+ break;
238
+ case ".eot":
239
+ contentType = "application/vnd.ms-fontobject";
240
+ break;
241
+ }
242
+ return new Response(file, {
243
+ headers: { "Content-Type": contentType }
257
244
  });
258
245
  }
259
- switch (ext) {
260
- case ".html":
261
- contentType = "text/html";
262
- break;
263
- case ".css":
264
- contentType = "text/css";
265
- break;
266
- case ".js":
267
- contentType = "text/javascript";
268
- break;
269
- case ".json":
270
- contentType = "application/json";
271
- break;
272
- case ".png":
273
- contentType = "image/png";
274
- break;
275
- case ".jpg":
276
- case ".jpeg":
277
- contentType = "image/jpeg";
278
- break;
279
- case ".gif":
280
- contentType = "image/gif";
281
- break;
282
- case ".svg":
283
- contentType = "image/svg+xml";
284
- break;
285
- case ".ico":
286
- contentType = "image/x-icon";
287
- break;
288
- case ".woff":
289
- contentType = "font/woff";
290
- break;
291
- case ".woff2":
292
- contentType = "font/woff2";
293
- break;
294
- case ".ttf":
295
- contentType = "font/ttf";
296
- break;
297
- case ".otf":
298
- contentType = "font/otf";
299
- break;
300
- case ".eot":
301
- contentType = "application/vnd.ms-fontobject";
302
- break;
303
- }
304
- return new Response(file, {
305
- headers: { "Content-Type": contentType }
306
- });
307
- }
308
- const publicPath = path.join(process.cwd(), "public", url.pathname);
309
- if (fs.existsSync(publicPath) && fs.statSync(publicPath).isFile()) {
310
- const file = Bun.file(publicPath), ext = path.extname(publicPath).toLowerCase();
311
- let contentType = "application/octet-stream";
312
- if (ext === ".ts" || ext === ".tsx") {
313
- const transpiler = new Bun.Transpiler({ loader: ext === ".tsx" ? "tsx" : "ts" }), code = await file.text(), js = transpiler.transformSync(code);
314
- return new Response(js, {
315
- headers: {
316
- "Content-Type": "application/javascript",
317
- "Cache-Control": "no-store, no-cache, must-revalidate",
318
- Pragma: "no-cache",
319
- Expires: "0"
320
- }
246
+ const sourceDir = path.dirname(absolutePath), sourcePath = path.join(sourceDir, url.pathname);
247
+ if (fs.existsSync(sourcePath) && fs.statSync(sourcePath).isFile()) {
248
+ const file = Bun.file(sourcePath), ext = path.extname(sourcePath).toLowerCase();
249
+ let contentType = "text/plain";
250
+ if (ext === ".ts" || ext === ".tsx") {
251
+ const transpiler = new Bun.Transpiler({ loader: ext === ".tsx" ? "tsx" : "ts" }), code = await file.text(), js = transpiler.transformSync(code);
252
+ return new Response(js, {
253
+ headers: {
254
+ "Content-Type": "application/javascript",
255
+ "Cache-Control": "no-store, no-cache, must-revalidate",
256
+ Pragma: "no-cache",
257
+ Expires: "0"
258
+ }
259
+ });
260
+ }
261
+ switch (ext) {
262
+ case ".html":
263
+ contentType = "text/html";
264
+ break;
265
+ case ".css":
266
+ contentType = "text/css";
267
+ break;
268
+ case ".js":
269
+ contentType = "text/javascript";
270
+ break;
271
+ case ".json":
272
+ contentType = "application/json";
273
+ break;
274
+ case ".png":
275
+ contentType = "image/png";
276
+ break;
277
+ case ".jpg":
278
+ case ".jpeg":
279
+ contentType = "image/jpeg";
280
+ break;
281
+ case ".gif":
282
+ contentType = "image/gif";
283
+ break;
284
+ case ".svg":
285
+ contentType = "image/svg+xml";
286
+ break;
287
+ case ".ico":
288
+ contentType = "image/x-icon";
289
+ break;
290
+ case ".woff":
291
+ contentType = "font/woff";
292
+ break;
293
+ case ".woff2":
294
+ contentType = "font/woff2";
295
+ break;
296
+ case ".ttf":
297
+ contentType = "font/ttf";
298
+ break;
299
+ case ".otf":
300
+ contentType = "font/otf";
301
+ break;
302
+ case ".eot":
303
+ contentType = "application/vnd.ms-fontobject";
304
+ break;
305
+ }
306
+ return new Response(file, {
307
+ headers: { "Content-Type": contentType }
321
308
  });
322
309
  }
323
- switch (ext) {
324
- case ".html":
325
- contentType = "text/html";
326
- break;
327
- case ".css":
328
- contentType = "text/css";
329
- break;
330
- case ".js":
331
- contentType = "text/javascript";
332
- break;
333
- case ".json":
334
- contentType = "application/json";
335
- break;
336
- case ".png":
337
- contentType = "image/png";
338
- break;
339
- case ".jpg":
340
- case ".jpeg":
341
- contentType = "image/jpeg";
342
- break;
343
- case ".gif":
344
- contentType = "image/gif";
345
- break;
346
- case ".svg":
347
- contentType = "image/svg+xml";
348
- break;
349
- case ".ico":
350
- contentType = "image/x-icon";
351
- break;
352
- case ".woff":
353
- contentType = "font/woff";
354
- break;
355
- case ".woff2":
356
- contentType = "font/woff2";
357
- break;
358
- case ".ttf":
359
- contentType = "font/ttf";
360
- break;
361
- case ".otf":
362
- contentType = "font/otf";
363
- break;
364
- case ".eot":
365
- contentType = "application/vnd.ms-fontobject";
366
- break;
310
+ const publicPath = path.join(process.cwd(), "public", url.pathname);
311
+ if (fs.existsSync(publicPath) && fs.statSync(publicPath).isFile()) {
312
+ const file = Bun.file(publicPath), ext = path.extname(publicPath).toLowerCase();
313
+ let contentType = "application/octet-stream";
314
+ if (ext === ".ts" || ext === ".tsx") {
315
+ const transpiler = new Bun.Transpiler({ loader: ext === ".tsx" ? "tsx" : "ts" }), code = await file.text(), js = transpiler.transformSync(code);
316
+ return new Response(js, {
317
+ headers: {
318
+ "Content-Type": "application/javascript",
319
+ "Cache-Control": "no-store, no-cache, must-revalidate",
320
+ Pragma: "no-cache",
321
+ Expires: "0"
322
+ }
323
+ });
324
+ }
325
+ switch (ext) {
326
+ case ".html":
327
+ contentType = "text/html";
328
+ break;
329
+ case ".css":
330
+ contentType = "text/css";
331
+ break;
332
+ case ".js":
333
+ contentType = "text/javascript";
334
+ break;
335
+ case ".json":
336
+ contentType = "application/json";
337
+ break;
338
+ case ".png":
339
+ contentType = "image/png";
340
+ break;
341
+ case ".jpg":
342
+ case ".jpeg":
343
+ contentType = "image/jpeg";
344
+ break;
345
+ case ".gif":
346
+ contentType = "image/gif";
347
+ break;
348
+ case ".svg":
349
+ contentType = "image/svg+xml";
350
+ break;
351
+ case ".ico":
352
+ contentType = "image/x-icon";
353
+ break;
354
+ case ".woff":
355
+ contentType = "font/woff";
356
+ break;
357
+ case ".woff2":
358
+ contentType = "font/woff2";
359
+ break;
360
+ case ".ttf":
361
+ contentType = "font/ttf";
362
+ break;
363
+ case ".otf":
364
+ contentType = "font/otf";
365
+ break;
366
+ case ".eot":
367
+ contentType = "application/vnd.ms-fontobject";
368
+ break;
369
+ }
370
+ return new Response(file, {
371
+ headers: { "Content-Type": contentType }
372
+ });
367
373
  }
368
- return new Response(file, {
369
- headers: { "Content-Type": contentType }
370
- });
371
- }
372
- return new Response("Not Found", { status: 404 });
374
+ return new Response("Not Found", { status: 404 });
375
+ })());
373
376
  },
374
377
  error(error) {
375
378
  return new Response(`<pre>${error}
package/dist/index.d.ts CHANGED
@@ -129,6 +129,7 @@ export * from './custom-directives';
129
129
  // database moved to stx/database
130
130
  export * from './defer';
131
131
  export * from './deploy/index';
132
+ export * from './compression';
132
133
  export * from './dev-server';
133
134
  export * from './document-shell';
134
135
  export * from './fragment-container';
package/dist/index.js CHANGED
@@ -55,6 +55,7 @@ export * from "./csp.js";
55
55
  export * from "./custom-directives.js";
56
56
  export * from "./defer.js";
57
57
  export * from "./deploy/index.js";
58
+ export * from "./compression.js";
58
59
  export * from "./dev-server.js";
59
60
  export * from "./document-shell.js";
60
61
  export * from "./fragment-container.js";