@stacksjs/stx 0.2.169 → 0.2.171

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.
@@ -30,7 +30,8 @@ export function resolveBundlerFallback() {
30
30
  if (_fallbackOverride)
31
31
  return _fallbackOverride;
32
32
  try {
33
- return config.strict?.bundlerFallback ?? "warn";
33
+ const strict = config.strict;
34
+ return typeof strict === "object" && strict !== null ? strict.bundlerFallback ?? "warn" : "warn";
34
35
  } catch {
35
36
  return "warn";
36
37
  }
@@ -32,6 +32,7 @@
32
32
  * @module client-script
33
33
  */
34
34
  import type { ParsedEvent } from './events';
35
+ export { findInterpolationEnd, stripCommentsAndLiterals } from './strip-literals';
35
36
  export interface VendorCssImport {
36
37
  /** The original specifier as written in the import — kept verbatim for the data attribute. */
37
38
  source: string;
@@ -117,18 +118,6 @@ export interface BrowserCoreAutoImportResult {
117
118
  * helpers together with the component, exactly like an explicit user import.
118
119
  */
119
120
  export declare function injectBrowserCoreAutoImports(code: string): BrowserCoreAutoImportResult;
120
- /**
121
- * Blank out comments and string literals, preserving length and line structure
122
- * so offsets still line up, so that identifier detection only ever sees code.
123
- *
124
- * Template-literal INTERPOLATIONS are kept, because `${...}` is code, not text.
125
- * Blanking them hid every identifier that appears only inside one — a helper
126
- * called as `${formatDate(d)}` looked unused to the auto-import scanner, and a
127
- * server value read as `` `/api/users/${userId}` `` looked unreferenced to the
128
- * data bridge, which would have withheld it and left the client throwing on an
129
- * undefined name.
130
- */
131
- export declare function stripCommentsAndLiterals(code: string): string;
132
121
  export interface ClientScriptOptions {
133
122
  /** Event bindings collected from template @event attributes */
134
123
  eventBindings?: ParsedEvent[];
@@ -2,6 +2,8 @@ import path from "node:path";
2
2
  import fs from "node:fs";
3
3
  import { asInvocableStatement } from "./events.js";
4
4
  import { transformStoreImports } from "./store-imports.js";
5
+ import { findInterpolationEnd, stripCommentsAndLiterals } from "./strip-literals.js";
6
+ export { findInterpolationEnd, stripCommentsAndLiterals } from "./strip-literals.js";
5
7
  import { shouldTranspileTypeScript, transpileTypeScript } from "./utils.js";
6
8
  import { importOnce } from "./lazy-module.js";
7
9
  import { STX_RUNTIME_GLOBALS, usesReactiveRuntime } from "./runtime-globals.js";
@@ -263,155 +265,6 @@ const JS_BUILTINS = new Set([
263
265
  "Modal",
264
266
  "Intl"
265
267
  ]);
266
- function skipQuoted(code, start) {
267
- const quote = code[start];
268
- let i = start + 1;
269
- while (i < code.length) {
270
- if (code[i] === "\\") {
271
- i += 2;
272
- continue;
273
- }
274
- if (code[i] === quote)
275
- return i + 1;
276
- i++;
277
- }
278
- return code.length;
279
- }
280
- function skipTemplate(code, start) {
281
- let i = start + 1;
282
- while (i < code.length) {
283
- if (code[i] === "\\") {
284
- i += 2;
285
- continue;
286
- }
287
- if (code[i] === "`")
288
- return i + 1;
289
- if (code[i] === "$" && code[i + 1] === "{") {
290
- i = findInterpolationEnd(code, i + 2);
291
- if (i < code.length)
292
- i++;
293
- continue;
294
- }
295
- i++;
296
- }
297
- return code.length;
298
- }
299
- function findInterpolationEnd(code, start) {
300
- let depth = 1, i = start;
301
- while (i < code.length) {
302
- const ch = code[i];
303
- if (ch === "\\") {
304
- i += 2;
305
- continue;
306
- }
307
- if (ch === '"' || ch === "'") {
308
- i = skipQuoted(code, i);
309
- continue;
310
- }
311
- if (ch === "`") {
312
- i = skipTemplate(code, i);
313
- continue;
314
- }
315
- if (ch === "/" && code[i + 1] === "/") {
316
- while (i < code.length && code[i] !== `
317
- `)
318
- i++;
319
- continue;
320
- }
321
- if (ch === "/" && code[i + 1] === "*") {
322
- i += 2;
323
- while (i < code.length && code.slice(i, i + 2) !== "*/")
324
- i++;
325
- i += 2;
326
- continue;
327
- }
328
- if (ch === "{") {
329
- depth++;
330
- i++;
331
- continue;
332
- }
333
- if (ch === "}") {
334
- depth--;
335
- if (depth === 0)
336
- return i;
337
- i++;
338
- continue;
339
- }
340
- i++;
341
- }
342
- return code.length;
343
- }
344
- export function stripCommentsAndLiterals(code) {
345
- let out = "", i = 0;
346
- while (i < code.length) {
347
- const two = code.slice(i, i + 2);
348
- if (two === "//") {
349
- while (i < code.length && code[i] !== `
350
- `)
351
- out += " ", i++;
352
- continue;
353
- }
354
- if (two === "/*") {
355
- while (i < code.length && code.slice(i, i + 2) !== "*/")
356
- out += code[i] === `
357
- ` ? `
358
- ` : " ", i++;
359
- out += " ";
360
- i += 2;
361
- continue;
362
- }
363
- const ch = code[i];
364
- if (ch === '"' || ch === "'") {
365
- const quote = ch;
366
- out += " ";
367
- i++;
368
- while (i < code.length && code[i] !== quote) {
369
- if (code[i] === "\\") {
370
- out += " ";
371
- i += 2;
372
- continue;
373
- }
374
- out += code[i] === `
375
- ` ? `
376
- ` : " ";
377
- i++;
378
- }
379
- out += " ";
380
- i++;
381
- continue;
382
- }
383
- if (ch === "`") {
384
- out += " ";
385
- i++;
386
- while (i < code.length && code[i] !== "`") {
387
- if (code[i] === "\\") {
388
- out += " ";
389
- i += 2;
390
- continue;
391
- }
392
- if (code[i] === "$" && code[i + 1] === "{") {
393
- const bodyStart = i + 2, bodyEnd = findInterpolationEnd(code, bodyStart);
394
- out += " ";
395
- out += stripCommentsAndLiterals(code.slice(bodyStart, bodyEnd));
396
- if (bodyEnd < code.length)
397
- out += " ";
398
- i = bodyEnd + 1;
399
- continue;
400
- }
401
- out += code[i] === `
402
- ` ? `
403
- ` : " ";
404
- i++;
405
- }
406
- out += " ";
407
- i++;
408
- continue;
409
- }
410
- out += ch;
411
- i++;
412
- }
413
- return out;
414
- }
415
268
  function detectModelUsage(code) {
416
269
  const models = new Set, modelPattern = /\b([A-Z][a-zA-Z0-9]*)\s*\.\s*(all|find|first|get|where|orderBy|orderByDesc|limit|select|pluck|create|update|delete)\s*\(/g;
417
270
  let match;
package/dist/codemod.js CHANGED
@@ -87,7 +87,12 @@ function isInsideAsyncFunction(source, index) {
87
87
  const window = source.slice(Math.max(0, boundary - 120), boundary);
88
88
  return /\basync\b[^;{}]*$/.test(window);
89
89
  }
90
+ function declaresOwnConfirm(source) {
91
+ return /\b(?:async\s+)?function\s+confirm\s*\(/.test(source) || /\b(?:const|let|var)\s+confirm\s*=/.test(source) || /\bimport\s*\{[^}]*\bconfirm\b[^}]*\}/.test(source);
92
+ }
90
93
  function applyConfirmRule(source, file, findings) {
94
+ if (declaresOwnConfirm(source))
95
+ return source;
91
96
  const pattern = /(^|[^\w$.])confirm\s*\(/g;
92
97
  let out = "", last = 0, match;
93
98
  while ((match = pattern.exec(source)) !== null) {
@@ -3,10 +3,17 @@ export declare function isCompressible(contentType: string | null): boolean;
3
3
  /**
4
4
  * The best encoding both sides support, or `null` for none.
5
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.
6
+ * This used to be `accepted.includes('br')` on the raw header, on the argument
7
+ * that q-values are only ever sent by testing tools. The substring is the
8
+ * problem: `br;q=0` contains `br`, and it is the one string in the header that
9
+ * means *do not send me brotli*. Answering that with a brotli body is not a
10
+ * missed optimisation, it is a response the client cannot read — the failure
11
+ * this function's `Vary` sibling exists to prevent, arrived at from the other
12
+ * direction.
13
+ *
14
+ * Brotli is still preferred when both are genuinely on offer: ~20% smaller
15
+ * than gzip on HTML at comparable cost, and every browser without it predates
16
+ * 2017.
10
17
  */
11
18
  export declare function negotiateEncoding(acceptEncoding: string | null): 'br' | 'gzip' | null;
12
19
  /**
@@ -1,5 +1,6 @@
1
1
  import zlib from "node:zlib";
2
- const COMPRESSIBLE = [
2
+ import { promisify } from "node:util";
3
+ const brotliCompress = promisify(zlib.brotliCompress), gzipCompress = promisify(zlib.gzip), COMPRESSIBLE = [
3
4
  "text/",
4
5
  "application/json",
5
6
  "application/javascript",
@@ -9,20 +10,44 @@ const COMPRESSIBLE = [
9
10
  "application/atom+xml",
10
11
  "application/manifest+json",
11
12
  "image/svg+xml"
13
+ ], NEVER_BUFFER = [
14
+ "text/event-stream",
15
+ "multipart/x-mixed-replace"
12
16
  ], MIN_BYTES = 1024;
13
17
  export function isCompressible(contentType) {
14
18
  if (!contentType)
15
19
  return !1;
16
20
  const type = contentType.toLowerCase();
21
+ if (NEVER_BUFFER.some((prefix) => type.startsWith(prefix)))
22
+ return !1;
17
23
  return COMPRESSIBLE.some((prefix) => type.startsWith(prefix));
18
24
  }
25
+ function parseAcceptEncoding(header) {
26
+ const weights = new Map;
27
+ for (const part of header.split(",")) {
28
+ const [rawName, ...params] = part.trim().split(";"), name = rawName.trim().toLowerCase();
29
+ if (!name)
30
+ continue;
31
+ let quality = 1;
32
+ for (const param of params) {
33
+ const match = /^\s*q\s*=\s*([\d.]+)\s*$/i.exec(param);
34
+ if (!match)
35
+ continue;
36
+ const parsed = Number.parseFloat(match[1]);
37
+ if (Number.isFinite(parsed))
38
+ quality = parsed;
39
+ }
40
+ weights.set(name, quality);
41
+ }
42
+ return weights;
43
+ }
19
44
  export function negotiateEncoding(acceptEncoding) {
20
45
  if (!acceptEncoding)
21
46
  return null;
22
- const accepted = acceptEncoding.toLowerCase();
23
- if (accepted.includes("br"))
47
+ const weights = parseAcceptEncoding(acceptEncoding), wildcard = weights.get("*"), quality = (coding) => weights.get(coding) ?? wildcard ?? 0;
48
+ if (quality("br") > 0)
24
49
  return "br";
25
- if (accepted.includes("gzip"))
50
+ if (quality("gzip") > 0)
26
51
  return "gzip";
27
52
  return null;
28
53
  }
@@ -36,28 +61,47 @@ export async function compressResponse(request, response, options = {}) {
36
61
  const encoding = negotiateEncoding(request.headers.get("accept-encoding"));
37
62
  if (!encoding)
38
63
  return response;
64
+ let body;
65
+ try {
66
+ body = new Uint8Array(await response.arrayBuffer());
67
+ } catch {
68
+ return response;
69
+ }
70
+ const passthrough = () => new Response(body, {
71
+ status: response.status,
72
+ statusText: response.statusText,
73
+ headers: varyByAcceptEncoding(response.headers)
74
+ });
75
+ if (body.length < (options.threshold ?? MIN_BYTES))
76
+ return passthrough();
39
77
  try {
40
- const body = Buffer.from(await response.clone().arrayBuffer());
41
- if (body.length < (options.threshold ?? MIN_BYTES))
42
- return response;
43
- const compressed = encoding === "br" ? zlib.brotliCompressSync(body, {
78
+ const compressed = encoding === "br" ? await brotliCompress(body, {
44
79
  params: {
45
80
  [zlib.constants.BROTLI_PARAM_QUALITY]: options.quality ?? 5,
46
81
  [zlib.constants.BROTLI_PARAM_SIZE_HINT]: body.length
47
82
  }
48
- }) : zlib.gzipSync(body);
83
+ }) : await gzipCompress(body);
49
84
  if (compressed.length >= body.length)
50
- return response;
51
- const headers = new Headers(response.headers);
85
+ return passthrough();
86
+ const headers = varyByAcceptEncoding(response.headers);
52
87
  headers.set("Content-Encoding", encoding);
53
88
  headers.set("Content-Length", String(compressed.length));
54
- headers.append("Vary", "Accept-Encoding");
55
89
  return new Response(compressed, {
56
90
  status: response.status,
57
91
  statusText: response.statusText,
58
92
  headers
59
93
  });
60
94
  } catch {
61
- return response;
95
+ return passthrough();
96
+ }
97
+ }
98
+ function varyByAcceptEncoding(source) {
99
+ const headers = new Headers(source), existing = headers.get("Vary");
100
+ if (!existing) {
101
+ headers.set("Vary", "Accept-Encoding");
102
+ return headers;
62
103
  }
104
+ if (!existing.split(",").some((field) => field.trim().toLowerCase() === "accept-encoding"))
105
+ headers.set("Vary", `${existing}, Accept-Encoding`);
106
+ return headers;
63
107
  }