@webjsdev/server 0.8.42 → 0.8.44

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.42",
3
+ "version": "0.8.44",
4
4
  "type": "module",
5
5
  "description": "webjs dev/prod server: SSR, router, API, server actions, live reload",
6
6
  "main": "index.js",
package/src/check.js CHANGED
@@ -139,6 +139,11 @@ export const RULES = [
139
139
  description:
140
140
  'API route handlers (`route.{js,ts}`) must NOT call `redirect()` from `@webjsdev/core`. That function throws a control-flow signal designed for the SSR page renderer; in a route handler it goes uncaught and produces a 500. Use `Response.redirect(url, 303)` for external redirects or return a 3xx Response directly. Page functions, layouts, and server actions may still use `redirect()` (caught by the SSR pipeline).',
141
141
  },
142
+ {
143
+ name: 'no-interpolation-in-raw-text-element',
144
+ description:
145
+ 'Flags a template interpolation (`${...}`) placed as a child of a `<style>` or `<script>` element inside a COMPONENT `html` template. Raw-text elements are an SSR/client asymmetry trap: the server renderer emits the interpolated content, but the client renderer drops it (a raw-text hole is a `noop`, since the compile cache is keyed on the static strings), so the element renders correctly on the server and then wipes to empty on hydration. Scoped to components (files with a `WebComponent` class), which hydrate; pages and layouts render server-only and never hydrate, so a page interpolating a `css` result into a `<style>` is a legitimate pattern and is not flagged. In a component, author scoped CSS with `static styles` (shadow DOM) or a `css` template. Found dogfooding a tic-tac-toe app (#845): a `<style>${STYLE}</style>` painted at SSR then vanished on hydrate.',
146
+ },
142
147
  ];
143
148
 
144
149
  /** Set of all known rule names for fast lookup. */
@@ -634,6 +639,50 @@ export async function checkConventions(appDir) {
634
639
  }
635
640
  }
636
641
 
