UNPKG

@stencil/dev-server

Version:

Development server for Stencil with DOM-based HMR

1,557 lines 54.4 kB
import * as path from "node:path";
import * as http from "node:http";
import * as https from "node:https";
import * as net from "node:net";
import { WebSocketServer } from "ws";
import * as fs from "node:fs";
import { inspect } from "node:util";
import { pathToFileURL } from "node:url";
import * as zlib from "node:zlib";
import { fork } from "node:child_process";
//#region src/server/utils.ts
const DEV_SERVER_URL = "/~dev-server";
const DEV_MODULE_URL = "/~dev-module";
const DEV_SERVER_INIT_URL = `${DEV_SERVER_URL}-init`;
const OPEN_IN_EDITOR_URL = `${DEV_SERVER_URL}-open-in-editor`;
const VERSION = "5.0.0";
const DEFAULT_HEADERS = {
	"cache-control": "no-cache, no-store, must-revalidate, max-age=0",
	expires: "0",
	date: "Wed, 1 Jan 2000 00:00:00 GMT",
	server: `Stencil Dev Server ${VERSION}`,
	"access-control-allow-origin": "*",
	"access-control-expose-headers": "*"
};
/**
* Build response headers with optional HTTP caching.
*
* @param headers - custom headers to merge with defaults
* @param httpCache - whether to enable HTTP caching
* @returns the combined response headers
*/
function responseHeaders(headers, httpCache = false) {
	const result = {
		...DEFAULT_HEADERS,
		...headers
	};
	if (httpCache) {
		result["cache-control"] = "max-age=3600";
		delete result["date"];
		delete result["expires"];
	}
	return result;
}
/**
* Build a browser URL from components.
*
* @param protocol - the URL protocol (http or https)
* @param address - the server address
* @param port - the server port
* @param basePath - the base path
* @param pathname - the URL pathname
* @returns the complete browser URL
*/
function getBrowserUrl(protocol, address, port, basePath, pathname) {
	address = address === "0.0.0.0" ? "localhost" : address;
	const portSuffix = !port || port === 80 || port === 443 ? "" : ":" + port;
	let path = basePath;
	if (pathname.startsWith("/")) pathname = pathname.substring(1);
	path += pathname;
	protocol = protocol.replace(/:/g, "");
	return `${protocol}://${address}${portSuffix}${path}`;
}
/**
* Get the URL for the dev server client script.
*
* @param devServerConfig - the dev server configuration
* @param host - optional host override
* @param protocol - optional protocol override
* @returns the dev server client URL
*/
function getDevServerClientUrl(devServerConfig, host, protocol) {
	let address = devServerConfig.address;
	let port = devServerConfig.port;
	if (host) {
		address = host;
		port = null;
	}
	return getBrowserUrl(protocol ?? devServerConfig.protocol, address, port, devServerConfig.basePath, DEV_SERVER_URL);
}
const CONTENT_TYPES = {
	html: "text/html",
	htm: "text/html",
	css: "text/css",
	js: "text/javascript",
	mjs: "text/javascript",
	json: "application/json",
	xml: "application/xml",
	svg: "image/svg+xml",
	png: "image/png",
	jpg: "image/jpeg",
	jpeg: "image/jpeg",
	gif: "image/gif",
	webp: "image/webp",
	ico: "image/x-icon",
	woff: "font/woff",
	woff2: "font/woff2",
	ttf: "font/ttf",
	otf: "font/otf",
	eot: "application/vnd.ms-fontobject",
	mp3: "audio/mpeg",
	mp4: "video/mp4",
	webm: "video/webm",
	ogg: "audio/ogg",
	wav: "audio/wav",
	pdf: "application/pdf",
	zip: "application/zip",
	wasm: "application/wasm",
	map: "application/json",
	txt: "text/plain",
	md: "text/markdown",
	ts: "text/typescript",
	tsx: "text/typescript-jsx"
};
/**
* Get the content type for a file based on its extension.
*
* @param filePath - the file path to check
* @returns the MIME content type
*/
function getContentType(filePath) {
	const last = filePath.replace(/^.*[/\\]/, "").toLowerCase();
	const ext = last.replace(/^.*\./, "").toLowerCase();
	const hasPath = last.length < filePath.length;
	return (ext.length < last.length - 1 || !hasPath) && CONTENT_TYPES[ext] || "application/octet-stream";
}
/**
* Check if a file is an HTML file.
*
* @param filePath - the file path to check
* @returns true if the file is HTML
*/
function isHtmlFile(filePath) {
	const lower = filePath.toLowerCase().trim();
	return lower.endsWith(".html") || lower.endsWith(".htm");
}
/**
* Check if a file is a CSS file.
*
* @param filePath - the file path to check
* @returns true if the file is CSS
*/
function isCssFile(filePath) {
	return filePath.toLowerCase().trim().endsWith(".css");
}
const TXT_EXT = [
	"css",
	"html",
	"htm",
	"js",
	"json",
	"svg",
	"xml",
	"mjs",
	"ts",
	"tsx",
	"md",
	"txt"
];
/**
* Check if a file is simple text (CSS, HTML, JS, JSON, etc.).
*
* @param filePath - the file path to check
* @returns true if the file is a simple text format
*/
function isSimpleText(filePath) {
	const ext = filePath.toLowerCase().trim().split(".").pop();
	return ext ? TXT_EXT.includes(ext) : false;
}
/**
* Check if a pathname has no file extension.
*
* @param pathname - the URL pathname to check
* @returns true if the path has no extension
*/
function isExtensionLessPath(pathname) {
	const parts = pathname.split("/");
	return !parts[parts.length - 1].includes(".");
}
/**
* Check if a pathname is for SSR static data (page.state.json).
*
* @param pathname - the URL pathname to check
* @returns true if the path is for SSR static data
*/
function isSsrStaticDataPath(pathname) {
	const parts = pathname.split("/");
	return parts[parts.length - 1].split("?")[0] === "page.state.json";
}
/**
* Extract SSR static data path information from an HTTP request.
*
* @param req - the HTTP request object
* @returns an object containing ssrPath, fileName, and hasQueryString
*/
function getSsrStaticDataPath(req) {
	const parts = req.url.href.split("/");
	const fileNameParts = parts[parts.length - 1].split("?");
	parts.pop();
	let ssrPath = new URL(parts.join("/")).href;
	if (!ssrPath.endsWith("/") && req.headers) {
		if (new Headers(req.headers).get("referer")?.endsWith("/")) ssrPath += "/";
	}
	return {
		ssrPath,
		fileName: fileNameParts[0],
		hasQueryString: typeof fileNameParts[1] === "string" && fileNameParts[1].length > 0
	};
}
/**
* Check if a pathname is for the dev client.
*
* @param pathname - the URL pathname to check
* @returns true if the path is for the dev client
*/
function isDevClient(pathname) {
	return pathname.startsWith(DEV_SERVER_URL);
}
/**
* Check if a pathname is for a dev module.
*
* @param pathname - the URL pathname to check
* @returns true if the path is for a dev module
*/
function isDevModule(pathname) {
	return pathname.includes(DEV_MODULE_URL);
}
/**
* Check if a pathname is for the open-in-editor endpoint.
*
* @param pathname - the URL pathname to check
* @returns true if the path is for opening in editor
*/
function isOpenInEditor(pathname) {
	return pathname === OPEN_IN_EDITOR_URL;
}
/**
* Check if a pathname is for the initial dev server load.
*
* @param pathname - the URL pathname to check
* @returns true if the path is for initial dev server load
*/
function isInitialDevServerLoad(pathname) {
	return pathname === DEV_SERVER_INIT_URL;
}
/**
* Check if a pathname is for the dev server client script.
*
* @param pathname - the URL pathname to check
* @returns true if the path is for the dev server client
*/
function isDevServerClient(pathname) {
	return pathname === DEV_SERVER_URL;
}
/**
* Check if a response should be gzip compressed.
*
* @param devServerConfig - the dev server configuration
* @param req - the HTTP request object
* @returns true if the response should be compressed
*/
function shouldCompress(devServerConfig, req) {
	if (!devServerConfig.gzip) return false;
	if (req.method !== "GET") return false;
	const acceptEncoding = req.headers?.["accept-encoding"];
	if (typeof acceptEncoding !== "string") return false;
	return acceptEncoding.includes("gzip");
}
/**
* Normalize a file path to use forward slashes and remove redundant slashes.
*
* @param path - the file path to normalize
* @returns the normalized path
*/
function normalizePath(path) {
	let normalized = path.replace(/\\/g, "/");
	normalized = normalized.replace(/\/+/g, "/");
	if (path.startsWith("\\\\")) normalized = "/" + normalized;
	return normalized;
}
//#endregion
//#region src/server/context.ts
/**
* Server context factory.
* Creates the shared context object passed to request handlers.
*/
function createServerContext(sys, sendMsg, devServerConfig, buildResultsResolves, compilerRequestResolves) {
	const logRequest = (req, status) => {
		if (devServerConfig) sendMsg({ requestLog: {
			method: req.method || "?",
			url: req.pathname || "?",
			status
		} });
	};
	const serve500 = (req, res, error, xSource) => {
		try {
			if (res.headersSent) {
				res.end();
				return;
			}
			res.writeHead(500, responseHeaders({
				"content-type": "text/plain; charset=utf-8",
				"x-source": xSource
			}));
			res.write(inspect(error));
			res.end();
			logRequest(req, 500);
		} catch (e) {
			sendMsg({ error: { message: "serve500: " + e } });
		}
	};
	const serve404 = (req, res, xSource, content = null) => {
		try {
			if (res.headersSent) {
				res.end();
				return;
			}
			if (req.pathname === "/favicon.ico") {
				const defaultFavicon = path.join(devServerConfig.devServerDir, "static", "favicon.ico");
				const rs = fs.createReadStream(defaultFavicon);
				rs.on("error", () => {
					if (!res.headersSent) res.writeHead(404);
					res.end();
				});
				res.writeHead(200, responseHeaders({
					"content-type": "image/x-icon",
					"x-source": `favicon: ${xSource}`
				}));
				rs.pipe(res);
				return;
			}
			if (content == null) content = [
				"404 File Not Found",
				"Url: " + req.pathname,
				"File: " + req.filePath
			].join("\n");
			res.writeHead(404, responseHeaders({
				"content-type": "text/plain; charset=utf-8",
				"x-source": xSource
			}));
			res.write(content);
			res.end();
			logRequest(req, 404);
		} catch (e) {
			serve500(req, res, e, xSource);
		}
	};
	const serve302 = (req, res, pathname = null) => {
		logRequest(req, 302);
		res.writeHead(302, { location: pathname || devServerConfig.basePath || "/" });
		res.end();
	};
	const getBuildResults = () => new Promise((resolve, reject) => {
		if (serverCtx.isServerListening) {
			buildResultsResolves.push({
				resolve,
				reject
			});
			sendMsg({ requestBuildResults: true });
		} else reject("dev server closed");
	});
	const getCompilerRequest = (compilerRequestPath) => new Promise((resolve, reject) => {
		if (serverCtx.isServerListening) {
			compilerRequestResolves.push({
				path: compilerRequestPath,
				resolve,
				reject
			});
			sendMsg({ compilerRequestPath });
		} else reject("dev server closed");
	});
	const serverCtx = {
		connectorHtml: null,
		dirTemplate: null,
		getBuildResults,
		getCompilerRequest,
		isServerListening: false,
		logRequest,
		prerenderConfig: null,
		serve302,
		serve404,
		serve500,
		sys
	};
	return serverCtx;
}
//#endregion
//#region src/server/editor.ts
/**
* Editor integration using launch-editor.
* Consolidated from open-in-browser.ts, open-in-editor.ts, and open-in-editor-api.ts.
*/
async function openInBrowser(opts) {
	const { default: open } = await import("open");
	await open(opts.url);
}
let launchEditorLoaded = false;
let launchEditor = null;
async function loadLaunchEditor() {
	if (launchEditorLoaded) return;
	try {
		const mod = await import("launch-editor");
		launchEditor = mod.default || mod;
	} catch {
		console.warn("launch-editor package is not available. Open in editor functionality will be disabled.");
		launchEditor = null;
	}
	launchEditorLoaded = true;
}
async function serveOpenInEditor(serverCtx, req, res) {
	let status = 200;
	const data = {};
	try {
		await parseEditorData(serverCtx.sys, req, data);
		await openDataInEditor(data);
	} catch (e) {
		data.error = String(e);
		status = 500;
	}
	serverCtx.logRequest(req, status);
	res.writeHead(status, responseHeaders({ "content-type": "application/json; charset=utf-8" }));
	res.write(JSON.stringify(data, null, 2));
	res.end();
}
async function parseEditorData(sys, req, data) {
	const qs = req.searchParams;
	if (!qs.has("file")) {
		data.error = "missing file";
		return;
	}
	data.file = qs.get("file");
	if (qs.has("line") && !isNaN(Number(qs.get("line")))) data.line = parseInt(qs.get("line"), 10);
	if (typeof data.line !== "number" || data.line < 1) data.line = 1;
	if (qs.has("column") && !isNaN(Number(qs.get("column")))) data.column = parseInt(qs.get("column"), 10);
	if (typeof data.column !== "number" || data.column < 1) data.column = 1;
	if (qs.has("editor")) data.editor = qs.get("editor");
	data.exists = (await sys.stat(data.file)).isFile;
}
async function openDataInEditor(data) {
	if (!data.exists || data.error) return;
	await loadLaunchEditor();
	if (!launchEditor) {
		data.error = "launch-editor not available";
		return;
	}
	try {
		const fileSpec = `${data.file}:${data.line}:${data.column}`;
		await new Promise((resolve, reject) => {
			let errorCalled = false;
			launchEditor(fileSpec, data.editor || process.env.EDITOR, (_fileName, errorMessage) => {
				errorCalled = true;
				const errMsg = errorMessage || "Unknown error";
				console.error("Editor launch failed.");
				console.error("The \"code\" executable was not found in your PATH.");
				console.error("This usually means your editor's command-line tool isn't installed.");
				console.error("Try running:");
				console.error("  code --version");
				console.error("If that fails, install your editor's CLI command and ensure it's in your PATH.\n");
				data.error = errMsg;
				reject(new Error(errMsg));
			});
			setTimeout(() => {
				if (!errorCalled) {
					data.open = fileSpec;
					resolve();
				}
			}, 100);
		});
	} catch (e) {
		if (!data.error) data.error = String(e);
	}
}
function getEditors() {
	return Promise.resolve([
		{
			id: "code",
			name: "Visual Studio Code"
		},
		{
			id: "cursor",
			name: "Cursor"
		},
		{
			id: "code-insiders",
			name: "VS Code Insiders"
		},
		{
			id: "webstorm",
			name: "WebStorm"
		},
		{
			id: "idea",
			name: "IntelliJ IDEA"
		},
		{
			id: "sublime",
			name: "Sublime Text"
		},
		{
			id: "atom",
			name: "Atom"
		},
		{
			id: "vim",
			name: "Vim"
		},
		{
			id: "emacs",
			name: "Emacs"
		}
	]);
}
//#endregion
//#region src/server/dev-preview.ts
/**
* Generate a virtual HTML page for the dev server when no index.html is present.
*
* Renders all discovered components. For each component, usage markdown files
* (src/components/my-cmp/usage/*.md) are scanned for ```html``` code blocks, which
* are used as preview snippets. Falls back to a bare tag if none are found.
* @param buildResults The compiler build results containing component metadata and output file information.
* @param filterDirPath Optional directory path to filter components by (e.g. when visiting /src/my-component/).
* @returns A string of HTML to be served as the dev preview page.
*/
const generateDevPreview = (buildResults, filterDirPath) => {
	const { namespace } = buildResults;
	let { components } = buildResults;
	if (filterDirPath) {
		const normalized = path.normalize(filterDirPath);
		components = components.filter((c) => path.normalize(path.dirname(c.sourceFilePath)) === normalized);
	}
	const title = filterDirPath ? `${namespace} / ${path.basename(filterDirPath)}` : namespace;
	const loaderUrl = getLoaderUrl(buildResults);
	const globalCssUrls = getGlobalCssUrls(buildResults);
	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>`];
	return `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>${title} — Stencil Dev</title>
  <style>
    *, *::before, *::after { box-sizing: border-box; }
    body { font-family: system-ui, sans-serif; margin: 0; padding: 2rem; }
    h1 { font-size: 1rem; font-weight: 500; margin: 0 0 2rem; color: color-mix(in oklab, CanvasText 90%, Canvas 10%); }
    .component { border: 1px solid color-mix(in oklab, CanvasText 20%, Canvas 80%); border-radius: 6px; margin-bottom: 1.5rem; overflow: hidden; }
    .component-header { padding: 0.5rem 1rem; background: color-mix(in oklab, CanvasText 5%, Canvas 95%); font-family: monospace; font-size: 0.875rem; }
    .component-description { margin: 0; padding: 0.75rem 1rem 0; font-size: 0.875rem; color: color-mix(in oklab, CanvasText 70%, Canvas 30%); }
    .component-preview { padding: 1.5rem; }
    .component-preview + .component-preview { border-top: 1px dashed color-mix(in oklab, CanvasText 20%, Canvas 80%); }
    .preview-note { margin: 0.75rem 0 0; font-size: 0.8125rem; color: color-mix(in oklab, CanvasText 50%, Canvas 50%); }
    .preview-note code { font-size: 0.8125rem; }
  </style>
${[loaderUrl ? `  <script type="module" src="${loaderUrl}"><\/script>` : "", ...globalCssUrls.map((url) => `  <link rel="stylesheet" href="${url}">`)].filter(Boolean).join("\n")}
</head>
<body>
  <h1>${title}</h1>
${sections.join("\n")}
</body>
</html>`;
};
const renderComponentSection = (component) => {
	const snippets = getUsageSnippets(component);
	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}>
      <p class="preview-note">No demos found - add usage examples to <code>usage/*.md</code> (fenced &#96;&#96;&#96;html&#96;&#96;&#96; blocks) to customize this preview.</p>
    </div>`;
	const description = component.docs.text.trim() ? `\n    <p class="component-description">${escapeHtml(component.docs.text.trim())}</p>` : "";
	return `  <div class="component">
    <div class="component-header">&lt;${component.tagName}&gt;</div>${description}
${previews}
  </div>`;
};
const escapeHtml = (unsafe) => unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
const getUsageSnippets = (component) => {
	const usageDir = path.join(path.dirname(component.sourceFilePath), "usage");
	try {
		const files = fs.readdirSync(usageDir).filter((f) => f.toLowerCase().endsWith(".md"));
		const snippets = [];
		for (const file of files.sort()) {
			const content = fs.readFileSync(path.join(usageDir, file), "utf-8");
			for (const match of content.matchAll(/```html\n([\s\S]*?)```/g)) snippets.push(match[1].trim());
		}
		return snippets;
	} catch {
		return [];
	}
};
/**
* Convert an absolute output file path to a server-relative URL.
* @param absPath The absolute file path.
* @param rootDir The root directory to which the path should be relative.
* @returns A server-relative URL.
*/
const toServePath = (absPath, rootDir) => "/" + path.relative(rootDir, absPath).split(path.sep).join("/");
/**
* Derive the loader script URL from build outputs.
* Prefers loader-bundle (always built in dev); falls back to standalone auto-loader if present.
* @param buildResults The compiler build results containing output file information.
* @returns A server-relative URL to a loader script, or null if not found.
*/
const getLoaderUrl = ({ outputs, fsNamespace, rootDir }) => {
	const distLazy = outputs.find((o) => o.type === "dist-lazy");
	if (distLazy) {
		const file = distLazy.files.find((f) => f.endsWith(`/${fsNamespace}.js`));
		if (file) return toServePath(file, rootDir);
	}
	const standalone = outputs.find((o) => o.type === "standalone");
	if (standalone) {
		const file = standalone.files.find((f) => /\/loader\.js$/.test(f));
		if (file) return toServePath(file, rootDir);
	}
	return null;
};
/**
* Collect server-relative URLs for all global-style CSS outputs.
* @param buildResults The compiler build results containing output file information.
* @returns An array of server-relative URLs to global-style CSS files.
*/
const getGlobalCssUrls = ({ outputs, rootDir }) => {
	const globalStyle = outputs.find((o) => o.type === "global-style");
	if (!globalStyle) return [];
	return globalStyle.files.filter((f) => f.endsWith(".css") && !f.endsWith(".css.map")).map((f) => toServePath(f, rootDir));
};
//#endregion
//#region src/server/ssr.ts
/**
* SSR (Server-Side Rendering) request handling.
* Migrated from ssr-request.ts.
*/
async function ssrPageRequest(devServerConfig, serverCtx, req, res) {
	try {
		let status = 500;
		let content = "";
		const { ssrApp, srcIndexHtml, diagnostics } = await setupHydrateApp(devServerConfig, serverCtx);
		if (!diagnostics.some((diagnostic) => diagnostic.level === "error")) try {
			const opts = getSsrHydrateOptions(devServerConfig, serverCtx, req.url);
			const ssrResults = await ssrApp.renderToString(srcIndexHtml, opts);
			diagnostics.push(...ssrResults.diagnostics);
			status = ssrResults.httpStatus ?? 500;
			content = ssrResults.html ?? "";
		} catch (e) {
			catchError(diagnostics, e);
		}
		if (diagnostics.some((diagnostic) => diagnostic.level === "error")) {
			content = getSsrErrorContent(diagnostics);
			status = 500;
		}
		if (devServerConfig.websocket) content = appendDevServerClientScript(devServerConfig, req, content);
		serverCtx.logRequest(req, status);
		res.writeHead(status, responseHeaders({
			"content-type": "text/html; charset=utf-8",
			"content-length": Buffer.byteLength(content, "utf8")
		}));
		res.write(content);
		res.end();
	} catch (e) {
		serverCtx.serve500(req, res, e, "ssrPageRequest");
	}
}
async function ssrStaticDataRequest(devServerConfig, serverCtx, req, res) {
	try {
		const data = {};
		let httpCache = false;
		const { ssrApp, srcIndexHtml, diagnostics } = await setupHydrateApp(devServerConfig, serverCtx);
		if (!diagnostics.some((diagnostic) => diagnostic.level === "error")) try {
			const { ssrPath, hasQueryString } = getSsrStaticDataPath(req);
			const opts = getSsrHydrateOptions(devServerConfig, serverCtx, new URL(ssrPath, req.url));
			const ssrResults = await ssrApp.renderToString(srcIndexHtml, opts);
			diagnostics.push(...ssrResults.diagnostics);
			ssrResults.staticData.forEach((s) => {
				if (s.type === "application/json") data[s.id] = JSON.parse(s.content);
				else data[s.id] = s.content;
			});
			data.components = ssrResults.components.map((c) => c.tag).sort();
			httpCache = hasQueryString;
		} catch (e) {
			catchError(diagnostics, e);
		}
		if (diagnostics.length > 0) data.diagnostics = diagnostics;
		const status = diagnostics.some((diagnostic) => diagnostic.level === "error") ? 500 : 200;
		const content = JSON.stringify(data);
		serverCtx.logRequest(req, status);
		res.writeHead(status, responseHeaders({
			"content-type": "application/json; charset=utf-8",
			"content-length": Buffer.byteLength(content, "utf8")
		}, httpCache && status === 200));
		res.write(content);
		res.end();
	} catch (e) {
		serverCtx.serve500(req, res, e, "ssrStaticDataRequest");
	}
}
async function setupHydrateApp(devServerConfig, serverCtx) {
	let srcIndexHtml = null;
	let ssrApp = null;
	const buildResults = await serverCtx.getBuildResults();
	const diagnostics = [];
	if (serverCtx.prerenderConfig == null && isString(devServerConfig.prerenderConfig)) try {
		const prerenderConfigResults = await (await import("@stencil/core/compiler")).nodeRequire(devServerConfig.prerenderConfig);
		diagnostics.push(...prerenderConfigResults.diagnostics);
		if (prerenderConfigResults.module?.config) serverCtx.prerenderConfig = prerenderConfigResults.module.config;
	} catch (e) {
		catchError(diagnostics, e);
	}
	if (!isString(buildResults.ssrAppFilePath)) diagnostics.push({
		messageText: "Missing ssrAppFilePath",
		level: "error",
		type: "ssr",
		lines: []
	});
	else if (!isString(devServerConfig.srcIndexHtml)) diagnostics.push({
		messageText: "Missing srcIndexHtml",
		level: "error",
		type: "ssr",
		lines: []
	});
	else {
		srcIndexHtml = await serverCtx.sys.readFile(devServerConfig.srcIndexHtml, "utf8");
		if (!isString(srcIndexHtml)) diagnostics.push({
			level: "error",
			lines: [],
			messageText: `Unable to load src index html: ${devServerConfig.srcIndexHtml}`,
			type: "ssr"
		});
		else {
			const ssrAppFilePath = path.resolve(buildResults.ssrAppFilePath);
			try {
				const hydrateUrl = pathToFileURL(ssrAppFilePath);
				hydrateUrl.search = `?t=${Date.now()}`;
				const hydrateModule = await import(hydrateUrl.href);
				ssrApp = hydrateModule.default || hydrateModule;
			} catch (e) {
				catchError(diagnostics, e);
			}
		}
	}
	return {
		ssrApp,
		srcIndexHtml,
		diagnostics
	};
}
function getSsrHydrateOptions(devServerConfig, serverCtx, url) {
	const opts = {
		url: url.href,
		addModulePreloads: false,
		approximateLineWidth: 120,
		inlineExternalStyleSheets: false,
		minifyScriptElements: false,
		minifyStyleElements: false,
		removeAttributeQuotes: false,
		removeBooleanAttributeQuotes: false,
		removeEmptyAttributes: false,
		removeHtmlComments: false,
		prettyHtml: true
	};
	const prerenderConfig = serverCtx?.prerenderConfig;
	if (isFunction(prerenderConfig?.prerenderOptions)) {
		const userOpts = prerenderConfig.prerenderOptions(url);
		if (userOpts) Object.assign(opts, userOpts);
	}
	if (isFunction(serverCtx.sys.applyPrerenderGlobalPatch)) {
		const orgBeforeHydrate = opts.beforeSsr;
		const applyPatch = serverCtx.sys.applyPrerenderGlobalPatch;
		opts.beforeSsr = (document) => {
			const devServerHostUrl = new URL(devServerConfig.browserUrl).origin;
			applyPatch({
				devServerHostUrl,
				window: document.defaultView
			});
			if (typeof orgBeforeHydrate === "function") return orgBeforeHydrate(document);
		};
	}
	return opts;
}
function getSsrErrorContent(diagnostics) {
	return `<!doctype html>
<html>
<head>
  <title>SSR Error</title>
  <style>
    body {
      font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace !important;
    }
  </style>
</head>
<body>
  <h1>SSR Dev Error</h1>
  ${diagnostics.map((diagnostic) => `
  <p>
    ${diagnostic.messageText}
  </p>
  `).join("")}
</body>
</html>`;
}
function catchError(diagnostics, err) {
	const diagnostic = {
		level: "error",
		type: "runtime",
		messageText: "",
		lines: []
	};
	if (err instanceof Error) {
		diagnostic.messageText = err.message;
		if (err.stack) diagnostic.messageText += "\n" + err.stack;
	} else diagnostic.messageText = String(err);
	diagnostics.push(diagnostic);
}
function isString(val) {
	return typeof val === "string";
}
function isFunction(val) {
	return typeof val === "function";
}
//#endregion
//#region src/server/handlers.ts
/**
* Request handlers.
* Consolidated from request-handler.ts, serve-file.ts, serve-dev-client.ts,
* serve-dev-node-module.ts, and serve-directory-index.ts.
*/
function createRequestHandler(devServerConfig, serverCtx) {
	let userRequestHandler = null;
	let userHandlerLoaded = false;
	return async function(incomingReq, res) {
		if (!userHandlerLoaded && typeof devServerConfig.requestListenerPath === "string") {
			userHandlerLoaded = true;
			try {
				const userModule = await import(pathToFileURL(devServerConfig.requestListenerPath).href);
				userRequestHandler = userModule.default || userModule;
			} catch (e) {
				console.error("Failed to load user request handler:", e);
			}
		}
		async function defaultHandler() {
			try {
				const req = normalizeHttpRequest(devServerConfig, incomingReq);
				if (!req.url) return serverCtx.serve302(req, res);
				if (devServerConfig.pingRoute !== null && req.pathname === devServerConfig.pingRoute) {
					try {
						if (!(await serverCtx.getBuildResults()).hasSuccessfulBuild) return serverCtx.serve500(req, res, "Build not successful", "build error");
						res.writeHead(200, "OK");
						res.write("OK");
						res.end();
					} catch {
						serverCtx.serve500(req, res, "Error getting build results", "ping error");
					}
					return;
				}
				if (isDevClient(req.pathname) && devServerConfig.websocket) return serveDevClient(devServerConfig, serverCtx, req, res);
				if (isDevModule(req.pathname)) return serveDevNodeModule(serverCtx, req, res);
				if (!isValidUrlBasePath(devServerConfig.basePath, req.url)) return serverCtx.serve404(req, res, "invalid basePath", `404 File Not Found, base path: ${devServerConfig.basePath}`);
				if (devServerConfig.ssr) {
					if (isExtensionLessPath(req.url.pathname)) return ssrPageRequest(devServerConfig, serverCtx, req, res);
					if (isSsrStaticDataPath(req.url.pathname)) return ssrStaticDataRequest(devServerConfig, serverCtx, req, res);
				}
				req.stats = await serverCtx.sys.stat(req.filePath);
				if (req.stats.isFile) return serveFile(devServerConfig, serverCtx, req, res);
				if (req.stats.isDirectory) return serveDirectoryIndex(devServerConfig, serverCtx, req, res);
				const xSource = ["notfound"];
				const validHistoryApi = isValidHistoryApi(devServerConfig, req);
				xSource.push(`validHistoryApi: ${validHistoryApi}`);
				if (validHistoryApi) try {
					const indexFilePath = path.join(devServerConfig.root, devServerConfig.historyApiFallback.index);
					xSource.push(`indexFilePath: ${indexFilePath}`);
					req.stats = await serverCtx.sys.stat(indexFilePath);
					if (req.stats.isFile) {
						req.filePath = indexFilePath;
						return serveFile(devServerConfig, serverCtx, req, res);
					}
				} catch (e) {
					xSource.push(`notfound error: ${e}`);
				}
				return serverCtx.serve404(req, res, xSource.join(", "));
			} catch (e) {
				const errorReq = {
					method: (incomingReq.method || "GET").toUpperCase(),
					acceptHeader: "",
					url: null,
					searchParams: null
				};
				return serverCtx.serve500(errorReq, res, e, "not found error");
			}
		}
		if (typeof userRequestHandler === "function") await userRequestHandler(incomingReq, res, defaultHandler);
		else await defaultHandler();
	};
}
function normalizeHttpRequest(devServerConfig, incomingReq) {
	const req = {
		method: (incomingReq.method || "GET").toUpperCase(),
		headers: incomingReq.headers,
		acceptHeader: incomingReq.headers && typeof incomingReq.headers.accept === "string" && incomingReq.headers.accept || "",
		host: incomingReq.headers && typeof incomingReq.headers.host === "string" && incomingReq.headers.host || void 0,
		url: null,
		searchParams: null
	};
	if ((incomingReq.url || "").trim() || null) {
		if (req.host) req.url = new URL(incomingReq.url, `http://${req.host}`);
		else req.url = new URL(incomingReq.url, "http://dev.stenciljs.com");
		req.searchParams = req.url.searchParams;
	}
	if (req.url) {
		req.pathname = req.url.pathname.replace(/\\/g, "/").split("/").map((part) => decodeURIComponent(part)).join("/");
		if (req.pathname.length > 0 && !isDevClient(req.pathname)) req.pathname = "/" + req.pathname.substring(devServerConfig.basePath.length);
		req.filePath = normalizePath(path.normalize(path.join(devServerConfig.root, path.relative("/", req.pathname))));
	}
	return req;
}
function isValidUrlBasePath(basePath, url) {
	let pathname = url.pathname;
	if (!pathname.endsWith("/")) pathname += "/";
	if (!basePath.endsWith("/")) basePath += "/";
	return pathname.startsWith(basePath);
}
function isValidHistoryApi(devServerConfig, req) {
	if (!devServerConfig.historyApiFallback) return false;
	if (req.method !== "GET") return false;
	if (!req.acceptHeader.includes("text/html")) return false;
	if (!devServerConfig.historyApiFallback.disableDotRule && req.pathname?.includes(".")) return false;
	return true;
}
const urlVersionIds = /* @__PURE__ */ new Map();
async function serveFile(devServerConfig, serverCtx, req, res) {
	try {
		if (isSimpleText(req.filePath)) {
			let content = await serverCtx.sys.readFile(req.filePath, "utf8");
			if (devServerConfig.websocket && isHtmlFile(req.filePath) && !isDevServerClient(req.pathname)) content = appendDevServerClientScript(devServerConfig, req, content);
			else if (isCssFile(req.filePath)) content = updateStyleUrls(req.url, content);
			if (shouldCompress(devServerConfig, req)) {
				res.writeHead(200, responseHeaders({
					"content-type": getContentType(req.filePath) + "; charset=utf-8",
					"content-encoding": "gzip",
					vary: "Accept-Encoding"
				}));
				zlib.gzip(content, { level: 9 }, (_, data) => {
					res.end(data);
				});
			} else {
				res.writeHead(200, responseHeaders({
					"content-type": getContentType(req.filePath) + "; charset=utf-8",
					"content-length": Buffer.byteLength(content, "utf8")
				}));
				res.write(content);
				res.end();
			}
		} else {
			const readStream = fs.createReadStream(req.filePath);
			readStream.on("error", (err) => {
				if (!res.headersSent) serverCtx.serve500(req, res, err, "serveFile");
				else res.end();
			});
			res.writeHead(200, responseHeaders({
				"content-type": getContentType(req.filePath),
				"content-length": req.stats.size
			}));
			readStream.pipe(res);
		}
		serverCtx.logRequest(req, 200);
	} catch (e) {
		serverCtx.serve500(req, res, e, "serveFile");
	}
}
function updateStyleUrls(url, oldCss) {
	const versionId = url.searchParams.get("s-hmr");
	const hmrUrls = url.searchParams.get("s-hmr-urls");
	if (versionId && hmrUrls) hmrUrls.split(",").forEach((hmrUrl) => {
		urlVersionIds.set(hmrUrl, versionId);
	});
	const reg = /url\((['"]?)(.*)\1\)/gi;
	let result;
	let newCss = oldCss;
	while ((result = reg.exec(oldCss)) !== null) {
		const oldUrl = result[2];
		const parsedUrl = new URL(oldUrl, url);
		const fileName = path.basename(parsedUrl.pathname);
		const cachedVersionId = urlVersionIds.get(fileName);
		if (!cachedVersionId) continue;
		parsedUrl.searchParams.set("s-hmr", cachedVersionId);
		newCss = newCss.replace(oldUrl, parsedUrl.pathname);
	}
	return newCss;
}
function appendDevServerClientScript(devServerConfig, req, content) {
	const devServerClientUrl = getDevServerClientUrl(devServerConfig, req.headers?.["x-forwarded-host"] ?? req.host, req.headers?.["x-forwarded-proto"]);
	return appendDevServerClientIframe(content, `<iframe title="Stencil Dev Server Connector ${VERSION} &#9889;" src="${devServerClientUrl}" style="display:block;width:0;height:0;border:0;visibility:hidden" aria-hidden="true"></iframe>`);
}
function appendDevServerClientIframe(content, iframe) {
	if (content.includes("</body>")) return content.replace("</body>", `${iframe}</body>`);
	if (content.includes("</html>")) return content.replace("</html>", `${iframe}</html>`);
	return `${content}${iframe}`;
}
async function serveDevClient(devServerConfig, serverCtx, req, res) {
	try {
		if (isOpenInEditor(req.pathname)) return serveOpenInEditor(serverCtx, req, res);
		if (isDevServerClient(req.pathname)) return serveDevClientScript(devServerConfig, serverCtx, req, res);
		if (isInitialDevServerLoad(req.pathname)) req.filePath = path.join(devServerConfig.devServerDir, "templates", "initial-load.html");
		else {
			const subPath = req.pathname.replace("/~dev-server/", "");
			if (subPath.startsWith("client/")) req.filePath = path.join(devServerConfig.devServerDir, subPath);
			else req.filePath = path.join(devServerConfig.devServerDir, "static", subPath);
		}
		try {
			req.stats = await serverCtx.sys.stat(req.filePath);
			if (req.stats.isFile) return serveFile(devServerConfig, serverCtx, req, res);
			return serverCtx.serve404(req, res, "serveDevClient not file");
		} catch (e) {
			return serverCtx.serve404(req, res, `serveDevClient stats error ${e}`);
		}
	} catch (e) {
		return serverCtx.serve500(req, res, e, "serveDevClient");
	}
}
async function serveDevClientScript(devServerConfig, serverCtx, req, res) {
	try {
		if (serverCtx.connectorHtml == null) {
			const filePath = path.join(devServerConfig.devServerDir, "connector.html");
			serverCtx.connectorHtml = serverCtx.sys.readFileSync(filePath, "utf8");
			if (typeof serverCtx.connectorHtml !== "string") return serverCtx.serve404(req, res, "serveDevClientScript");
			const devClientConfig = {
				basePath: devServerConfig.basePath,
				editors: await getEditors(),
				reloadStrategy: devServerConfig.reloadStrategy
			};
			serverCtx.connectorHtml = serverCtx.connectorHtml.replace("window.__DEV_CLIENT_CONFIG__", JSON.stringify(devClientConfig));
		}
		res.writeHead(200, responseHeaders({ "content-type": "text/html; charset=utf-8" }));
		res.write(serverCtx.connectorHtml);
		res.end();
	} catch (e) {
		return serverCtx.serve500(req, res, e, "serveDevClientScript");
	}
}
async function serveDevNodeModule(serverCtx, req, res) {
	try {
		const results = await serverCtx.getCompilerRequest(req.pathname);
		const headers = {
			"content-type": "application/javascript; charset=utf-8",
			"content-length": Buffer.byteLength(results.content, "utf8"),
			"x-dev-node-module-id": results.nodeModuleId,
			"x-dev-node-module-version": results.nodeModuleVersion,
			"x-dev-node-module-resolved-path": results.nodeResolvedPath,
			"x-dev-node-module-cache-path": results.cachePath,
			"x-dev-node-module-cache-hit": results.cacheHit
		};
		res.writeHead(results.status, responseHeaders(headers));
		res.write(results.content);
		res.end();
	} catch (e) {
		serverCtx.serve500(req, res, e, "serveDevNodeModule");
	}
}
async function serveDirectoryIndex(devServerConfig, serverCtx, req, res) {
	const indexFilePath = path.join(req.filePath, "index.html");
	req.stats = await serverCtx.sys.stat(indexFilePath);
	if (req.stats.isFile) {
		req.filePath = indexFilePath;
		return serveFile(devServerConfig, serverCtx, req, res);
	}
	if (!req.pathname.endsWith("/")) return serverCtx.serve302(req, res, req.pathname + "/");
	try {
		const dirFilePaths = await serverCtx.sys.readDir(req.filePath);
		if (dirFilePaths.some((f) => f.endsWith(".tsx")) && !await hasNestedHtmlFile(serverCtx.sys, dirFilePaths)) return serveDevPreview(devServerConfig, serverCtx, req, res, req.filePath);
		if (req.pathname === "/" || req.pathname === devServerConfig.basePath) {
			const srcPath = path.join(req.filePath, "src");
			try {
				if ((await serverCtx.sys.stat(srcPath)).isDirectory) return serverCtx.serve302(req, res, "/src/");
			} catch {}
		}
		try {
			if (serverCtx.dirTemplate == null) {
				const dirTemplatePath = path.join(devServerConfig.devServerDir, "templates", "directory-index.html");
				serverCtx.dirTemplate = serverCtx.sys.readFileSync(dirTemplatePath);
			}
			const files = await getDirectoryFiles(serverCtx.sys, req.url, dirFilePaths);
			const templateHtml = serverCtx.dirTemplate.replace("{{title}}", req.pathname).replace("{{nav}}", getDirectoryNav(req.pathname)).replace("{{files}}", files);
			serverCtx.logRequest(req, 200);
			res.writeHead(200, responseHeaders({
				"content-type": "text/html; charset=utf-8",
				"x-directory-index": req.pathname
			}));
			res.write(templateHtml);
			res.end();
		} catch (e) {
			return serverCtx.serve500(req, res, e, "serveDirectoryIndex");
		}
	} catch {
		return serverCtx.serve404(req, res, "serveDirectoryIndex");
	}
}
/**
* Recursively checks a directory (and its subdirectories) for any .html/.htm file.
* Used to decide whether the auto-generated component preview should be shown, or
* the default file-listing view (since a nested html file likely means a custom
* preview/demo page already exists somewhere in the tree).
* @param sys The dev server's compiler system, used to read directories and stat files.
* @param dirFilePaths The full paths of entries in the directory to check.
* @returns true if an html file exists at this level or in any subdirectory.
*/
async function hasNestedHtmlFile(sys, dirFilePaths) {
	const subDirPaths = [];
	for (const filePath of dirFilePaths) {
		if (filePath.endsWith(".html") || filePath.endsWith(".htm")) return true;
		if ((await sys.stat(filePath)).isDirectory) subDirPaths.push(filePath);
	}
	for (const subDirPath of subDirPaths) if (await hasNestedHtmlFile(sys, await sys.readDir(subDirPath))) return true;
	return false;
}
async function serveDevPreview(devServerConfig, serverCtx, req, res, filterDirPath) {
	try {
		const buildResults = await serverCtx.getBuildResults();
		const html = appendDevServerClientScript(devServerConfig, req, generateDevPreview(buildResults, filterDirPath));
		const buf = Buffer.from(html, "utf-8");
		serverCtx.logRequest(req, 200);
		res.writeHead(200, responseHeaders({
			"content-type": "text/html;charset=utf-8",
			"content-length": buf.byteLength
		}));
		res.end(buf);
	} catch (e) {
		serverCtx.serve500(req, res, e, "serveDevPreview");
	}
}
async function getDirectoryFiles(sys, baseUrl, dirItemNames) {
	const items = await getDirectoryItems(sys, baseUrl, dirItemNames);
	if (baseUrl.pathname !== "/") items.unshift({
		isDirectory: true,
		pathname: "../",
		name: ".."
	});
	return items.map((item) => {
		return `
        <li class="${item.isDirectory ? "directory" : "file"}">
          <a href="${item.pathname}">
            <span class="icon"></span>
            <span>${item.name}</span>
          </a>
        </li>`;
	}).join("");
}
async function getDirectoryItems(sys, baseUrl, dirFilePaths) {
	return await Promise.all(dirFilePaths.map(async (dirFilePath) => {
		const fileName = path.basename(dirFilePath);
		const url = new URL(fileName, baseUrl);
		const stats = await sys.stat(dirFilePath);
		return {
			name: fileName,
			pathname: url.pathname,
			isDirectory: stats.isDirectory
		};
	}));
}
function getDirectoryNav(pathName) {
	const dirs = pathName.split("/");
	dirs.pop();
	let url = "";
	return dirs.map((dir, index) => {
		url += dir + "/";
		return `<a href="${url}">${index === 0 ? "~" : dir}</a>`;
	}).join("<span>/</span>") + "<span>/</span>";
}
//#endregion
//#region src/server/server.ts
/**
* HTTP and WebSocket server.
* Consolidated from server-process.ts, server-http.ts, and server-web-socket.ts.
* Uses native Node 22+ WebSocket instead of the 'ws' package.
*/
function createHttpServer(devServerConfig, serverCtx) {
	const reqHandler = createRequestHandler(devServerConfig, serverCtx);
	const credentials = devServerConfig.https;
	return credentials ? https.createServer(credentials, reqHandler) : http.createServer(reqHandler);
}
async function findClosestOpenPort(host, port, strictPort = false) {
	if (!await isPortTaken(host, port)) return port;
	if (strictPort) throw new Error(`Port ${port} is already in use. Please specify a different port or set strictPort to false.`);
	async function findNext(portToCheck) {
		if (!await isPortTaken(host, portToCheck)) return portToCheck;
		return findNext(portToCheck + 1);
	}
	return findNext(port + 1);
}
function isPortTaken(host, port) {
	return new Promise((resolve, reject) => {
		const tester = net.createServer().once("error", () => {
			resolve(true);
		}).once("listening", () => {
			tester.once("close", () => resolve(false)).close();
		}).on("error", (err) => {
			reject(err);
		}).listen(port, host);
	});
}
function createWebSocket(httpServer, onMessageFromClient) {
	const wsServer = new WebSocketServer({ server: httpServer });
	wsServer.on("connection", (rawWs) => {
		const ws = rawWs;
		ws.isAlive = true;
		ws.on("message", (data) => {
			try {
				onMessageFromClient(JSON.parse(data.toString()));
			} catch (e) {
				console.error("WebSocket message parse error:", e);
			}
		});
		ws.on("pong", () => {
			ws.isAlive = true;
		});
		ws.on("error", (err) => {
			console.error("WebSocket error:", err);
		});
	});
	const pingInterval = setInterval(() => {
		wsServer.clients.forEach((ws) => {
			const devWs = ws;
			if (!devWs.isAlive) return devWs.close(1e3);
			devWs.isAlive = false;
			devWs.ping();
		});
	}, 1e4);
	return {
		sendToBrowser: (msg) => {
			if (msg && wsServer && wsServer.clients) {
				const data = JSON.stringify(msg);
				wsServer.clients.forEach((ws) => {
					if (ws.readyState === ws.OPEN) ws.send(data);
				});
			}
		},
		close: () => {
			return new Promise((resolve, reject) => {
				clearInterval(pingInterval);
				wsServer.clients.forEach((ws) => {
					ws.close(1e3);
				});
				wsServer.close((err) => {
					if (err) reject(err);
					else resolve();
				});
			});
		}
	};
}
function initServerProcess(sendMsg) {
	let server = null;
	let webSocket = null;
	let serverCtx = null;
	const buildResultsResolves = [];
	const compilerRequestResolves = [];
	const createNodeSys = async () => {
		const { createNodeSys: createSys } = await import("@stencil/core/sys/node");
		return createSys({ process });
	};
	const startServer = async (msg) => {
		const devServerConfig = msg.startServer;
		devServerConfig.port = await findClosestOpenPort(devServerConfig.address, devServerConfig.port, devServerConfig.strictPort);
		devServerConfig.browserUrl = getBrowserUrl(devServerConfig.protocol, devServerConfig.address, devServerConfig.port, devServerConfig.basePath, "/");
		devServerConfig.root = normalizePath(devServerConfig.root);
		serverCtx = createServerContext(await createNodeSys(), sendMsg, devServerConfig, buildResultsResolves, compilerRequestResolves);
		server = createHttpServer(devServerConfig, serverCtx);
		webSocket = devServerConfig.websocket ? createWebSocket(server, sendMsg) : null;
		server.listen(devServerConfig.port, devServerConfig.address);
		serverCtx.isServerListening = true;
		if (devServerConfig.openBrowser) openInBrowser({ url: getBrowserUrl(devServerConfig.protocol, devServerConfig.address, devServerConfig.port, devServerConfig.basePath, devServerConfig.initialLoadUrl || DEV_SERVER_INIT_URL) });
		sendMsg({ serverStarted: devServerConfig });
	};
	const closeServer = () => {
		const promises = [];
		buildResultsResolves.forEach((r) => r.reject("dev server closed"));
		buildResultsResolves.length = 0;
		compilerRequestResolves.forEach((r) => r.reject("dev server closed"));
		compilerRequestResolves.length = 0;
		if (serverCtx?.sys) promises.push(serverCtx.sys.destroy());
		if (webSocket) {
			promises.push(webSocket.close());
			webSocket = null;
		}
		if (server) promises.push(new Promise((resolve) => {
			server.close((err) => {
				if (err) console.error(`close error: ${err}`);
				resolve();
			});
		}));
		Promise.all(promises).finally(() => {
			sendMsg({ serverClosed: true });
		});
	};
	const receiveMessageFromMain = (msg) => {
		try {
			if (msg) {
				if (msg.startServer) startServer(msg);
				else if (msg.closeServer) closeServer();
				else if (msg.compilerRequestResults) for (let i = compilerRequestResolves.length - 1; i >= 0; i--) {
					const r = compilerRequestResolves[i];
					if (r.path === msg.compilerRequestResults.path) {
						r.resolve(msg.compilerRequestResults);
						compilerRequestResolves.splice(i, 1);
					}
				}
				else if (serverCtx) {
					if (msg.buildResults && !msg.isActivelyBuilding) {
						buildResultsResolves.forEach((r) => r.resolve(msg.buildResults));
						buildResultsResolves.length = 0;
					}
					if (webSocket) webSocket.sendToBrowser(msg);
				}
			}
		} catch (e) {
			let stack = null;
			if (e instanceof Error) stack = e.stack ?? null;
			sendMsg({ error: {
				message: String(e),
				stack
			} });
		}
	};
	return receiveMessageFromMain;
}
//#endregion
//#region src/server/worker-main.ts
/**
* Worker process proxy for dev server.
* Forks a child process to run the HTTP and WebSocket server in isolation.
*/
/**
* Initialize the dev server in a forked worker process.
* This provides process isolation so that server crashes don't affect the main compiler.
*
* @param sendToMain - Callback to send messages from worker to main process
* @returns Function to send messages from main to worker process
*/
function initServerProcessWorkerProxy(sendToMain) {
	const workerPath = path.join(import.meta.dirname, "worker-thread.js");
	const forkOpts = {
		execArgv: process.execArgv.filter((v) => !/^--(debug|inspect)/.test(v)),
		env: process.env,
		cwd: process.cwd(),
		stdio: [
			"pipe",
			"pipe",
			"pipe",
			"ipc"
		]
	};
	let serverProcess = fork(workerPath, [], forkOpts);
	/**
	* Send a message from main to the worker process
	*
	* @param msg - the message to send to the worker
	*/
	const receiveFromMain = (msg) => {
		if (serverProcess && serverProcess.connected) serverProcess.send(msg);
		else if (msg.closeServer) sendToMain({ serverClosed: true });
	};
	serverProcess.on("message", (msg) => {
		if (msg.serverClosed && serverProcess) {
			serverProcess.kill("SIGINT");
			serverProcess = null;
		}
		sendToMain(msg);
	});
	serverProcess.stdout?.on("data", (data) => {
		console.log(`dev server: ${data}`);
	});
	serverProcess.stderr?.on("data", (data) => {
		sendToMain({ error: {
			message: "stderr: " + data.toString(),
			type: "stderr",
			stack: null
		} });
	});
	serverProcess.on("error", (error) => {
		sendToMain({ error: {
			message: error.message,
			type: "worker-error",
			stack: error.stack || null
		} });
	});
	serverProcess.on("exit", (code) => {
		if (code !== 0 && code !== null) sendToMain({ error: {
			message: `Worker process exited with code ${code}`,
			type: "worker-exit",
			stack: null
		} });
		if (serverProcess) {
			serverProcess = null;
			sendToMain({ serverClosed: true });
		}
	});
	return receiveFromMain;
}
//#endregion
//#region src/server/index.ts
/**
* Stencil Dev Server
*
* A modern development server for Stencil with DOM-based HMR.
* Designed for lazy-loading component architectures where module graphs
* are discovered at runtime from the DOM.
* @module @stencil/dev-server
*/
/**
* Start the Stencil development server.
*
* @param stencilDevServerConfig - Configuration for the dev server
* @param logger - Logger instance for output
* @param watcher - Optional compiler watcher for build events
* @returns Promise resolving to the DevServer instance
*/
function start(stencilDevServerConfig, logger, watcher) {
	return new Promise(async (resolve, reject) => {
		try {
			const devServerConfig = {
				devServerDir: import.meta.dirname,
				...stencilDevServerConfig
			};
			if (!path.isAbsolute(devServerConfig.root)) devServerConfig.root = path.join(process.cwd(), devServerConfig.root);
			let initServerProcessFn;
			if (stencilDevServerConfig.worker === true || stencilDevServerConfig.worker === void 0) initServerProcessFn = initServerProcessWorkerProxy;
			else initServerProcessFn = initServerProcess;
			startServer(devServerConfig, logger, watcher, initServerProcessFn, resolve, reject);
		} catch (e) {
			reject(e);
		}
	});
}
/**
* Internal function to start the dev server.
*
* @param devServerConfig - configuration for the dev server
* @param logger - logger instance for output
* @param watcher - optional compiler watcher for build events
* @param initServerProcessFn - function to initialize the server process
* @param resolve - promise resolve callback
* @param reject - promise reject callback
*/
function startServer(devServerConfig, logger, watcher, initServerProcessFn, resolve, reject) {
	const timespan = logger.createTimeSpan("starting dev server", true);
	const startupTimeout = logger.getLevel() !== "debug" || devServerConfig.startupTimeout !== 0 ? setTimeout(() => {
		reject("dev server startup timeout");
	}, devServerConfig.startupTimeout ?? 15e3) : null;
	let isActivelyBuilding = false;
	let lastBuildResults = null;
	let devServer = null;
	let removeWatcher = null;
	let closeResolve = null;
	let hasStarted = false;
	let browserUrl = "";
	let sendToServer = null;
	const closePromise = new Promise((res) => {
		closeResolve = res;
	});
	const close = async () => {
		if (startupTimeout) clearTimeout(startupTimeout);
		isActivelyBuilding = false;
		if (removeWatcher) removeWatcher();
		if (devServer) devServer = null;
		if (sendToServer) {
			sendToServer({ closeServer: true });
			sendToServer = null;
		}
		return closePromise;
	};
	const emit = (eventName, data) => {
		if (sendToServer) {
			if (eventName === "buildFinish") {
				isActivelyBuilding = false;
				lastBuildResults = { ...data };
				sendToServer({
					buildResults: { ...lastBuildResults },
					isActivelyBuilding
				});
			} else if (eventName === "buildLog") sendToServer({ buildLog: { ...data } });
			else if (eventName === "buildStart") isActivelyBuilding = true;
		}
	};
	const serverStarted = (msg) => {
		hasStarted = true;
		if (startupTimeout) clearTimeout(startupTimeout);
		devServerConfig = msg.serverStarted;
		devServer = {
			address: devServerConfig.address,
			basePath: devServerConfig.basePath,
			browserUrl: devServerConfig.browserUrl,
			protocol: devServerConfig.protocol,
			port: devServerConfig.port,
			root: devServerConfig.root,
			emit,
			close
		};
		browserUrl = devServerConfig.browserUrl;
		timespan.finish(`dev server started: ${browserUrl}`);
		resolve(devServer);
	};
	const requestLog = (msg) => {
		if (devServerConfig.logRequests && msg.requestLog) {
			if (msg.requestLog.status >= 500) logger.info(logger.red(`${msg.requestLog.method} ${msg.requestLog.url} (${msg.requestLog.status})`));
			else if (msg.requestLog.status >= 400) logger.info(logger.dim(logger.red(`${msg.requestLog.method} ${msg.requestLog.url} (${msg.requestLog.status})`)));
			else if (msg.requestLog.status >= 300) logger.info(logger.dim(logger.magenta(`${msg.requestLog.method} ${msg.requestLog.url} (${msg.requestLog.status})`)));
			else logger.info(logger.dim(`${logger.cyan(msg.requestLog.method)} ${msg.requestLog.url}`));
		}
	};
	const serverError = async (msg) => {
		if (msg.error) {
			if (hasStarted) logger.error(msg.error.message + " " + msg.error.stack);
			else {
				await close();
				reject(msg.error.message);
			}
		}
	};
	const requestBuildResults = () => {
		if (sendToServer) {
			if (lastBuildResults != null) {
				const msg = {
					buildResults: { ...lastBuildResults },
					isActivelyBuilding
				};
				delete msg.buildResults.hmr;
				sendToServer(msg);
			} else sendToServer({ isActivelyBuilding: true });
		}
	};
	const compilerRequest = async (compilerRequestPath) => {
		if (watcher?.request && sendToServer) {
			const compilerRequestResults = await watcher.request({ path: compilerRequestPath });
			sendToServer({ compilerRequestResults });
		}
	};
	const receiveFromServer = (msg) => {
		try {
			if (msg.serverStarted) serverStarted(msg);
			else if (msg.serverClosed) {
				logger.debug(`dev server closed: ${browserUrl}`);
				closeResolve?.();
			} else if (msg.requestBuildResults) requestBuildResults();
			else if (msg.compilerRequestPath) compilerRequest(msg.compilerRequestPath);
			else if (msg.requestLog) requestLog(msg);
			else if (msg.error) serverError(msg);
			else logger.debug(`server msg not handled: ${JSON.stringify(msg)}`);
		} catch (e) {
			logger.error("receiveFromServer: " + e);
		}
	};
	try {
		if (watcher) removeWatcher = watcher.on(emit);
		sendToServer = initServerProcessFn(receiveFromServer);
		sendToServer({ startServer: devServerConfig });
	} catch (e) {
		close();
		reject(e);
	}
}
//#endregion
export { initServerProcess, start };