@stencil/dev-server 5.0.0-alpha.1 → 5.0.0-alpha.11

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.
@@ -507,6 +507,7 @@ const hmrInlineStylesTraverse = (elm, versionId, trackers) => {
507
507
  if (hasShadowRoot(elm)) hmrInlineStylesTraverse(elm.shadowRoot, versionId, trackers);
508
508
  if (elm.children) for (let i = 0; i < elm.children.length; i++) hmrInlineStylesTraverse(elm.children[i], versionId, trackers);
509
509
  };
510
+ const SLOT_FB_CSS = "slot-fb{display:contents}slot-fb[hidden]{display:none}";
510
511
  /**
511
512
  * Update or remove a style element based on the HMR update.
512
513
  * @param elm - the style element to update
@@ -517,7 +518,8 @@ const hmrInlineStylesTraverse = (elm, versionId, trackers) => {
517
518
  const hmrStyleElement = (elm, versionId, stylesUpdated) => {
518
519
  if (elm.getAttribute(STYLE_ID_ATTR) === stylesUpdated.styleId) {
519
520
  if (stylesUpdated.styleText) {
520
- elm.innerHTML = stylesUpdated.styleText.replace(/\\n/g, "\n");
521
+ const slotFbSuffix = elm.hasAttribute("data-slot-fb") ? SLOT_FB_CSS : "";
522
+ elm.innerHTML = stylesUpdated.styleText.replace(/\\n/g, "\n") + slotFbSuffix;
521
523
  elm.setAttribute("data-hmr", versionId);
522
524
  } else elm.remove();
523
525
  return true;
package/dist/index.mjs CHANGED
@@ -510,6 +510,110 @@ function getEditors() {
510
510
  ]);
511
511
  }
512
512
  //#endregion
513
+ //#region src/server/dev-preview.ts
514
+ /**
515
+ * Generate a virtual HTML page for the dev server when no index.html is present.
516
+ *
517
+ * Renders all discovered components. For each component, usage markdown files
518
+ * (src/components/my-cmp/usage/*.md) are scanned for ```html``` code blocks, which
519
+ * are used as preview snippets. Falls back to a bare tag if none are found.
520
+ * @param buildResults The compiler build results containing component metadata and output file information.
521
+ * @param filterDirPath Optional directory path to filter components by (e.g. when visiting /src/my-component/).
522
+ * @returns A string of HTML to be served as the dev preview page.
523
+ */
524
+ const generateDevPreview = (buildResults, filterDirPath) => {
525
+ const { namespace } = buildResults;
526
+ let { components } = buildResults;
527
+ if (filterDirPath) {
528
+ const normalized = path.normalize(filterDirPath);
529
+ components = components.filter((c) => path.normalize(path.dirname(c.sourceFilePath)) === normalized);
530
+ }
531
+ const title = filterDirPath ? `${namespace} / ${path.basename(filterDirPath)}` : namespace;
532
+ const loaderUrl = getLoaderUrl(buildResults);
533
+ const globalCssUrls = getGlobalCssUrls(buildResults);
534
+ const sections = components.length > 0 ? components.map((c) => renderComponentSection(c)) : [` <p style="color:color-mix(in oklab,CanvasText 50%,Canvas 50%)">No Stencil components found in this directory.</p>`];
535
+ return `<!DOCTYPE html>
536
+ <html lang="en">
537
+ <head>
538
+ <meta charset="UTF-8">
539
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
540
+ <title>${title} — Stencil Dev</title>
541
+ <style>
542
+ *, *::before, *::after { box-sizing: border-box; }
543
+ body { font-family: system-ui, sans-serif; margin: 0; padding: 2rem; }
544
+ h1 { font-size: 1rem; font-weight: 500; margin: 0 0 2rem; color: color-mix(in oklab, CanvasText 90%, Canvas 10%); }
545
+ .component { border: 1px solid color-mix(in oklab, CanvasText 20%, Canvas 80%); border-radius: 6px; margin-bottom: 1.5rem; overflow: hidden; }
546
+ .component-header { padding: 0.5rem 1rem; background: color-mix(in oklab, CanvasText 5%, Canvas 95%); font-family: monospace; font-size: 0.875rem; }
547
+ .component-preview { padding: 1.5rem; }
548
+ .component-preview + .component-preview { border-top: 1px dashed color-mix(in oklab, CanvasText 20%, Canvas 80%); }
549
+ </style>
550
+ ${[loaderUrl ? ` <script type="module" src="${loaderUrl}"><\/script>` : "", ...globalCssUrls.map((url) => ` <link rel="stylesheet" href="${url}">`)].filter(Boolean).join("\n")}
551
+ </head>
552
+ <body>
553
+ <h1>${title}</h1>
554
+ ${sections.join("\n")}
555
+ </body>
556
+ </html>`;
557
+ };
558
+ const renderComponentSection = (component) => {
559
+ const snippets = getUsageSnippets(component);
560
+ const previews = snippets.length > 0 ? snippets.map((html) => ` <div class="component-preview">\n ${html}\n </div>`).join("\n") : ` <div class="component-preview"><${component.tagName}></${component.tagName}></div>`;
561
+ return ` <div class="component">
562
+ <div class="component-header">&lt;${component.tagName}&gt;</div>
563
+ ${previews}
564
+ </div>`;
565
+ };
566
+ const getUsageSnippets = (component) => {
567
+ const usageDir = path.join(path.dirname(component.sourceFilePath), "usage");
568
+ try {
569
+ const files = fs.readdirSync(usageDir).filter((f) => f.toLowerCase().endsWith(".md"));
570
+ const snippets = [];
571
+ for (const file of files.sort()) {
572
+ const content = fs.readFileSync(path.join(usageDir, file), "utf-8");
573
+ for (const match of content.matchAll(/```html\n([\s\S]*?)```/g)) snippets.push(match[1].trim());
574
+ }
575
+ return snippets;
576
+ } catch {
577
+ return [];
578
+ }
579
+ };
580
+ /**
581
+ * Convert an absolute output file path to a server-relative URL.
582
+ * @param absPath The absolute file path.
583
+ * @param rootDir The root directory to which the path should be relative.
584
+ * @returns A server-relative URL.
585
+ */
586
+ const toServePath = (absPath, rootDir) => "/" + path.relative(rootDir, absPath).split(path.sep).join("/");
587
+ /**
588
+ * Derive the loader script URL from build outputs.
589
+ * Prefers loader-bundle (always built in dev); falls back to standalone auto-loader if present.
590
+ * @param buildResults The compiler build results containing output file information.
591
+ * @returns A server-relative URL to a loader script, or null if not found.
592
+ */
593
+ const getLoaderUrl = ({ outputs, fsNamespace, rootDir }) => {
594
+ const distLazy = outputs.find((o) => o.type === "dist-lazy");
595
+ if (distLazy) {
596
+ const file = distLazy.files.find((f) => f.endsWith(`/${fsNamespace}.js`));
597
+ if (file) return toServePath(file, rootDir);
598
+ }
599
+ const standalone = outputs.find((o) => o.type === "standalone");
600
+ if (standalone) {
601
+ const file = standalone.files.find((f) => /\/loader\.js$/.test(f));
602
+ if (file) return toServePath(file, rootDir);
603
+ }
604
+ return null;
605
+ };
606
+ /**
607
+ * Collect server-relative URLs for all global-style CSS outputs.
608
+ * @param buildResults The compiler build results containing output file information.
609
+ * @returns An array of server-relative URLs to global-style CSS files.
610
+ */
611
+ const getGlobalCssUrls = ({ outputs, rootDir }) => {
612
+ const globalStyle = outputs.find((o) => o.type === "global-style");
613
+ if (!globalStyle) return [];
614
+ return globalStyle.files.filter((f) => f.endsWith(".css") && !f.endsWith(".css.map")).map((f) => toServePath(f, rootDir));
615
+ };
616
+ //#endregion
513
617
  //#region src/server/ssr.ts