642
+ // --- Rule: no-interpolation-in-raw-text-element ---
643
+ // A `${...}` hole inside a `<style>` or `<script>` element in an `html`
644
+ // template is an SSR/client asymmetry: `renderToString` emits it, but the
645
+ // client parser drops a raw-text hole as a `noop` (the compile cache is keyed
646
+ // on the static strings, so a per-render value cannot be baked in), so the
647
+ // element paints at SSR then wipes to empty on hydration.
648
+ //
649
+ // Scoped to COMPONENTS. The drop only happens on the CLIENT renderer, which
650
+ // runs for components (hydration + re-render). Pages and layouts render
651
+ // server-only (never hydrate), so a page's `<style>${STYLES.text}</style>` is
652
+ // a legitimate, taught pattern and must NOT be flagged. Scan raw source with
653
+ // comments stripped (the tag text lives in a template string, which the
654
+ // redacted `scan` view blanks). One violation per file.
655
+ {
656
+ for (const { rel, content } of files) {
657
+ // Only files that define a hydrating custom element. A page/layout that
658
+ // interpolates a `css` result into a `<style>` is server-only and fine.
659
+ if (!/class\s+\w+\s+extends\s+WebComponent/.test(content)) continue;
660
+ const stripped = content
661
+ .replace(/\/\/.*$/gm, '')
662
+ .replace(/\/\*[\s\S]*?\*\//g, '');
663
+ for (const tag of ['style', 'script']) {
664
+ // `<tag ...> ... ${ ... </tag>`, where the hole sits before the close
665
+ // tag. The negative lookahead keeps the match from crossing `</tag>`.
666
+ const re = new RegExp(
667
+ `<${tag}\\b[^>]*>(?:(?!<\\/${tag}>)[\\s\\S])*?\\$\\{(?:(?!<\\/${tag}>)[\\s\\S])*?<\\/${tag}>`,
668
+ 'i',
669
+ );
670
+ if (re.test(stripped)) {
671
+ violations.push({
672
+ rule: 'no-interpolation-in-raw-text-element',
673
+ file: rel,
674
+ message: `An interpolation (\`\${...}\`) sits inside a <${tag}> element in an html template. The server renderer emits it but the client renderer drops it, so it paints at SSR then wipes to empty on hydration.`,
675
+ fix:
676
+ tag === 'style'
677
+ ? `Move the CSS out of the raw-text hole: use \`static styles\` (shadow DOM) or a \`css\` template for a component, or put page CSS in the layout. Static \`<style>...</style>\` with no \`\${}\` is fine.`
678
+ : `Build the script body outside the raw-text element (set attributes/properties via bindings, or compute the value before the template). Static \`<script>...</script>\` with no \`\${}\` is fine.`,
679
+ });
680
+ break; // one violation per file is enough
681
+ }
682
+ }
683
+ }
684
+ }
685
+
637
686
  // --- Rule: no-server-env-in-components ---
638
687
  // Catches `process.env.X` reads in component files where X is not a
639
688
  // WEBJS_PUBLIC_* var and not NODE_ENV. The SSR shim only exposes those
package/src/dev.js CHANGED
@@ -1825,9 +1825,14 @@ async function handleCore(req, ctx) {
1825
1825
  return invokeAction(state.actionIndex, actMatch[1], actMatch[2], req, onActionError, allowedOrigins);
1826
1826
  }
1827
1827
 
1828
- // Static: /public/*
1829
- if (path.startsWith('/public/') || path === '/favicon.ico') {
1830
- const p = path === '/favicon.ico' ? '/public/favicon.ico' : path;
1828
+ // Static: /public/*, plus a small set of ROOT assets that must serve at the
1829
+ // site root even though they live under public/. A service worker registered
1830
+ // at /sw.js scopes to the origin root, so it MUST serve at / (not
1831
+ // /public/sw.js), and so must its offline fallback. Same remap shape as the
1832
+ // /favicon.ico special-case below. (#830)
1833
+ const ROOT_ASSETS = { '/sw.js': '/public/sw.js', '/offline.html': '/public/offline.html' };
1834
+ if (path.startsWith('/public/') || path === '/favicon.ico' || path in ROOT_ASSETS) {
1835
+ const p = path === '/favicon.ico' ? '/public/favicon.ico' : (ROOT_ASSETS[path] || path);
1831
1836
  const abs = join(appDir, p);
1832
1837
  // Containment check. `join` normalises `..` segments, so a path
1833
1838
  // like `/public/%2E%2E/secret/x.svg` decodes (after URL parsing,
@@ -1843,7 +1848,13 @@ async function handleCore(req, ctx) {
1843
1848
  return new Response(null, { status: 404 });
1844
1849
  }
1845
1850
  // A `?v=<hash>` public asset is content-addressed -> immutable (#243).
1846
- if (await exists(abs)) return fileResponse(abs, { dev, immutable: versioned });
1851
+ if (await exists(abs)) {
1852
+ const res = await fileResponse(abs, { dev, immutable: versioned });
1853
+ // A worker served below its registration path only controls that subtree
1854
+ // unless the response opts it up to the root scope. (#830)
1855
+ if (path === '/sw.js') res.headers.set('Service-Worker-Allowed', '/');
1856
+ return res;
1857
+ }
1847
1858
  }
1848
1859
 
1849
1860
  // User source modules (served as ES modules, with action-file rewriting).
package/src/vendor.js CHANGED
@@ -726,7 +726,8 @@ function pinFilePath(appDir) {
726
726
  * The three-line `.gitignore` pattern that ignores the transient
727
727
  * `.webjs` caches at any depth while re-including the committed
728
728
  * `.webjs/vendor/` pin output. This mirrors the scaffold template
729
- * (`packages/cli/templates/.gitignore`) and the `vendor-gitignore`
729
+ * (`packages/cli/templates/gitignore`, shipped dotless so npm cannot
730
+ * strip it, renamed to `.gitignore` on copy) and the `vendor-gitignore`
730
731
  * check in `doctor.js` verbatim, so a self-healed `.gitignore`
731
732
  * ends up byte-identical to a freshly scaffolded one.
732
733
  */