@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.
- package/dist/client/index.js +3 -1
- package/dist/index.mjs +144 -17
- package/package.json +8 -8
package/dist/client/index.js
CHANGED
|
@@ -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
|
-
|
|
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"><${component.tagName}></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 {
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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.
|
|
594
|
-
messageText: "Missing
|
|
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
|
|
718
|
+
const ssrAppFilePath = path.resolve(buildResults.ssrAppFilePath);
|
|
615
719
|
try {
|
|
616
|
-
const hydrateUrl = pathToFileURL(
|
|
720
|
+
const hydrateUrl = pathToFileURL(ssrAppFilePath);
|
|
617
721
|
hydrateUrl.search = `?t=${Date.now()}`;
|
|
618
722
|
const hydrateModule = await import(hydrateUrl.href);
|
|
619
|
-
|
|
723
|
+
ssrApp = hydrateModule.default || hydrateModule;
|
|
620
724
|
} catch (e) {
|
|
621
725
|
catchError(diagnostics, e);
|
|
622
726
|
}
|
|
623
727
|
}
|
|
624
728
|
}
|
|
625
729
|
return {
|
|
626
|
-
|
|
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?.
|
|
647
|
-
const userOpts = prerenderConfig.
|
|
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.
|
|
755
|
+
const orgBeforeHydrate = opts.beforeSsr;
|
|
652
756
|
const applyPatch = serverCtx.sys.applyPrerenderGlobalPatch;
|
|
653
|
-
opts.
|
|
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(
|
|
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.
|
|
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.
|
|
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": "
|
|
43
|
+
"@tsdown/css": ">=0.21.0 <1.0.0",
|
|
44
44
|
"@types/ws": "^8.0.0",
|
|
45
|
-
"tsdown": "
|
|
46
|
-
"typescript": "
|
|
47
|
-
"vitest": "^4.1.
|
|
48
|
-
"vitest-environment-stencil": "^1.
|
|
49
|
-
"@stencil/core": "5.0.0-alpha.
|
|
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"
|