@webjsdev/server 0.8.81 → 0.8.82

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/server",
3
- "version": "0.8.81",
3
+ "version": "0.8.82",
4
4
  "type": "module",
5
5
  "description": "The server for WebJs, a full-stack JavaScript framework built on web components with server-side rendering and no build step. Provides the file-based router, SSR, server actions, route handlers, middleware, and live reload on Node 24+ or Bun.",
6
6
  "main": "index.js",
@@ -285,6 +285,118 @@ const PURE_DATA_CONSTRUCTORS = new Set([
285
285
  'BigInt64Array', 'BigUint64Array',
286
286
  ]);
287
287
 
288
+ /**
289
+ * Marks where `hasModuleScopeSideEffect` dropped a top-level `{...}` body from
290
+ * its depth-0 frame, so an arrow followed by a block body is not mistaken for
291
+ * an expression body that runs on into the next statement. Removed again
292
+ * before the call scan.
293
+ */
294
+ const BLOCK = '\x01';
295
+
296
+ /** `function`, `function*`, `async function name` up to its parameter list's `(`. */
297
+ const FUNCTION_PARAMS_RE = /(?<![\w$.])function\s*\*?\s*(?:[A-Za-z_$][\w$]*)?\s*\(/g;
298
+
299
+ /**
300
+ * The text before an arrow's `=>` when that arrow is a declarator initializer
301
+ * (`const usd = (m) =>`, `let f = async x =>`, `, g = (a) =>`). The parameter
302
+ * list must hold no parens, so a call in a parameter default
303
+ * (`(a = init()) =>`) keeps its arrow unrecognised and still ships.
304
+ */
305
+ const ARROW_DECLARATOR_RE =
306
+ /(?:(?<![\w$.])(?:const|let|var)\s+|,\s*)[A-Za-z_$][\w$]*\s*=\s*(?:async\s*)?(?:\([^()]*\)|[A-Za-z_$][\w$]*)\s*$/;
307
+
308
+ /**
309
+ * An expression body that ends this way cannot stop at the newline (a binary
310
+ * operator, `?`, `:`, or a curried `=>`). Postfix `++` / `--` and a regex
311
+ * literal's closing `/` end a statement, so neither is listed.
312
+ */
313
+ const BODY_CONTINUES_RE = /(?:=>|&&|\|\||\?\?|[?:=*%|&^<>]|(?<![+])\+|(?<![-])-)\s*$/;
314
+
315
+ /** A line starting this way continues the previous one: no statement starts with it. */
316
+ const LINE_CONTINUES_RE = /^\s*(?:\?|:|&&|\|\||\.(?![.\d])|\*|%|=(?!>)|\|)/;
317
+
318
+ /**
319
+ * Removes the two depth-0 shapes that look like calls and run nothing at module
320
+ * load: a `function` declaration's parameter list (a default such as
321
+ * `now = Date.now()` runs per call) and an arrow's expression body when the
322
+ * arrow initializes a declarator (`const usd = (m) => Math.round(m) / MICROS`).
323
+ *
324
+ * The arrow body ends at a `,` or `;` at its own paren/bracket depth 0, at a
325
+ * `)` / `]` closing something outside it, or at a newline once the body has
326
+ * content, unless `BODY_CONTINUES_RE` / `LINE_CONTINUES_RE` prove the
327
+ * expression carries on. Ending early is always safe, because whatever is left
328
+ * stays in the frame and is still scanned; ending late could swallow a real
329
+ * statement, so every rule errs towards ending.
330
+ *
331
+ * @param {string} frame depth-0 text, with `BLOCK` where a `{...}` was dropped
332
+ * @returns {string | null} the frame without those parts, or null when a
333
+ * parameter list does not close (the caller then ships)
334
+ */
335
+ function blankDeclarationOnlyCode(frame) {
336
+ let out = '';
337
+ let last = 0;
338
+ for (const m of frame.matchAll(FUNCTION_PARAMS_RE)) {
339
+ if (m.index < last) continue;
340
+ const open = m.index + m[0].length - 1;
341
+ let depth = 0;
342
+ let close = -1;
343
+ for (let j = open; j < frame.length; j++) {
344
+ if (frame[j] === '(') depth++;
345
+ else if (frame[j] === ')' && --depth === 0) { close = j; break; }
346
+ }
347
+ if (close < 0) return null;
348
+ out += frame.slice(last, open + 1);
349
+ last = close;
350
+ }
351
+ frame = out + frame.slice(last);
352
+
353
+ out = '';
354
+ let depth = 0;
355
+ for (let i = 0; i < frame.length; i++) {
356
+ const c = frame[i];
357
+ if (c === '(' || c === '[') depth++;
358
+ else if ((c === ')' || c === ']') && depth > 0) depth--;
359
+ if (depth === 0 && c === '=' && frame[i + 1] === '>' && ARROW_DECLARATOR_RE.test(out)) {
360
+ out += '=>';
361
+ i = arrowBodyEnd(frame, i + 2) - 1;
362
+ continue;
363
+ }
364
+ out += c;
365
+ }
366
+ return out;
367
+ }
368
+
369
+ /**
370
+ * Index where an arrow's expression body starting at `start` ends (see
371
+ * `blankDeclarationOnlyCode`).
372
+ *
373
+ * @param {string} frame
374
+ * @param {number} start index just past the `=>`
375
+ * @returns {number}
376
+ */
377
+ function arrowBodyEnd(frame, start) {
378
+ let depth = 0;
379
+ let i = start;
380
+ for (; i < frame.length; i++) {
381
+ const c = frame[i];
382
+ if (c === '(' || c === '[') { depth++; continue; }
383
+ if (c === ')' || c === ']') {
384
+ if (depth === 0) return i;
385
+ depth--;
386
+ continue;
387
+ }
388
+ if (depth > 0) continue;
389
+ if (c === ',' || c === ';') return i;
390
+ if (c === '\n') {
391
+ const body = frame.slice(start, i);
392
+ if (!body.trim() || BODY_CONTINUES_RE.test(body)) continue;
393
+ if (LINE_CONTINUES_RE.test(frame.slice(i + 1))) continue;
394
+ return i;
395
+ }
396
+ }
397
+ return i;
398
+ }
399
+
288
400
  /**
289
401
  * Module-scope client work, detected by an ALLOWLIST of safe top-level forms
290
402
  * rather than a denylist of browser globals. A module that runs ANY code when
@@ -320,8 +432,13 @@ const PURE_DATA_CONSTRUCTORS = new Set([
320
432
  * request path is covered; a DIRECT caller handing it `.ts` source as authored
321
433
  * is opting into that false positive.
322
434
  *
323
- * Over-detection is safe (a top-level arrow whose body calls something, or a
324
- * pure top-level helper call, only ships). The accepted residual misses, all
435
+ * A `function` declaration's parameter list and the expression body of an
436
+ * arrow that initializes a `const` / `let` / `var` declarator are dropped from
437
+ * the frame first (`blankDeclarationOnlyCode`): `now = Date.now()` and
438
+ * `(m) => Math.round(m) / MICROS` declare code, they do not run it.
439
+ *
440
+ * Over-detection is safe (a pure top-level helper call, or an arrow outside a
441
+ * declarator whose body calls something, only ships). The accepted residual misses, all
325
442
  * contrived and structural (so they do not rot), are a call buried inside a
326
443
  * top-level object / array initializer or a destructuring default, and a
327
444
  * side-effecting tagged-template hole evaluated at module scope.
@@ -361,7 +478,7 @@ export function hasModuleScopeSideEffect(src, literals) {
361
478
  if (depth === 0) frame += "''";
362
479
  continue;
363
480
  }
364
- if (c === '{') { depth++; continue; }
481
+ if (c === '{') { if (depth === 0) frame += BLOCK; depth++; continue; }
365
482
  if (c === '}') { if (depth > 0) depth--; continue; }
366
483
  if (depth === 0) frame += c;
367
484
  }
@@ -369,6 +486,11 @@ export function hasModuleScopeSideEffect(src, literals) {
369
486
  // literal with a stray brace is the common case). Ship rather than risk a
370
487
  // hidden top-level statement.
371
488
  if (depth !== 0) return true;
489
+ // Parameter lists and declarator arrow bodies are declarations, not code
490
+ // that runs at load.
491
+ const declared = blankDeclarationOnlyCode(frame);
492
+ if (declared === null) return true;
493
+ frame = declared.replaceAll(BLOCK, '');
372
494
  // Optional-chaining call/index: `foo?.()`, `x?.[i]()` (the `?.` defeats the
373
495
  // identifier-before-paren match below).
374
496
  if (/\?\.\s*[([]/.test(frame)) return true;