@webjsdev/core 0.7.16 → 0.7.17

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/core",
3
- "version": "0.7.16",
3
+ "version": "0.7.17",
4
4
  "type": "module",
5
5
  "description": "webjs core runtime - html/css tags, WebComponent base, isomorphic renderers",
6
6
  "types": "./index.d.ts",
@@ -30,8 +30,14 @@ import { cspNonce } from './csp-nonce.js';
30
30
  */
31
31
  export async function renderToString(value, opts = { ssr: true }) {
32
32
  const ctx = opts && opts.suspenseCtx;
33
+ // The server `dev` flag drives prod-silence of SSR error states (#483).
34
+ // `opts.dev` wins; else inherit a `dev` already stamped on the ctx (so a
35
+ // streamed sub-context inherits it); back-fill the ctx so downstream renders
36
+ // sharing it see the same flag. Undefined stays undefined (NODE_ENV fallback).
37
+ const dev = opts && opts.dev !== undefined ? opts.dev : ctx && ctx.dev;
38
+ if (ctx && ctx.dev === undefined && dev !== undefined) ctx.dev = dev;
33
39
  const html = await render(value, ctx);
34
- return opts && opts.ssr === false ? html : await injectDSD(html, ctx);
40
+ return opts && opts.ssr === false ? html : await injectDSD(html, ctx, [], dev);
35
41
  }
36
42
 
37
43
  /**
@@ -377,12 +383,21 @@ function isProd() {
377
383
  * Dev surfaces the tag + message loudly so the failure is obvious; prod
378
384
  * renders an empty (silent, isolated) element so no internal detail leaks.
379
385
  *
386
+ * The prod-silence signal is the SERVER's `dev` flag, threaded through the SSR
387
+ * render context (#483). webjs keys prod on the CLI `dev` flag, not `NODE_ENV`,
388
+ * and `webjs start` does not export `NODE_ENV=production`, so a bare prod launch
389
+ * would otherwise leak the message. When `dev` is undefined (a context-free
390
+ * `renderToString` with no server signal, e.g. a bare unit test) it falls back
391
+ * to `isProd()` / `NODE_ENV`, preserving the prior behaviour for that path.
392
+ *
380
393
  * @param {string} tag
381
394
  * @param {Error} err
395
+ * @param {boolean} [dev] server dev flag; undefined falls back to NODE_ENV
382
396
  * @returns {unknown} a TemplateResult (dev) or '' (prod)
383
397
  */