514
618
  /**
515
619
  * SSR (Server-Side Rendering) request handling.
@@ -519,10 +623,10 @@ async function ssrPageRequest(devServerConfig, serverCtx, req, res) {
519
623
  try {
520
624
  let status = 500;
521
625
  let content = "";
522
- const { hydrateApp, srcIndexHtml, diagnostics } = await setupHydrateApp(devServerConfig, serverCtx);
626
+ const { ssrApp, srcIndexHtml, diagnostics } = await setupHydrateApp(devServerConfig, serverCtx);
523
627
  if (!diagnostics.some((diagnostic) => diagnostic.level === "error")) try {
524
628
  const opts = getSsrHydrateOptions(devServerConfig, serverCtx, req.url);
525
- const ssrResults = await hydrateApp.renderToString(srcIndexHtml, opts);
629
+ const ssrResults = await ssrApp.renderToString(srcIndexHtml, opts);
526
630
  diagnostics.push(...ssrResults.diagnostics);
527
631
  status = ssrResults.httpStatus ?? 500;
528
632
  content = ssrResults.html ?? "";
@@ -549,11 +653,11 @@ async function ssrStaticDataRequest(devServerConfig, serverCtx, req, res) {
549
653
  try {
550
654
  const data = {};
551
655
  let httpCache = false;
552
- const { hydrateApp, srcIndexHtml, diagnostics } = await setupHydrateApp(devServerConfig, serverCtx);
656
+ const { ssrApp, srcIndexHtml, diagnostics } = await setupHydrateApp(devServerConfig, serverCtx);
553
657
  if (!diagnostics.some((diagnostic) => diagnostic.level === "error")) try {
554
658
  const { ssrPath, hasQueryString } = getSsrStaticDataPath(req);
555
659
  const opts = getSsrHydrateOptions(devServerConfig, serverCtx, new URL(ssrPath, req.url));
556
- const ssrResults = await hydrateApp.renderToString(srcIndexHtml, opts);
660
+ const ssrResults = await ssrApp.renderToString(srcIndexHtml, opts);
557
661
  diagnostics.push(...ssrResults.diagnostics);
558
662
  ssrResults.staticData.forEach((s) => {
559
663
  if (s.type === "application/json") data[s.id] = JSON.parse(s.content);
@@ -580,18 +684,18 @@ async function ssrStaticDataRequest(devServerConfig, serverCtx, req, res) {
580
684
  }
581
685
  async function setupHydrateApp(devServerConfig, serverCtx) {
582
686
  let srcIndexHtml = null;
583
- let hydrateApp = null;
687
+ let ssrApp = null;
584
688
  const buildResults = await serverCtx.getBuildResults();
585
689
  const diagnostics = [];
586
690
  if (serverCtx.prerenderConfig == null && isString(devServerConfig.prerenderConfig)) try {
587
- const prerenderConfigResults = (await import("@stencil/core/compiler")).nodeRequire(devServerConfig.prerenderConfig);
691
+ const prerenderConfigResults = await (await import("@stencil/core/compiler")).nodeRequire(devServerConfig.prerenderConfig);
588
692
  diagnostics.push(...prerenderConfigResults.diagnostics);
589
693
  if (prerenderConfigResults.module?.config) serverCtx.prerenderConfig = prerenderConfigResults.module.config;
590
694
  } catch (e) {
591
695
  catchError(diagnostics, e);
592
696
  }
593
- if (!isString(buildResults.hydrateAppFilePath)) diagnostics.push({
594
- messageText: "Missing hydrateAppFilePath",
697
+ if (!isString(buildResults.ssrAppFilePath)) diagnostics.push({
698
+ messageText: "Missing ssrAppFilePath",
595
699
  level: "error",
596
700
  type: "ssr",
597
701
  lines: []
@@ -611,19 +715,19 @@ async function setupHydrateApp(devServerConfig, serverCtx) {
611
715
  type: "ssr"
612
716
  });
613
717
  else {
614
- const hydrateAppFilePath = path.resolve(buildResults.hydrateAppFilePath);
718
+ const ssrAppFilePath = path.resolve(buildResults.ssrAppFilePath);
615
719
  try {
616
- const hydrateUrl = pathToFileURL(hydrateAppFilePath);
720
+ const hydrateUrl = pathToFileURL(ssrAppFilePath);
617
721
  hydrateUrl.search = `?t=${Date.now()}`;
618
722
  const hydrateModule = await import(hydrateUrl.href);
619
- hydrateApp = hydrateModule.default || hydrateModule;
723
+ ssrApp = hydrateModule.default || hydrateModule;
620
724
  } catch (e) {
621
725
  catchError(diagnostics, e);
622
726
  }
623
727
  }
624
728
  }
625
729
  return {
626
- hydrateApp,
730
+ ssrApp,
627
731
  srcIndexHtml,
628
732
  diagnostics
629
733
  };
@@ -643,14 +747,14 @@ function getSsrHydrateOptions(devServerConfig, serverCtx, url) {
643
747
  prettyHtml: true
644
748
  };
645
749
  const prerenderConfig = serverCtx?.prerenderConfig;
646
- if (isFunction(prerenderConfig?.hydrateOptions)) {
647
- const userOpts = prerenderConfig.hydrateOptions(url);
750
+ if (isFunction(prerenderConfig?.prerenderOptions)) {
751
+ const userOpts = prerenderConfig.prerenderOptions(url);
648
752
  if (userOpts) Object.assign(opts, userOpts);
649
753
  }
650
754
  if (isFunction(serverCtx.sys.applyPrerenderGlobalPatch)) {
651
- const orgBeforeHydrate = opts.beforeHydrate;
755
+ const orgBeforeHydrate = opts.beforeSsr;
652
756
  const applyPatch = serverCtx.sys.applyPrerenderGlobalPatch;
653
- opts.beforeHydrate = (document) => {
757
+ opts.beforeSsr = (document) => {
654
758
  const devServerHostUrl = new URL(devServerConfig.browserUrl).origin;
655
759
  applyPatch({
656
760
  devServerHostUrl,
@@ -884,7 +988,7 @@ async function serveDevClient(devServerConfig, serverCtx, req, res) {
884
988
  if (isDevServerClient(req.pathname)) return serveDevClientScript(devServerConfig, serverCtx, req, res);
885
989
  if (isInitialDevServerLoad(req.pathname)) req.filePath = path.join(devServerConfig.devServerDir, "templates", "initial-load.html");
886
990
  else {
887
- const subPath = req.pathname.replace(DEV_SERVER_URL + "/", "");
991
+ const subPath = req.pathname.replace("/~dev-server/", "");
888
992
  if (subPath.startsWith("client/")) req.filePath = path.join(devServerConfig.devServerDir, subPath);
889
993
  else req.filePath = path.join(devServerConfig.devServerDir, "static", subPath);
890
994
  }
@@ -948,6 +1052,15 @@ async function serveDirectoryIndex(devServerConfig, serverCtx, req, res) {
948
1052
  if (!req.pathname.endsWith("/")) return serverCtx.serve302(req, res, req.pathname + "/");
949
1053
  try {
950
1054
  const dirFilePaths = await serverCtx.sys.readDir(req.filePath);
1055
+ const hasTsx = dirFilePaths.some((f) => f.endsWith(".tsx"));
1056
+ const hasHtml = dirFilePaths.some((f) => f.endsWith(".html") || f.endsWith(".htm"));
1057
+ if (hasTsx && !hasHtml) return serveDevPreview(devServerConfig, serverCtx, req, res, req.filePath);
1058
+ if (req.pathname === "/" || req.pathname === devServerConfig.basePath) {
1059
+ const srcPath = path.join(req.filePath, "src");
1060
+ try {
1061
+ if ((await serverCtx.sys.stat(srcPath)).isDirectory) return serverCtx.serve302(req, res, "/src/");
1062
+ } catch {}
1063
+ }
951
1064
  try {
952
1065
  if (serverCtx.dirTemplate == null) {
953
1066
  const dirTemplatePath = path.join(devServerConfig.devServerDir, "templates", "directory-index.html");
@@ -969,6 +1082,20 @@ async function serveDirectoryIndex(devServerConfig, serverCtx, req, res) {
969
1082
  return serverCtx.serve404(req, res, "serveDirectoryIndex");
970
1083
  }
971
1084
  }
1085
+ async function serveDevPreview(devServerConfig, serverCtx, req, res, filterDirPath) {
1086
+ try {
1087
+ const html = appendDevServerClientScript(devServerConfig, req, generateDevPreview(await serverCtx.getBuildResults(), filterDirPath));
1088
+ const buf = Buffer.from(html, "utf-8");
1089
+ serverCtx.logRequest(req, 200);
1090
+ res.writeHead(200, responseHeaders({
1091
+ "content-type": "text/html;charset=utf-8",
1092
+ "content-length": buf.byteLength
1093
+ }));
1094
+ res.end(buf);
1095
+ } catch (e) {
1096
+ serverCtx.serve500(req, res, e, "serveDevPreview");
1097
+ }
1098
+ }
972
1099
  async function getDirectoryFiles(sys, baseUrl, dirItemNames) {
973
1100
  const items = await getDirectoryItems(sys, baseUrl, dirItemNames);
974
1101
  if (baseUrl.pathname !== "/") items.unshift({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stencil/dev-server",
3
- "version": "5.0.0-alpha.1",
3
+ "version": "5.0.0-alpha.11",
4
4
  "description": "Development server for Stencil with DOM-based HMR",
5
5
  "keywords": [
6
6
  "dev server",
@@ -35,18 +35,18 @@
35
35
  }
36
36
  },
37
37
  "dependencies": {
38
- "launch-editor": "^2.9.1",
38
+ "launch-editor": "^2.0.0",
39
39
  "open": "^11.0.0",
40
40
  "ws": "^8.0.0"
41
41
  },
42
42
  "devDependencies": {
43
- "@tsdown/css": "^0.21.6",
43
+ "@tsdown/css": ">=0.21.0 <1.0.0",
44
44
  "@types/ws": "^8.0.0",
45
- "tsdown": "^0.21.6",
46
- "typescript": "~6.0.2",
47
- "vitest": "^4.1.1",
48
- "vitest-environment-stencil": "^1.9.3",
49
- "@stencil/core": "5.0.0-alpha.1"
45
+ "tsdown": ">=0.21.0 <1.0.0",
46
+ "typescript": ">4.0.0 <7.0.0",
47
+ "vitest": "^4.1.7",
48
+ "vitest-environment-stencil": "^1.12.1",
49
+ "@stencil/core": "5.0.0-alpha.11"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "@stencil/core": "^5.0.0-0"