@stencil/dev-server 0.0.19-1 → 5.0.0-alpha.2

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/index.mjs ADDED
@@ -0,0 +1,1395 @@
1
+ import * as path from "node:path";
2
+ import * as http from "node:http";
3
+ import * as https from "node:https";
4
+ import * as net from "node:net";
5
+ import { WebSocketServer } from "ws";
6
+ import * as fs from "node:fs";
7
+ import { inspect } from "node:util";
8
+ import { pathToFileURL } from "node:url";
9
+ import * as zlib from "node:zlib";
10
+ import { fork } from "node:child_process";
11
+ //#region src/server/utils.ts
12
+ const DEV_SERVER_URL = "/~dev-server";
13
+ const DEV_MODULE_URL = "/~dev-module";
14
+ const DEV_SERVER_INIT_URL = `${DEV_SERVER_URL}-init`;
15
+ const OPEN_IN_EDITOR_URL = `${DEV_SERVER_URL}-open-in-editor`;
16
+ const VERSION = "5.0.0";
17
+ const DEFAULT_HEADERS = {
18
+ "cache-control": "no-cache, no-store, must-revalidate, max-age=0",
19
+ expires: "0",
20
+ date: "Wed, 1 Jan 2000 00:00:00 GMT",
21
+ server: `Stencil Dev Server ${VERSION}`,
22
+ "access-control-allow-origin": "*",
23
+ "access-control-expose-headers": "*"
24
+ };
25
+ /**
26
+ * Build response headers with optional HTTP caching.
27
+ *
28
+ * @param headers - custom headers to merge with defaults
29
+ * @param httpCache - whether to enable HTTP caching
30
+ * @returns the combined response headers
31
+ */
32
+ function responseHeaders(headers, httpCache = false) {
33
+ const result = {
34
+ ...DEFAULT_HEADERS,
35
+ ...headers
36
+ };
37
+ if (httpCache) {
38
+ result["cache-control"] = "max-age=3600";
39
+ delete result["date"];
40
+ delete result["expires"];
41
+ }
42
+ return result;
43
+ }
44
+ /**
45
+ * Build a browser URL from components.
46
+ *
47
+ * @param protocol - the URL protocol (http or https)
48
+ * @param address - the server address
49
+ * @param port - the server port
50
+ * @param basePath - the base path
51
+ * @param pathname - the URL pathname
52
+ * @returns the complete browser URL
53
+ */
54
+ function getBrowserUrl(protocol, address, port, basePath, pathname) {
55
+ address = address === "0.0.0.0" ? "localhost" : address;
56
+ const portSuffix = !port || port === 80 || port === 443 ? "" : ":" + port;
57
+ let path = basePath;
58
+ if (pathname.startsWith("/")) pathname = pathname.substring(1);
59
+ path += pathname;
60
+ protocol = protocol.replace(/:/g, "");
61
+ return `${protocol}://${address}${portSuffix}${path}`;
62
+ }
63
+ /**
64
+ * Get the URL for the dev server client script.
65
+ *
66
+ * @param devServerConfig - the dev server configuration
67
+ * @param host - optional host override
68
+ * @param protocol - optional protocol override
69
+ * @returns the dev server client URL
70
+ */
71
+ function getDevServerClientUrl(devServerConfig, host, protocol) {
72
+ let address = devServerConfig.address;
73
+ let port = devServerConfig.port;
74
+ if (host) {
75
+ address = host;
76
+ port = null;
77
+ }
78
+ return getBrowserUrl(protocol ?? devServerConfig.protocol, address, port, devServerConfig.basePath, DEV_SERVER_URL);
79
+ }
80
+ const CONTENT_TYPES = {
81
+ html: "text/html",
82
+ htm: "text/html",
83
+ css: "text/css",
84
+ js: "text/javascript",
85
+ mjs: "text/javascript",
86
+ json: "application/json",
87
+ xml: "application/xml",
88
+ svg: "image/svg+xml",
89
+ png: "image/png",
90
+ jpg: "image/jpeg",
91
+ jpeg: "image/jpeg",
92
+ gif: "image/gif",
93
+ webp: "image/webp",
94
+ ico: "image/x-icon",
95
+ woff: "font/woff",
96
+ woff2: "font/woff2",
97
+ ttf: "font/ttf",
98
+ otf: "font/otf",
99
+ eot: "application/vnd.ms-fontobject",
100
+ mp3: "audio/mpeg",
101
+ mp4: "video/mp4",
102
+ webm: "video/webm",
103
+ ogg: "audio/ogg",
104
+ wav: "audio/wav",
105
+ pdf: "application/pdf",
106
+ zip: "application/zip",
107
+ wasm: "application/wasm",
108
+ map: "application/json",
109
+ txt: "text/plain",
110
+ md: "text/markdown",
111
+ ts: "text/typescript",
112
+ tsx: "text/typescript-jsx"
113
+ };
114
+ /**
115
+ * Get the content type for a file based on its extension.
116
+ *
117
+ * @param filePath - the file path to check
118
+ * @returns the MIME content type
119
+ */
120
+ function getContentType(filePath) {
121
+ const last = filePath.replace(/^.*[/\\]/, "").toLowerCase();
122
+ const ext = last.replace(/^.*\./, "").toLowerCase();
123
+ const hasPath = last.length < filePath.length;
124
+ return (ext.length < last.length - 1 || !hasPath) && CONTENT_TYPES[ext] || "application/octet-stream";
125
+ }
126
+ /**
127
+ * Check if a file is an HTML file.
128
+ *
129
+ * @param filePath - the file path to check
130
+ * @returns true if the file is HTML
131
+ */
132
+ function isHtmlFile(filePath) {
133
+ const lower = filePath.toLowerCase().trim();
134
+ return lower.endsWith(".html") || lower.endsWith(".htm");
135
+ }
136
+ /**
137
+ * Check if a file is a CSS file.
138
+ *
139
+ * @param filePath - the file path to check
140
+ * @returns true if the file is CSS
141
+ */
142
+ function isCssFile(filePath) {
143
+ return filePath.toLowerCase().trim().endsWith(".css");
144
+ }
145
+ const TXT_EXT = [
146
+ "css",
147
+ "html",
148
+ "htm",
149
+ "js",
150
+ "json",
151
+ "svg",
152
+ "xml",
153
+ "mjs",
154
+ "ts",
155
+ "tsx",
156
+ "md",
157
+ "txt"
158
+ ];
159
+ /**
160
+ * Check if a file is simple text (CSS, HTML, JS, JSON, etc.).
161
+ *
162
+ * @param filePath - the file path to check
163
+ * @returns true if the file is a simple text format
164
+ */
165
+ function isSimpleText(filePath) {
166
+ const ext = filePath.toLowerCase().trim().split(".").pop();
167
+ return ext ? TXT_EXT.includes(ext) : false;
168
+ }
169
+ /**
170
+ * Check if a pathname has no file extension.
171
+ *
172
+ * @param pathname - the URL pathname to check
173
+ * @returns true if the path has no extension
174
+ */
175
+ function isExtensionLessPath(pathname) {
176
+ const parts = pathname.split("/");
177
+ return !parts[parts.length - 1].includes(".");
178
+ }
179
+ /**
180
+ * Check if a pathname is for SSR static data (page.state.json).
181
+ *
182
+ * @param pathname - the URL pathname to check
183
+ * @returns true if the path is for SSR static data
184
+ */
185
+ function isSsrStaticDataPath(pathname) {
186
+ const parts = pathname.split("/");
187
+ return parts[parts.length - 1].split("?")[0] === "page.state.json";
188
+ }
189
+ /**
190
+ * Extract SSR static data path information from an HTTP request.
191
+ *
192
+ * @param req - the HTTP request object
193
+ * @returns an object containing ssrPath, fileName, and hasQueryString
194
+ */
195
+ function getSsrStaticDataPath(req) {
196
+ const parts = req.url.href.split("/");
197
+ const fileNameParts = parts[parts.length - 1].split("?");
198
+ parts.pop();
199
+ let ssrPath = new URL(parts.join("/")).href;
200
+ if (!ssrPath.endsWith("/") && req.headers) {
201
+ if (new Headers(req.headers).get("referer")?.endsWith("/")) ssrPath += "/";
202
+ }
203
+ return {
204
+ ssrPath,
205
+ fileName: fileNameParts[0],
206
+ hasQueryString: typeof fileNameParts[1] === "string" && fileNameParts[1].length > 0
207
+ };
208
+ }
209
+ /**
210
+ * Check if a pathname is for the dev client.
211
+ *
212
+ * @param pathname - the URL pathname to check
213
+ * @returns true if the path is for the dev client
214
+ */
215
+ function isDevClient(pathname) {
216
+ return pathname.startsWith(DEV_SERVER_URL);
217
+ }
218
+ /**
219
+ * Check if a pathname is for a dev module.
220
+ *
221
+ * @param pathname - the URL pathname to check
222
+ * @returns true if the path is for a dev module
223
+ */
224
+ function isDevModule(pathname) {
225
+ return pathname.includes(DEV_MODULE_URL);
226
+ }
227
+ /**
228
+ * Check if a pathname is for the open-in-editor endpoint.
229
+ *
230
+ * @param pathname - the URL pathname to check
231
+ * @returns true if the path is for opening in editor
232
+ */
233
+ function isOpenInEditor(pathname) {
234
+ return pathname === OPEN_IN_EDITOR_URL;
235
+ }
236
+ /**
237
+ * Check if a pathname is for the initial dev server load.
238
+ *
239
+ * @param pathname - the URL pathname to check
240
+ * @returns true if the path is for initial dev server load
241
+ */
242
+ function isInitialDevServerLoad(pathname) {
243
+ return pathname === DEV_SERVER_INIT_URL;
244
+ }
245
+ /**
246
+ * Check if a pathname is for the dev server client script.
247
+ *
248
+ * @param pathname - the URL pathname to check
249
+ * @returns true if the path is for the dev server client
250
+ */
251
+ function isDevServerClient(pathname) {
252
+ return pathname === DEV_SERVER_URL;
253
+ }
254
+ /**
255
+ * Check if a response should be gzip compressed.
256
+ *
257
+ * @param devServerConfig - the dev server configuration
258
+ * @param req - the HTTP request object
259
+ * @returns true if the response should be compressed
260
+ */
261
+ function shouldCompress(devServerConfig, req) {
262
+ if (!devServerConfig.gzip) return false;
263
+ if (req.method !== "GET") return false;
264
+ const acceptEncoding = req.headers?.["accept-encoding"];
265
+ if (typeof acceptEncoding !== "string") return false;
266
+ return acceptEncoding.includes("gzip");
267
+ }
268
+ /**
269
+ * Normalize a file path to use forward slashes and remove redundant slashes.
270
+ *
271
+ * @param path - the file path to normalize
272
+ * @returns the normalized path
273
+ */
274
+ function normalizePath(path) {
275
+ let normalized = path.replace(/\\/g, "/");
276
+ normalized = normalized.replace(/\/+/g, "/");
277
+ if (path.startsWith("\\\\")) normalized = "/" + normalized;
278
+ return normalized;
279
+ }
280
+ //#endregion
281
+ //#region src/server/context.ts
282
+ /**
283
+ * Server context factory.
284
+ * Creates the shared context object passed to request handlers.
285
+ */
286
+ function createServerContext(sys, sendMsg, devServerConfig, buildResultsResolves, compilerRequestResolves) {
287
+ const logRequest = (req, status) => {
288
+ if (devServerConfig) sendMsg({ requestLog: {
289
+ method: req.method || "?",
290
+ url: req.pathname || "?",
291
+ status
292
+ } });
293
+ };
294
+ const serve500 = (req, res, error, xSource) => {
295
+ try {
296
+ if (res.headersSent) {
297
+ res.end();
298
+ return;
299
+ }
300
+ res.writeHead(500, responseHeaders({
301
+ "content-type": "text/plain; charset=utf-8",
302
+ "x-source": xSource
303
+ }));
304
+ res.write(inspect(error));
305
+ res.end();
306
+ logRequest(req, 500);
307
+ } catch (e) {
308
+ sendMsg({ error: { message: "serve500: " + e } });
309
+ }
310
+ };
311
+ const serve404 = (req, res, xSource, content = null) => {
312
+ try {
313
+ if (res.headersSent) {
314
+ res.end();
315
+ return;
316
+ }
317
+ if (req.pathname === "/favicon.ico") {
318
+ const defaultFavicon = path.join(devServerConfig.devServerDir, "static", "favicon.ico");
319
+ const rs = fs.createReadStream(defaultFavicon);
320
+ rs.on("error", () => {
321
+ if (!res.headersSent) res.writeHead(404);
322
+ res.end();
323
+ });
324
+ res.writeHead(200, responseHeaders({
325
+ "content-type": "image/x-icon",
326
+ "x-source": `favicon: ${xSource}`
327
+ }));
328
+ rs.pipe(res);
329
+ return;
330
+ }
331
+ if (content == null) content = [
332
+ "404 File Not Found",
333
+ "Url: " + req.pathname,
334
+ "File: " + req.filePath
335
+ ].join("\n");
336
+ res.writeHead(404, responseHeaders({
337
+ "content-type": "text/plain; charset=utf-8",
338
+ "x-source": xSource
339
+ }));
340
+ res.write(content);
341
+ res.end();
342
+ logRequest(req, 404);
343
+ } catch (e) {
344
+ serve500(req, res, e, xSource);
345
+ }
346
+ };
347
+ const serve302 = (req, res, pathname = null) => {
348
+ logRequest(req, 302);
349
+ res.writeHead(302, { location: pathname || devServerConfig.basePath || "/" });
350
+ res.end();
351
+ };
352
+ const getBuildResults = () => new Promise((resolve, reject) => {
353
+ if (serverCtx.isServerListening) {
354
+ buildResultsResolves.push({
355
+ resolve,
356
+ reject
357
+ });
358
+ sendMsg({ requestBuildResults: true });
359
+ } else reject("dev server closed");
360
+ });
361
+ const getCompilerRequest = (compilerRequestPath) => new Promise((resolve, reject) => {
362
+ if (serverCtx.isServerListening) {
363
+ compilerRequestResolves.push({
364
+ path: compilerRequestPath,
365
+ resolve,
366
+ reject
367
+ });
368
+ sendMsg({ compilerRequestPath });
369
+ } else reject("dev server closed");
370
+ });
371
+ const serverCtx = {
372
+ connectorHtml: null,
373
+ dirTemplate: null,
374
+ getBuildResults,
375
+ getCompilerRequest,
376
+ isServerListening: false,
377
+ logRequest,
378
+ prerenderConfig: null,
379
+ serve302,
380
+ serve404,
381
+ serve500,
382
+ sys
383
+ };
384
+ return serverCtx;
385
+ }
386
+ //#endregion
387
+ //#region src/server/editor.ts
388
+ /**
389
+ * Editor integration using launch-editor.
390
+ * Consolidated from open-in-browser.ts, open-in-editor.ts, and open-in-editor-api.ts.
391
+ */
392
+ async function openInBrowser(opts) {
393
+ const { default: open } = await import("open");
394
+ await open(opts.url);
395
+ }
396
+ let launchEditorLoaded = false;
397
+ let launchEditor = null;
398
+ async function loadLaunchEditor() {
399
+ if (launchEditorLoaded) return;
400
+ try {
401
+ const mod = await import("launch-editor");
402
+ launchEditor = mod.default || mod;
403
+ } catch {
404
+ console.warn("launch-editor package is not available. Open in editor functionality will be disabled.");
405
+ launchEditor = null;
406
+ }
407
+ launchEditorLoaded = true;
408
+ }
409
+ async function serveOpenInEditor(serverCtx, req, res) {
410
+ let status = 200;
411
+ const data = {};
412
+ try {
413
+ await parseEditorData(serverCtx.sys, req, data);
414
+ await openDataInEditor(data);
415
+ } catch (e) {
416
+ data.error = String(e);
417
+ status = 500;
418
+ }
419
+ serverCtx.logRequest(req, status);
420
+ res.writeHead(status, responseHeaders({ "content-type": "application/json; charset=utf-8" }));
421
+ res.write(JSON.stringify(data, null, 2));
422
+ res.end();
423
+ }
424
+ async function parseEditorData(sys, req, data) {
425
+ const qs = req.searchParams;
426
+ if (!qs.has("file")) {
427
+ data.error = "missing file";
428
+ return;
429
+ }
430
+ data.file = qs.get("file");
431
+ if (qs.has("line") && !isNaN(Number(qs.get("line")))) data.line = parseInt(qs.get("line"), 10);
432
+ if (typeof data.line !== "number" || data.line < 1) data.line = 1;
433
+ if (qs.has("column") && !isNaN(Number(qs.get("column")))) data.column = parseInt(qs.get("column"), 10);
434
+ if (typeof data.column !== "number" || data.column < 1) data.column = 1;
435
+ if (qs.has("editor")) data.editor = qs.get("editor");
436
+ data.exists = (await sys.stat(data.file)).isFile;
437
+ }
438
+ async function openDataInEditor(data) {
439
+ if (!data.exists || data.error) return;
440
+ await loadLaunchEditor();
441
+ if (!launchEditor) {
442
+ data.error = "launch-editor not available";
443
+ return;
444
+ }
445
+ try {
446
+ const fileSpec = `${data.file}:${data.line}:${data.column}`;
447
+ await new Promise((resolve, reject) => {
448
+ let errorCalled = false;
449
+ launchEditor(fileSpec, data.editor || process.env.EDITOR, (_fileName, errorMessage) => {
450
+ errorCalled = true;
451
+ const errMsg = errorMessage || "Unknown error";
452
+ console.error("Editor launch failed.");
453
+ console.error("The \"code\" executable was not found in your PATH.");
454
+ console.error("This usually means your editor's command-line tool isn't installed.");
455
+ console.error("Try running:");
456
+ console.error(" code --version");
457
+ console.error("If that fails, install your editor's CLI command and ensure it's in your PATH.\n");
458
+ data.error = errMsg;
459
+ reject(new Error(errMsg));
460
+ });
461
+ setTimeout(() => {
462
+ if (!errorCalled) {
463
+ data.open = fileSpec;
464
+ resolve();
465
+ }
466
+ }, 100);
467
+ });
468
+ } catch (e) {
469
+ if (!data.error) data.error = String(e);
470
+ }
471
+ }
472
+ function getEditors() {
473
+ return Promise.resolve([
474
+ {
475
+ id: "code",
476
+ name: "Visual Studio Code"
477
+ },
478
+ {
479
+ id: "cursor",
480
+ name: "Cursor"
481
+ },
482
+ {
483
+ id: "code-insiders",
484
+ name: "VS Code Insiders"
485
+ },
486
+ {
487
+ id: "webstorm",
488
+ name: "WebStorm"
489
+ },
490
+ {
491
+ id: "idea",
492
+ name: "IntelliJ IDEA"
493
+ },
494
+ {
495
+ id: "sublime",
496
+ name: "Sublime Text"
497
+ },
498
+ {
499
+ id: "atom",
500
+ name: "Atom"
501
+ },
502
+ {
503
+ id: "vim",
504
+ name: "Vim"
505
+ },
506
+ {
507
+ id: "emacs",
508
+ name: "Emacs"
509
+ }
510
+ ]);
511
+ }
512
+ //#endregion
513
+ //#region src/server/ssr.ts
514
+ /**
515
+ * SSR (Server-Side Rendering) request handling.
516
+ * Migrated from ssr-request.ts.
517
+ */
518
+ async function ssrPageRequest(devServerConfig, serverCtx, req, res) {
519
+ try {
520
+ let status = 500;
521
+ let content = "";
522
+ const { hydrateApp, srcIndexHtml, diagnostics } = await setupHydrateApp(devServerConfig, serverCtx);
523
+ if (!diagnostics.some((diagnostic) => diagnostic.level === "error")) try {
524
+ const opts = getSsrHydrateOptions(devServerConfig, serverCtx, req.url);
525
+ const ssrResults = await hydrateApp.renderToString(srcIndexHtml, opts);
526
+ diagnostics.push(...ssrResults.diagnostics);
527
+ status = ssrResults.httpStatus ?? 500;
528
+ content = ssrResults.html ?? "";
529
+ } catch (e) {
530
+ catchError(diagnostics, e);
531
+ }
532
+ if (diagnostics.some((diagnostic) => diagnostic.level === "error")) {
533
+ content = getSsrErrorContent(diagnostics);
534
+ status = 500;
535
+ }
536
+ if (devServerConfig.websocket) content = appendDevServerClientScript(devServerConfig, req, content);
537
+ serverCtx.logRequest(req, status);
538
+ res.writeHead(status, responseHeaders({
539
+ "content-type": "text/html; charset=utf-8",
540
+ "content-length": Buffer.byteLength(content, "utf8")
541
+ }));
542
+ res.write(content);
543
+ res.end();
544
+ } catch (e) {
545
+ serverCtx.serve500(req, res, e, "ssrPageRequest");
546
+ }
547
+ }
548
+ async function ssrStaticDataRequest(devServerConfig, serverCtx, req, res) {
549
+ try {
550
+ const data = {};
551
+ let httpCache = false;
552
+ const { hydrateApp, srcIndexHtml, diagnostics } = await setupHydrateApp(devServerConfig, serverCtx);
553
+ if (!diagnostics.some((diagnostic) => diagnostic.level === "error")) try {
554
+ const { ssrPath, hasQueryString } = getSsrStaticDataPath(req);
555
+ const opts = getSsrHydrateOptions(devServerConfig, serverCtx, new URL(ssrPath, req.url));
556
+ const ssrResults = await hydrateApp.renderToString(srcIndexHtml, opts);
557
+ diagnostics.push(...ssrResults.diagnostics);
558
+ ssrResults.staticData.forEach((s) => {
559
+ if (s.type === "application/json") data[s.id] = JSON.parse(s.content);
560
+ else data[s.id] = s.content;
561
+ });
562
+ data.components = ssrResults.components.map((c) => c.tag).sort();
563
+ httpCache = hasQueryString;
564
+ } catch (e) {
565
+ catchError(diagnostics, e);
566
+ }
567
+ if (diagnostics.length > 0) data.diagnostics = diagnostics;
568
+ const status = diagnostics.some((diagnostic) => diagnostic.level === "error") ? 500 : 200;
569
+ const content = JSON.stringify(data);
570
+ serverCtx.logRequest(req, status);
571
+ res.writeHead(status, responseHeaders({
572
+ "content-type": "application/json; charset=utf-8",
573
+ "content-length": Buffer.byteLength(content, "utf8")
574
+ }, httpCache && status === 200));
575
+ res.write(content);
576
+ res.end();
577
+ } catch (e) {
578
+ serverCtx.serve500(req, res, e, "ssrStaticDataRequest");
579
+ }
580
+ }
581
+ async function setupHydrateApp(devServerConfig, serverCtx) {
582
+ let srcIndexHtml = null;
583
+ let hydrateApp = null;
584
+ const buildResults = await serverCtx.getBuildResults();
585
+ const diagnostics = [];
586
+ if (serverCtx.prerenderConfig == null && isString(devServerConfig.prerenderConfig)) try {
587
+ const prerenderConfigResults = (await import("@stencil/core/compiler")).nodeRequire(devServerConfig.prerenderConfig);
588
+ diagnostics.push(...prerenderConfigResults.diagnostics);
589
+ if (prerenderConfigResults.module?.config) serverCtx.prerenderConfig = prerenderConfigResults.module.config;
590
+ } catch (e) {
591
+ catchError(diagnostics, e);
592
+ }
593
+ if (!isString(buildResults.hydrateAppFilePath)) diagnostics.push({
594
+ messageText: "Missing hydrateAppFilePath",
595
+ level: "error",
596
+ type: "ssr",
597
+ lines: []
598
+ });
599
+ else if (!isString(devServerConfig.srcIndexHtml)) diagnostics.push({
600
+ messageText: "Missing srcIndexHtml",
601
+ level: "error",
602
+ type: "ssr",
603
+ lines: []
604
+ });
605
+ else {
606
+ srcIndexHtml = await serverCtx.sys.readFile(devServerConfig.srcIndexHtml, "utf8");
607
+ if (!isString(srcIndexHtml)) diagnostics.push({
608
+ level: "error",
609
+ lines: [],
610
+ messageText: `Unable to load src index html: ${devServerConfig.srcIndexHtml}`,
611
+ type: "ssr"
612
+ });
613
+ else {
614
+ const hydrateAppFilePath = path.resolve(buildResults.hydrateAppFilePath);
615
+ try {
616
+ const hydrateUrl = pathToFileURL(hydrateAppFilePath);
617
+ hydrateUrl.search = `?t=${Date.now()}`;
618
+ const hydrateModule = await import(hydrateUrl.href);
619
+ hydrateApp = hydrateModule.default || hydrateModule;
620
+ } catch (e) {
621
+ catchError(diagnostics, e);
622
+ }
623
+ }
624
+ }
625
+ return {
626
+ hydrateApp,
627
+ srcIndexHtml,
628
+ diagnostics
629
+ };
630
+ }
631
+ function getSsrHydrateOptions(devServerConfig, serverCtx, url) {
632
+ const opts = {
633
+ url: url.href,
634
+ addModulePreloads: false,
635
+ approximateLineWidth: 120,
636
+ inlineExternalStyleSheets: false,
637
+ minifyScriptElements: false,
638
+ minifyStyleElements: false,
639
+ removeAttributeQuotes: false,
640
+ removeBooleanAttributeQuotes: false,
641
+ removeEmptyAttributes: false,
642
+ removeHtmlComments: false,
643
+ prettyHtml: true
644
+ };
645
+ const prerenderConfig = serverCtx?.prerenderConfig;
646
+ if (isFunction(prerenderConfig?.hydrateOptions)) {
647
+ const userOpts = prerenderConfig.hydrateOptions(url);
648
+ if (userOpts) Object.assign(opts, userOpts);
649
+ }
650
+ if (isFunction(serverCtx.sys.applyPrerenderGlobalPatch)) {
651
+ const orgBeforeHydrate = opts.beforeHydrate;
652
+ const applyPatch = serverCtx.sys.applyPrerenderGlobalPatch;
653
+ opts.beforeHydrate = (document) => {
654
+ const devServerHostUrl = new URL(devServerConfig.browserUrl).origin;
655
+ applyPatch({
656
+ devServerHostUrl,
657
+ window: document.defaultView
658
+ });
659
+ if (typeof orgBeforeHydrate === "function") return orgBeforeHydrate(document);
660
+ };
661
+ }
662
+ return opts;
663
+ }
664
+ function getSsrErrorContent(diagnostics) {
665
+ return `<!doctype html>
666
+ <html>
667
+ <head>
668
+ <title>SSR Error</title>
669
+ <style>
670
+ body {
671
+ font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace !important;
672
+ }
673
+ </style>
674
+ </head>
675
+ <body>
676
+ <h1>SSR Dev Error</h1>
677
+ ${diagnostics.map((diagnostic) => `
678
+ <p>
679
+ ${diagnostic.messageText}
680
+ </p>
681
+ `).join("")}
682
+ </body>
683
+ </html>`;
684
+ }
685
+ function catchError(diagnostics, err) {
686
+ const diagnostic = {
687
+ level: "error",
688
+ type: "runtime",
689
+ messageText: "",
690
+ lines: []
691
+ };
692
+ if (err instanceof Error) {
693
+ diagnostic.messageText = err.message;
694
+ if (err.stack) diagnostic.messageText += "\n" + err.stack;
695
+ } else diagnostic.messageText = String(err);
696
+ diagnostics.push(diagnostic);
697
+ }
698
+ function isString(val) {
699
+ return typeof val === "string";
700
+ }
701
+ function isFunction(val) {
702
+ return typeof val === "function";
703
+ }
704
+ //#endregion
705
+ //#region src/server/handlers.ts
706
+ /**
707
+ * Request handlers.
708
+ * Consolidated from request-handler.ts, serve-file.ts, serve-dev-client.ts,
709
+ * serve-dev-node-module.ts, and serve-directory-index.ts.
710
+ */
711
+ function createRequestHandler(devServerConfig, serverCtx) {
712
+ let userRequestHandler = null;
713
+ let userHandlerLoaded = false;
714
+ return async function(incomingReq, res) {
715
+ if (!userHandlerLoaded && typeof devServerConfig.requestListenerPath === "string") {
716
+ userHandlerLoaded = true;
717
+ try {
718
+ const userModule = await import(pathToFileURL(devServerConfig.requestListenerPath).href);
719
+ userRequestHandler = userModule.default || userModule;
720
+ } catch (e) {
721
+ console.error("Failed to load user request handler:", e);
722
+ }
723
+ }
724
+ async function defaultHandler() {
725
+ try {
726
+ const req = normalizeHttpRequest(devServerConfig, incomingReq);
727
+ if (!req.url) return serverCtx.serve302(req, res);
728
+ if (devServerConfig.pingRoute !== null && req.pathname === devServerConfig.pingRoute) {
729
+ try {
730
+ if (!(await serverCtx.getBuildResults()).hasSuccessfulBuild) return serverCtx.serve500(req, res, "Build not successful", "build error");
731
+ res.writeHead(200, "OK");
732
+ res.write("OK");
733
+ res.end();
734
+ } catch {
735
+ serverCtx.serve500(req, res, "Error getting build results", "ping error");
736
+ }
737
+ return;
738
+ }
739
+ if (isDevClient(req.pathname) && devServerConfig.websocket) return serveDevClient(devServerConfig, serverCtx, req, res);
740
+ if (isDevModule(req.pathname)) return serveDevNodeModule(serverCtx, req, res);
741
+ if (!isValidUrlBasePath(devServerConfig.basePath, req.url)) return serverCtx.serve404(req, res, "invalid basePath", `404 File Not Found, base path: ${devServerConfig.basePath}`);
742
+ if (devServerConfig.ssr) {
743
+ if (isExtensionLessPath(req.url.pathname)) return ssrPageRequest(devServerConfig, serverCtx, req, res);
744
+ if (isSsrStaticDataPath(req.url.pathname)) return ssrStaticDataRequest(devServerConfig, serverCtx, req, res);
745
+ }
746
+ req.stats = await serverCtx.sys.stat(req.filePath);
747
+ if (req.stats.isFile) return serveFile(devServerConfig, serverCtx, req, res);
748
+ if (req.stats.isDirectory) return serveDirectoryIndex(devServerConfig, serverCtx, req, res);
749
+ const xSource = ["notfound"];
750
+ const validHistoryApi = isValidHistoryApi(devServerConfig, req);
751
+ xSource.push(`validHistoryApi: ${validHistoryApi}`);
752
+ if (validHistoryApi) try {
753
+ const indexFilePath = path.join(devServerConfig.root, devServerConfig.historyApiFallback.index);
754
+ xSource.push(`indexFilePath: ${indexFilePath}`);
755
+ req.stats = await serverCtx.sys.stat(indexFilePath);
756
+ if (req.stats.isFile) {
757
+ req.filePath = indexFilePath;
758
+ return serveFile(devServerConfig, serverCtx, req, res);
759
+ }
760
+ } catch (e) {
761
+ xSource.push(`notfound error: ${e}`);
762
+ }
763
+ return serverCtx.serve404(req, res, xSource.join(", "));
764
+ } catch (e) {
765
+ const errorReq = {
766
+ method: (incomingReq.method || "GET").toUpperCase(),
767
+ acceptHeader: "",
768
+ url: null,
769
+ searchParams: null
770
+ };
771
+ return serverCtx.serve500(errorReq, res, e, "not found error");
772
+ }
773
+ }
774
+ if (typeof userRequestHandler === "function") await userRequestHandler(incomingReq, res, defaultHandler);
775
+ else await defaultHandler();
776
+ };
777
+ }
778
+ function normalizeHttpRequest(devServerConfig, incomingReq) {
779
+ const req = {
780
+ method: (incomingReq.method || "GET").toUpperCase(),
781
+ headers: incomingReq.headers,
782
+ acceptHeader: incomingReq.headers && typeof incomingReq.headers.accept === "string" && incomingReq.headers.accept || "",
783
+ host: incomingReq.headers && typeof incomingReq.headers.host === "string" && incomingReq.headers.host || void 0,
784
+ url: null,
785
+ searchParams: null
786
+ };
787
+ if ((incomingReq.url || "").trim() || null) {
788
+ if (req.host) req.url = new URL(incomingReq.url, `http://${req.host}`);
789
+ else req.url = new URL(incomingReq.url, "http://dev.stenciljs.com");
790
+ req.searchParams = req.url.searchParams;
791
+ }
792
+ if (req.url) {
793
+ req.pathname = req.url.pathname.replace(/\\/g, "/").split("/").map((part) => decodeURIComponent(part)).join("/");
794
+ if (req.pathname.length > 0 && !isDevClient(req.pathname)) req.pathname = "/" + req.pathname.substring(devServerConfig.basePath.length);
795
+ req.filePath = normalizePath(path.normalize(path.join(devServerConfig.root, path.relative("/", req.pathname))));
796
+ }
797
+ return req;
798
+ }
799
+ function isValidUrlBasePath(basePath, url) {
800
+ let pathname = url.pathname;
801
+ if (!pathname.endsWith("/")) pathname += "/";
802
+ if (!basePath.endsWith("/")) basePath += "/";
803
+ return pathname.startsWith(basePath);
804
+ }
805
+ function isValidHistoryApi(devServerConfig, req) {
806
+ if (!devServerConfig.historyApiFallback) return false;
807
+ if (req.method !== "GET") return false;
808
+ if (!req.acceptHeader.includes("text/html")) return false;
809
+ if (!devServerConfig.historyApiFallback.disableDotRule && req.pathname?.includes(".")) return false;
810
+ return true;
811
+ }
812
+ const urlVersionIds = /* @__PURE__ */ new Map();
813
+ async function serveFile(devServerConfig, serverCtx, req, res) {
814
+ try {
815
+ if (isSimpleText(req.filePath)) {
816
+ let content = await serverCtx.sys.readFile(req.filePath, "utf8");
817
+ if (devServerConfig.websocket && isHtmlFile(req.filePath) && !isDevServerClient(req.pathname)) content = appendDevServerClientScript(devServerConfig, req, content);
818
+ else if (isCssFile(req.filePath)) content = updateStyleUrls(req.url, content);
819
+ if (shouldCompress(devServerConfig, req)) {
820
+ res.writeHead(200, responseHeaders({
821
+ "content-type": getContentType(req.filePath) + "; charset=utf-8",
822
+ "content-encoding": "gzip",
823
+ vary: "Accept-Encoding"
824
+ }));
825
+ zlib.gzip(content, { level: 9 }, (_, data) => {
826
+ res.end(data);
827
+ });
828
+ } else {
829
+ res.writeHead(200, responseHeaders({
830
+ "content-type": getContentType(req.filePath) + "; charset=utf-8",
831
+ "content-length": Buffer.byteLength(content, "utf8")
832
+ }));
833
+ res.write(content);
834
+ res.end();
835
+ }
836
+ } else {
837
+ const readStream = fs.createReadStream(req.filePath);
838
+ readStream.on("error", (err) => {
839
+ if (!res.headersSent) serverCtx.serve500(req, res, err, "serveFile");
840
+ else res.end();
841
+ });
842
+ res.writeHead(200, responseHeaders({
843
+ "content-type": getContentType(req.filePath),
844
+ "content-length": req.stats.size
845
+ }));
846
+ readStream.pipe(res);
847
+ }
848
+ serverCtx.logRequest(req, 200);
849
+ } catch (e) {
850
+ serverCtx.serve500(req, res, e, "serveFile");
851
+ }
852
+ }
853
+ function updateStyleUrls(url, oldCss) {
854
+ const versionId = url.searchParams.get("s-hmr");
855
+ const hmrUrls = url.searchParams.get("s-hmr-urls");
856
+ if (versionId && hmrUrls) hmrUrls.split(",").forEach((hmrUrl) => {
857
+ urlVersionIds.set(hmrUrl, versionId);
858
+ });
859
+ const reg = /url\((['"]?)(.*)\1\)/gi;
860
+ let result;
861
+ let newCss = oldCss;
862
+ while ((result = reg.exec(oldCss)) !== null) {
863
+ const oldUrl = result[2];
864
+ const parsedUrl = new URL(oldUrl, url);
865
+ const fileName = path.basename(parsedUrl.pathname);
866
+ const cachedVersionId = urlVersionIds.get(fileName);
867
+ if (!cachedVersionId) continue;
868
+ parsedUrl.searchParams.set("s-hmr", cachedVersionId);
869
+ newCss = newCss.replace(oldUrl, parsedUrl.pathname);
870
+ }
871
+ return newCss;
872
+ }
873
+ function appendDevServerClientScript(devServerConfig, req, content) {
874
+ return appendDevServerClientIframe(content, `<iframe title="Stencil Dev Server Connector ${VERSION} &#9889;" src="${getDevServerClientUrl(devServerConfig, req.headers?.["x-forwarded-host"] ?? req.host, req.headers?.["x-forwarded-proto"])}" style="display:block;width:0;height:0;border:0;visibility:hidden" aria-hidden="true"></iframe>`);
875
+ }
876
+ function appendDevServerClientIframe(content, iframe) {
877
+ if (content.includes("</body>")) return content.replace("</body>", `${iframe}</body>`);
878
+ if (content.includes("</html>")) return content.replace("</html>", `${iframe}</html>`);
879
+ return `${content}${iframe}`;
880
+ }
881
+ async function serveDevClient(devServerConfig, serverCtx, req, res) {
882
+ try {
883
+ if (isOpenInEditor(req.pathname)) return serveOpenInEditor(serverCtx, req, res);
884
+ if (isDevServerClient(req.pathname)) return serveDevClientScript(devServerConfig, serverCtx, req, res);
885
+ if (isInitialDevServerLoad(req.pathname)) req.filePath = path.join(devServerConfig.devServerDir, "templates", "initial-load.html");
886
+ else {
887
+ const subPath = req.pathname.replace(DEV_SERVER_URL + "/", "");
888
+ if (subPath.startsWith("client/")) req.filePath = path.join(devServerConfig.devServerDir, subPath);
889
+ else req.filePath = path.join(devServerConfig.devServerDir, "static", subPath);
890
+ }
891
+ try {
892
+ req.stats = await serverCtx.sys.stat(req.filePath);
893
+ if (req.stats.isFile) return serveFile(devServerConfig, serverCtx, req, res);
894
+ return serverCtx.serve404(req, res, "serveDevClient not file");
895
+ } catch (e) {
896
+ return serverCtx.serve404(req, res, `serveDevClient stats error ${e}`);
897
+ }
898
+ } catch (e) {
899
+ return serverCtx.serve500(req, res, e, "serveDevClient");
900
+ }
901
+ }
902
+ async function serveDevClientScript(devServerConfig, serverCtx, req, res) {
903
+ try {
904
+ if (serverCtx.connectorHtml == null) {
905
+ const filePath = path.join(devServerConfig.devServerDir, "connector.html");
906
+ serverCtx.connectorHtml = serverCtx.sys.readFileSync(filePath, "utf8");
907
+ if (typeof serverCtx.connectorHtml !== "string") return serverCtx.serve404(req, res, "serveDevClientScript");
908
+ const devClientConfig = {
909
+ basePath: devServerConfig.basePath,
910
+ editors: await getEditors(),
911
+ reloadStrategy: devServerConfig.reloadStrategy
912
+ };
913
+ serverCtx.connectorHtml = serverCtx.connectorHtml.replace("window.__DEV_CLIENT_CONFIG__", JSON.stringify(devClientConfig));
914
+ }
915
+ res.writeHead(200, responseHeaders({ "content-type": "text/html; charset=utf-8" }));
916
+ res.write(serverCtx.connectorHtml);
917
+ res.end();
918
+ } catch (e) {
919
+ return serverCtx.serve500(req, res, e, "serveDevClientScript");
920
+ }
921
+ }
922
+ async function serveDevNodeModule(serverCtx, req, res) {
923
+ try {
924
+ const results = await serverCtx.getCompilerRequest(req.pathname);
925
+ const headers = {
926
+ "content-type": "application/javascript; charset=utf-8",
927
+ "content-length": Buffer.byteLength(results.content, "utf8"),
928
+ "x-dev-node-module-id": results.nodeModuleId,
929
+ "x-dev-node-module-version": results.nodeModuleVersion,
930
+ "x-dev-node-module-resolved-path": results.nodeResolvedPath,
931
+ "x-dev-node-module-cache-path": results.cachePath,
932
+ "x-dev-node-module-cache-hit": results.cacheHit
933
+ };
934
+ res.writeHead(results.status, responseHeaders(headers));
935
+ res.write(results.content);
936
+ res.end();
937
+ } catch (e) {
938
+ serverCtx.serve500(req, res, e, "serveDevNodeModule");
939
+ }
940
+ }
941
+ async function serveDirectoryIndex(devServerConfig, serverCtx, req, res) {
942
+ const indexFilePath = path.join(req.filePath, "index.html");
943
+ req.stats = await serverCtx.sys.stat(indexFilePath);
944
+ if (req.stats.isFile) {
945
+ req.filePath = indexFilePath;
946
+ return serveFile(devServerConfig, serverCtx, req, res);
947
+ }
948
+ if (!req.pathname.endsWith("/")) return serverCtx.serve302(req, res, req.pathname + "/");
949
+ try {
950
+ const dirFilePaths = await serverCtx.sys.readDir(req.filePath);
951
+ try {
952
+ if (serverCtx.dirTemplate == null) {
953
+ const dirTemplatePath = path.join(devServerConfig.devServerDir, "templates", "directory-index.html");
954
+ serverCtx.dirTemplate = serverCtx.sys.readFileSync(dirTemplatePath);
955
+ }
956
+ const files = await getDirectoryFiles(serverCtx.sys, req.url, dirFilePaths);
957
+ const templateHtml = serverCtx.dirTemplate.replace("{{title}}", req.pathname).replace("{{nav}}", getDirectoryNav(req.pathname)).replace("{{files}}", files);
958
+ serverCtx.logRequest(req, 200);
959
+ res.writeHead(200, responseHeaders({
960
+ "content-type": "text/html; charset=utf-8",
961
+ "x-directory-index": req.pathname
962
+ }));
963
+ res.write(templateHtml);
964
+ res.end();
965
+ } catch (e) {
966
+ return serverCtx.serve500(req, res, e, "serveDirectoryIndex");
967
+ }
968
+ } catch {
969
+ return serverCtx.serve404(req, res, "serveDirectoryIndex");
970
+ }
971
+ }
972
+ async function getDirectoryFiles(sys, baseUrl, dirItemNames) {
973
+ const items = await getDirectoryItems(sys, baseUrl, dirItemNames);
974
+ if (baseUrl.pathname !== "/") items.unshift({
975
+ isDirectory: true,
976
+ pathname: "../",
977
+ name: ".."
978
+ });
979
+ return items.map((item) => {
980
+ return `
981
+ <li class="${item.isDirectory ? "directory" : "file"}">
982
+ <a href="${item.pathname}">
983
+ <span class="icon"></span>
984
+ <span>${item.name}</span>
985
+ </a>
986
+ </li>`;
987
+ }).join("");
988
+ }
989
+ async function getDirectoryItems(sys, baseUrl, dirFilePaths) {
990
+ return await Promise.all(dirFilePaths.map(async (dirFilePath) => {
991
+ const fileName = path.basename(dirFilePath);
992
+ const url = new URL(fileName, baseUrl);
993
+ const stats = await sys.stat(dirFilePath);
994
+ return {
995
+ name: fileName,
996
+ pathname: url.pathname,
997
+ isDirectory: stats.isDirectory
998
+ };
999
+ }));
1000
+ }
1001
+ function getDirectoryNav(pathName) {
1002
+ const dirs = pathName.split("/");
1003
+ dirs.pop();
1004
+ let url = "";
1005
+ return dirs.map((dir, index) => {
1006
+ url += dir + "/";
1007
+ return `<a href="${url}">${index === 0 ? "~" : dir}</a>`;
1008
+ }).join("<span>/</span>") + "<span>/</span>";
1009
+ }
1010
+ //#endregion
1011
+ //#region src/server/server.ts
1012
+ /**
1013
+ * HTTP and WebSocket server.
1014
+ * Consolidated from server-process.ts, server-http.ts, and server-web-socket.ts.
1015
+ * Uses native Node 22+ WebSocket instead of the 'ws' package.
1016
+ */
1017
+ function createHttpServer(devServerConfig, serverCtx) {
1018
+ const reqHandler = createRequestHandler(devServerConfig, serverCtx);
1019
+ const credentials = devServerConfig.https;
1020
+ return credentials ? https.createServer(credentials, reqHandler) : http.createServer(reqHandler);
1021
+ }
1022
+ async function findClosestOpenPort(host, port, strictPort = false) {
1023
+ if (!await isPortTaken(host, port)) return port;
1024
+ if (strictPort) throw new Error(`Port ${port} is already in use. Please specify a different port or set strictPort to false.`);
1025
+ async function findNext(portToCheck) {
1026
+ if (!await isPortTaken(host, portToCheck)) return portToCheck;
1027
+ return findNext(portToCheck + 1);
1028
+ }
1029
+ return findNext(port + 1);
1030
+ }
1031
+ function isPortTaken(host, port) {
1032
+ return new Promise((resolve, reject) => {
1033
+ const tester = net.createServer().once("error", () => {
1034
+ resolve(true);
1035
+ }).once("listening", () => {
1036
+ tester.once("close", () => resolve(false)).close();
1037
+ }).on("error", (err) => {
1038
+ reject(err);
1039
+ }).listen(port, host);
1040
+ });
1041
+ }
1042
+ function createWebSocket(httpServer, onMessageFromClient) {
1043
+ const wsServer = new WebSocketServer({ server: httpServer });
1044
+ wsServer.on("connection", (rawWs) => {
1045
+ const ws = rawWs;
1046
+ ws.isAlive = true;
1047
+ ws.on("message", (data) => {
1048
+ try {
1049
+ onMessageFromClient(JSON.parse(data.toString()));
1050
+ } catch (e) {
1051
+ console.error("WebSocket message parse error:", e);
1052
+ }
1053
+ });
1054
+ ws.on("pong", () => {
1055
+ ws.isAlive = true;
1056
+ });
1057
+ ws.on("error", (err) => {
1058
+ console.error("WebSocket error:", err);
1059
+ });
1060
+ });
1061
+ const pingInterval = setInterval(() => {
1062
+ wsServer.clients.forEach((ws) => {
1063
+ const devWs = ws;
1064
+ if (!devWs.isAlive) return devWs.close(1e3);
1065
+ devWs.isAlive = false;
1066
+ devWs.ping();
1067
+ });
1068
+ }, 1e4);
1069
+ return {
1070
+ sendToBrowser: (msg) => {
1071
+ if (msg && wsServer && wsServer.clients) {
1072
+ const data = JSON.stringify(msg);
1073
+ wsServer.clients.forEach((ws) => {
1074
+ if (ws.readyState === ws.OPEN) ws.send(data);
1075
+ });
1076
+ }
1077
+ },
1078
+ close: () => {
1079
+ return new Promise((resolve, reject) => {
1080
+ clearInterval(pingInterval);
1081
+ wsServer.clients.forEach((ws) => {
1082
+ ws.close(1e3);
1083
+ });
1084
+ wsServer.close((err) => {
1085
+ if (err) reject(err);
1086
+ else resolve();
1087
+ });
1088
+ });
1089
+ }
1090
+ };
1091
+ }
1092
+ function initServerProcess(sendMsg) {
1093
+ let server = null;
1094
+ let webSocket = null;
1095
+ let serverCtx = null;
1096
+ const buildResultsResolves = [];
1097
+ const compilerRequestResolves = [];
1098
+ const createNodeSys = async () => {
1099
+ const { createNodeSys: createSys } = await import("@stencil/core/sys/node");
1100
+ return createSys({ process });
1101
+ };
1102
+ const startServer = async (msg) => {
1103
+ const devServerConfig = msg.startServer;
1104
+ devServerConfig.port = await findClosestOpenPort(devServerConfig.address, devServerConfig.port, devServerConfig.strictPort);
1105
+ devServerConfig.browserUrl = getBrowserUrl(devServerConfig.protocol, devServerConfig.address, devServerConfig.port, devServerConfig.basePath, "/");
1106
+ devServerConfig.root = normalizePath(devServerConfig.root);
1107
+ serverCtx = createServerContext(await createNodeSys(), sendMsg, devServerConfig, buildResultsResolves, compilerRequestResolves);
1108
+ server = createHttpServer(devServerConfig, serverCtx);
1109
+ webSocket = devServerConfig.websocket ? createWebSocket(server, sendMsg) : null;
1110
+ server.listen(devServerConfig.port, devServerConfig.address);
1111
+ serverCtx.isServerListening = true;
1112
+ if (devServerConfig.openBrowser) openInBrowser({ url: getBrowserUrl(devServerConfig.protocol, devServerConfig.address, devServerConfig.port, devServerConfig.basePath, devServerConfig.initialLoadUrl || DEV_SERVER_INIT_URL) });
1113
+ sendMsg({ serverStarted: devServerConfig });
1114
+ };
1115
+ const closeServer = () => {
1116
+ const promises = [];
1117
+ buildResultsResolves.forEach((r) => r.reject("dev server closed"));
1118
+ buildResultsResolves.length = 0;
1119
+ compilerRequestResolves.forEach((r) => r.reject("dev server closed"));
1120
+ compilerRequestResolves.length = 0;
1121
+ if (serverCtx?.sys) promises.push(serverCtx.sys.destroy());
1122
+ if (webSocket) {
1123
+ promises.push(webSocket.close());
1124
+ webSocket = null;
1125
+ }
1126
+ if (server) promises.push(new Promise((resolve) => {
1127
+ server.close((err) => {
1128
+ if (err) console.error(`close error: ${err}`);
1129
+ resolve();
1130
+ });
1131
+ }));
1132
+ Promise.all(promises).finally(() => {
1133
+ sendMsg({ serverClosed: true });
1134
+ });
1135
+ };
1136
+ const receiveMessageFromMain = (msg) => {
1137
+ try {
1138
+ if (msg) {
1139
+ if (msg.startServer) startServer(msg);
1140
+ else if (msg.closeServer) closeServer();
1141
+ else if (msg.compilerRequestResults) for (let i = compilerRequestResolves.length - 1; i >= 0; i--) {
1142
+ const r = compilerRequestResolves[i];
1143
+ if (r.path === msg.compilerRequestResults.path) {
1144
+ r.resolve(msg.compilerRequestResults);
1145
+ compilerRequestResolves.splice(i, 1);
1146
+ }
1147
+ }
1148
+ else if (serverCtx) {
1149
+ if (msg.buildResults && !msg.isActivelyBuilding) {
1150
+ buildResultsResolves.forEach((r) => r.resolve(msg.buildResults));
1151
+ buildResultsResolves.length = 0;
1152
+ }
1153
+ if (webSocket) webSocket.sendToBrowser(msg);
1154
+ }
1155
+ }
1156
+ } catch (e) {
1157
+ let stack = null;
1158
+ if (e instanceof Error) stack = e.stack ?? null;
1159
+ sendMsg({ error: {
1160
+ message: String(e),
1161
+ stack
1162
+ } });
1163
+ }
1164
+ };
1165
+ return receiveMessageFromMain;
1166
+ }
1167
+ //#endregion
1168
+ //#region src/server/worker-main.ts
1169
+ /**
1170
+ * Worker process proxy for dev server.
1171
+ * Forks a child process to run the HTTP and WebSocket server in isolation.
1172
+ */
1173
+ /**
1174
+ * Initialize the dev server in a forked worker process.
1175
+ * This provides process isolation so that server crashes don't affect the main compiler.
1176
+ *
1177
+ * @param sendToMain - Callback to send messages from worker to main process
1178
+ * @returns Function to send messages from main to worker process
1179
+ */
1180
+ function initServerProcessWorkerProxy(sendToMain) {
1181
+ let serverProcess = fork(path.join(import.meta.dirname, "worker-thread.js"), [], {
1182
+ execArgv: process.execArgv.filter((v) => !/^--(debug|inspect)/.test(v)),
1183
+ env: process.env,
1184
+ cwd: process.cwd(),
1185
+ stdio: [
1186
+ "pipe",
1187
+ "pipe",
1188
+ "pipe",
1189
+ "ipc"
1190
+ ]
1191
+ });
1192
+ /**
1193
+ * Send a message from main to the worker process
1194
+ *
1195
+ * @param msg - the message to send to the worker
1196
+ */
1197
+ const receiveFromMain = (msg) => {
1198
+ if (serverProcess && serverProcess.connected) serverProcess.send(msg);
1199
+ else if (msg.closeServer) sendToMain({ serverClosed: true });
1200
+ };
1201
+ serverProcess.on("message", (msg) => {
1202
+ if (msg.serverClosed && serverProcess) {
1203
+ serverProcess.kill("SIGINT");
1204
+ serverProcess = null;
1205
+ }
1206
+ sendToMain(msg);
1207
+ });
1208
+ serverProcess.stdout?.on("data", (data) => {
1209
+ console.log(`dev server: ${data}`);
1210
+ });
1211
+ serverProcess.stderr?.on("data", (data) => {
1212
+ sendToMain({ error: {
1213
+ message: "stderr: " + data.toString(),
1214
+ type: "stderr",
1215
+ stack: null
1216
+ } });
1217
+ });
1218
+ serverProcess.on("error", (error) => {
1219
+ sendToMain({ error: {
1220
+ message: error.message,
1221
+ type: "worker-error",
1222
+ stack: error.stack || null
1223
+ } });
1224
+ });
1225
+ serverProcess.on("exit", (code) => {
1226
+ if (code !== 0 && code !== null) sendToMain({ error: {
1227
+ message: `Worker process exited with code ${code}`,
1228
+ type: "worker-exit",
1229
+ stack: null
1230
+ } });
1231
+ if (serverProcess) {
1232
+ serverProcess = null;
1233
+ sendToMain({ serverClosed: true });
1234
+ }
1235
+ });
1236
+ return receiveFromMain;
1237
+ }
1238
+ //#endregion
1239
+ //#region src/server/index.ts
1240
+ /**
1241
+ * Stencil Dev Server
1242
+ *
1243
+ * A modern development server for Stencil with DOM-based HMR.
1244
+ * Designed for lazy-loading component architectures where module graphs
1245
+ * are discovered at runtime from the DOM.
1246
+ * @module @stencil/dev-server
1247
+ */
1248
+ /**
1249
+ * Start the Stencil development server.
1250
+ *
1251
+ * @param stencilDevServerConfig - Configuration for the dev server
1252
+ * @param logger - Logger instance for output
1253
+ * @param watcher - Optional compiler watcher for build events
1254
+ * @returns Promise resolving to the DevServer instance
1255
+ */
1256
+ function start(stencilDevServerConfig, logger, watcher) {
1257
+ return new Promise(async (resolve, reject) => {
1258
+ try {
1259
+ const devServerConfig = {
1260
+ devServerDir: import.meta.dirname,
1261
+ ...stencilDevServerConfig
1262
+ };
1263
+ if (!path.isAbsolute(devServerConfig.root)) devServerConfig.root = path.join(process.cwd(), devServerConfig.root);
1264
+ let initServerProcessFn;
1265
+ if (stencilDevServerConfig.worker === true || stencilDevServerConfig.worker === void 0) initServerProcessFn = initServerProcessWorkerProxy;
1266
+ else initServerProcessFn = initServerProcess;
1267
+ startServer(devServerConfig, logger, watcher, initServerProcessFn, resolve, reject);
1268
+ } catch (e) {
1269
+ reject(e);
1270
+ }
1271
+ });
1272
+ }
1273
+ /**
1274
+ * Internal function to start the dev server.
1275
+ *
1276
+ * @param devServerConfig - configuration for the dev server
1277
+ * @param logger - logger instance for output
1278
+ * @param watcher - optional compiler watcher for build events
1279
+ * @param initServerProcessFn - function to initialize the server process
1280
+ * @param resolve - promise resolve callback
1281
+ * @param reject - promise reject callback
1282
+ */
1283
+ function startServer(devServerConfig, logger, watcher, initServerProcessFn, resolve, reject) {
1284
+ const timespan = logger.createTimeSpan("starting dev server", true);
1285
+ const startupTimeout = logger.getLevel() !== "debug" || devServerConfig.startupTimeout !== 0 ? setTimeout(() => {
1286
+ reject("dev server startup timeout");
1287
+ }, devServerConfig.startupTimeout ?? 15e3) : null;
1288
+ let isActivelyBuilding = false;
1289
+ let lastBuildResults = null;
1290
+ let devServer = null;
1291
+ let removeWatcher = null;
1292
+ let closeResolve = null;
1293
+ let hasStarted = false;
1294
+ let browserUrl = "";
1295
+ let sendToServer = null;
1296
+ const closePromise = new Promise((res) => {
1297
+ closeResolve = res;
1298
+ });
1299
+ const close = async () => {
1300
+ if (startupTimeout) clearTimeout(startupTimeout);
1301
+ isActivelyBuilding = false;
1302
+ if (removeWatcher) removeWatcher();
1303
+ if (devServer) devServer = null;
1304
+ if (sendToServer) {
1305
+ sendToServer({ closeServer: true });
1306
+ sendToServer = null;
1307
+ }
1308
+ return closePromise;
1309
+ };
1310
+ const emit = (eventName, data) => {
1311
+ if (sendToServer) {
1312
+ if (eventName === "buildFinish") {
1313
+ isActivelyBuilding = false;
1314
+ lastBuildResults = { ...data };
1315
+ sendToServer({
1316
+ buildResults: { ...lastBuildResults },
1317
+ isActivelyBuilding
1318
+ });
1319
+ } else if (eventName === "buildLog") sendToServer({ buildLog: { ...data } });
1320
+ else if (eventName === "buildStart") isActivelyBuilding = true;
1321
+ }
1322
+ };
1323
+ const serverStarted = (msg) => {
1324
+ hasStarted = true;
1325
+ if (startupTimeout) clearTimeout(startupTimeout);
1326
+ devServerConfig = msg.serverStarted;
1327
+ devServer = {
1328
+ address: devServerConfig.address,
1329
+ basePath: devServerConfig.basePath,
1330
+ browserUrl: devServerConfig.browserUrl,
1331
+ protocol: devServerConfig.protocol,
1332
+ port: devServerConfig.port,
1333
+ root: devServerConfig.root,
1334
+ emit,
1335
+ close
1336
+ };
1337
+ browserUrl = devServerConfig.browserUrl;
1338
+ timespan.finish(`dev server started: ${browserUrl}`);
1339
+ resolve(devServer);
1340
+ };
1341
+ const requestLog = (msg) => {
1342
+ if (devServerConfig.logRequests && msg.requestLog) if (msg.requestLog.status >= 500) logger.info(logger.red(`${msg.requestLog.method} ${msg.requestLog.url} (${msg.requestLog.status})`));
1343
+ else if (msg.requestLog.status >= 400) logger.info(logger.dim(logger.red(`${msg.requestLog.method} ${msg.requestLog.url} (${msg.requestLog.status})`)));
1344
+ else if (msg.requestLog.status >= 300) logger.info(logger.dim(logger.magenta(`${msg.requestLog.method} ${msg.requestLog.url} (${msg.requestLog.status})`)));
1345
+ else logger.info(logger.dim(`${logger.cyan(msg.requestLog.method)} ${msg.requestLog.url}`));
1346
+ };
1347
+ const serverError = async (msg) => {
1348
+ if (msg.error) if (hasStarted) logger.error(msg.error.message + " " + msg.error.stack);
1349
+ else {
1350
+ await close();
1351
+ reject(msg.error.message);
1352
+ }
1353
+ };
1354
+ const requestBuildResults = () => {
1355
+ if (sendToServer) if (lastBuildResults != null) {
1356
+ const msg = {
1357
+ buildResults: { ...lastBuildResults },
1358
+ isActivelyBuilding
1359
+ };
1360
+ delete msg.buildResults.hmr;
1361
+ sendToServer(msg);
1362
+ } else sendToServer({ isActivelyBuilding: true });
1363
+ };
1364
+ const compilerRequest = async (compilerRequestPath) => {
1365
+ if (watcher?.request && sendToServer) {
1366
+ const compilerRequestResults = await watcher.request({ path: compilerRequestPath });
1367
+ sendToServer({ compilerRequestResults });
1368
+ }
1369
+ };
1370
+ const receiveFromServer = (msg) => {
1371
+ try {
1372
+ if (msg.serverStarted) serverStarted(msg);
1373
+ else if (msg.serverClosed) {
1374
+ logger.debug(`dev server closed: ${browserUrl}`);
1375
+ closeResolve?.();
1376
+ } else if (msg.requestBuildResults) requestBuildResults();
1377
+ else if (msg.compilerRequestPath) compilerRequest(msg.compilerRequestPath);
1378
+ else if (msg.requestLog) requestLog(msg);
1379
+ else if (msg.error) serverError(msg);
1380
+ else logger.debug(`server msg not handled: ${JSON.stringify(msg)}`);
1381
+ } catch (e) {
1382
+ logger.error("receiveFromServer: " + e);
1383
+ }
1384
+ };
1385
+ try {
1386
+ if (watcher) removeWatcher = watcher.on(emit);
1387
+ sendToServer = initServerProcessFn(receiveFromServer);
1388
+ sendToServer({ startServer: devServerConfig });
1389
+ } catch (e) {
1390
+ close();
1391
+ reject(e);
1392
+ }
1393
+ }
1394
+ //#endregion
1395
+ export { initServerProcess, start };