384
- function defaultSSRErrorTemplate(tag, err) {
385
- if (isProd()) return '';
398
+ function defaultSSRErrorTemplate(tag, err, dev) {
399
+ const surface = dev === undefined ? !isProd() : !!dev;
400
+ if (!surface) return '';
386
401
  const msg = err && err.message ? err.message : String(err);
387
402
  return html`<div data-webjs-error="${tag}" style="border:1px solid #f5c2c7;background:#f8d7da;color:#842029;padding:8px 12px;border-radius:6px;font:13px/1.4 system-ui,sans-serif">
388
403
  <strong>&lt;${tag}&gt; failed to render</strong>
@@ -397,15 +412,18 @@ function defaultSSRErrorTemplate(tag, err) {
397
412
  *
398
413
  * @param {string} html
399
414
  * @param {SuspenseCtx} [ctx]
415
+ * @param {any[]} [ancestors]
416
+ * @param {boolean} [dev] server dev flag, threaded to the per-component error
417
+ * template for prod-silence (#483); undefined falls back to NODE_ENV
400
418
  * @returns {Promise<string>}
401
419
  */
402
- async function injectDSD(html, ctx, ancestors = []) {
420
+ async function injectDSD(html, ctx, ancestors = [], dev) {
403
421
  // Resolve <webjs-suspense> boundaries first (#471): in a streaming context
404
422
  // each becomes a fallback placeholder now, with its children pushed for
405
423
  // out-of-order streaming; without a streaming context the children render
406
424
  // inline (blocking). Run before the custom-element walk so a streamed
407
425
  // boundary's children leave the main flow and are not double-processed.
408
- html = await processSuspenseElements(html, ctx, ancestors);
426
+ html = await processSuspenseElements(html, ctx, ancestors, dev);
409
427
  const tags = allTags();
410
428
  if (!tags.length) return html;
411
429
  // Sort longest tag name first so the regex alternation tries the most
@@ -487,7 +505,7 @@ async function injectDSD(html, ctx, ancestors = []) {
487
505
  // Shadow DOM: native <slot> stays as-is in the DSD template. The
488
506
  // browser handles projection from the host's light-DOM children
489
507
  // into the shadow tree natively. No framework substitution here.
490
- const innerProcessed = await injectDSD(rawInner, ctx, [...ancestors, instance]);
508
+ const innerProcessed = await injectDSD(rawInner, ctx, [...ancestors, instance], dev);
491
509
  const rawStyles = /** @type any */ (Cls).styles;
492
510
  const styleList = Array.isArray(rawStyles) ? rawStyles : rawStyles && isCSS(rawStyles) ? [rawStyles] : [];
493
511
  const styleStr = stylesToString(styleList);
@@ -550,7 +568,7 @@ async function injectDSD(html, ctx, ancestors = []) {
550
568
  }
551
569
  const partitioned = partitionAuthoredBySlot(authoredInner);
552
570
  const innerWithSlots = substituteSlotsInRender(rawInner, partitioned);
553
- const innerProcessed = await injectDSD(innerWithSlots, ctx, [...ancestors, instance]);
571
+ const innerProcessed = await injectDSD(innerWithSlots, ctx, [...ancestors, instance], dev);
554
572
  edits.push({
555
573
  start: m.index,
556
574
  end: closeEnd,
@@ -582,10 +600,10 @@ async function injectDSD(html, ctx, ancestors = []) {
582
600
  if (instance && typeof instance.renderError === 'function') {
583
601
  errTpl = instance.renderError(err);
584
602
  }
585
- if (errTpl === undefined) errTpl = defaultSSRErrorTemplate(tag, err);
603
+ if (errTpl === undefined) errTpl = defaultSSRErrorTemplate(tag, err, dev);
586
604
  errorInner = await render(errTpl, ctx);
587
605
  if (errorInner.trim()) {
588
- errorInner = await injectDSD(errorInner, ctx, instance ? [...ancestors, instance] : ancestors);
606
+ errorInner = await injectDSD(errorInner, ctx, instance ? [...ancestors, instance] : ancestors, dev);
589
607
  }
590
608
  } catch (renderErrorThrew) {
591
609
  console.error(`[webjs] renderError() for <${tag}> also threw:`, renderErrorThrew);
@@ -691,9 +709,11 @@ function isVoidElement(tag) {
691
709
  * @param {string} html
692
710
  * @param {SuspenseCtx} [ctx]
693
711
  * @param {any[]} [ancestors]
712
+ * @param {boolean} [dev] server dev flag for prod-silence of a throwing
713
+ * component in an inline (ctx-absent) boundary (#483)
694
714
  * @returns {Promise<string>}
695
715
  */
696
- async function processSuspenseElements(html, ctx, ancestors = []) {
716
+ async function processSuspenseElements(html, ctx, ancestors = [], dev) {
697
717
  if (html.indexOf('<webjs-suspense') === -1) return html;
698
718
  const OPEN = /<webjs-suspense((?:"[^"]*"|'[^']*'|[^>])*?)>/i;
699
719
  let result = '';
@@ -732,7 +752,7 @@ async function processSuspenseElements(html, ctx, ancestors = []) {
732
752
  ctx.pending.push({ id, promise: Promise.resolve(unsafeHTML(inner)) });
733
753
  result += `<webjs-suspense id="${id}">${fallbackHtml}</webjs-suspense>`;
734
754
  } else {
735
- const innerProcessed = await injectDSD(inner, ctx, ancestors);
755
+ const innerProcessed = await injectDSD(inner, ctx, ancestors, dev);
736
756
  result += `<webjs-suspense>${innerProcessed}</webjs-suspense>`;
737
757
  }
738
758
  rest = afterClose;
@@ -1130,6 +1150,11 @@ function consumePropAttrs(attrs) {
1130
1150
  */
1131
1151
  export function renderToStream(value, opts = { ssr: true }) {
1132
1152
  const ctx = opts && opts.suspenseCtx;
1153
+ // Server dev flag for prod-silence of SSR error states (#483), same sourcing
1154
+ // as renderToString: opts.dev wins, else inherit from the ctx, else undefined
1155
+ // (NODE_ENV fallback). Back-fill the ctx so the streamed sub-renders share it.
1156
+ const dev = opts && opts.dev !== undefined ? opts.dev : ctx && ctx.dev;
1157
+ if (ctx && ctx.dev === undefined && dev !== undefined) ctx.dev = dev;
1133
1158
  return new ReadableStream({
1134
1159
  async start(controller) {
1135
1160
  try {
@@ -1141,13 +1166,13 @@ export function renderToStream(value, opts = { ssr: true }) {
1141
1166
  // the full HTML), then enqueue the result. This matches the
1142
1167
  // semantics of renderToString but still gives us a stream.
1143
1168
  const html = await render(value, ctx);
1144
- const full = await injectDSD(html, ctx);
1169
+ const full = await injectDSD(html, ctx, [], dev);
1145
1170
  controller.enqueue(full);
1146
1171
  }
1147
1172
 
1148
1173
  // Stream resolved Suspense boundaries after the main content.
1149
1174
  if (ctx && ctx.pending.length) {
1150
- await streamSuspenseBoundaries(ctx, controller);
1175
+ await streamSuspenseBoundaries(ctx, controller, dev);
1151
1176
  }
1152
1177
  controller.close();
1153
1178
  } catch (err) {
@@ -1404,8 +1429,10 @@ async function streamTemplate(tr, ctx, controller) {
1404
1429
  *
1405
1430
  * @param {SuspenseCtx} ctx
1406
1431
  * @param {ReadableStreamDefaultController<string>} controller
1432
+ * @param {boolean} [dev] server dev flag for prod-silence of a rejected
1433
+ * streamed boundary (#483); undefined falls back to NODE_ENV
1407
1434
  */
1408
- async function streamSuspenseBoundaries(ctx, controller) {
1435
+ async function streamSuspenseBoundaries(ctx, controller, dev) {
1409
1436
  // Resolve the per-request nonce once per call. The provider in
1410
1437
  // @webjsdev/server sources it from AsyncLocalStorage; outside a
1411
1438
  // request scope (or in the browser) the helper returns '' and we
@@ -1421,7 +1448,7 @@ async function streamSuspenseBoundaries(ctx, controller) {
1421
1448
  try {
1422
1449
  const resolved = await promise;
1423
1450
  const html = await render(resolved, ctx);
1424
- const full = await injectDSD(html, ctx);
1451
+ const full = await injectDSD(html, ctx, [], dev);
1425
1452
  controller.enqueue(
1426
1453
  `<template data-webjs-resolve="${id}">${full}</template>` +
1427
1454
  `<script${nonceAttr}>` +
@@ -1441,7 +1468,7 @@ async function streamSuspenseBoundaries(ctx, controller) {
1441
1468
  // error render itself throwing) leaves the fallback in place.
1442
1469
  try {
1443
1470
  const e = err instanceof Error ? err : new Error(String(err));
1444
- const errHtml = await injectDSD(await render(defaultSSRErrorTemplate('webjs-suspense', e), ctx), ctx);
1471
+ const errHtml = await injectDSD(await render(defaultSSRErrorTemplate('webjs-suspense', e, dev), ctx), ctx, [], dev);
1445
1472
  controller.enqueue(
1446
1473
  `<template data-webjs-resolve="${id}">${errHtml}</template>` +
1447
1474
  `<script${nonceAttr}>` +