@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/{LICENSE → LICENSE.md} +12 -6
- package/dist/client/index.d.ts +132 -0
- package/dist/client/index.js +1076 -0
- package/dist/connector.html +34 -0
- package/dist/index.d.mts +52 -0
- package/dist/index.mjs +1395 -0
- package/dist/static/favicon.ico +0 -0
- package/dist/templates/directory-index.html +186 -0
- package/dist/templates/initial-load.html +157 -0
- package/dist/worker-thread.js +63 -0
- package/package.json +50 -51
- package/static/favicon.ico +0 -0
- package/templates/directory-index.html +186 -0
- package/templates/initial-load.html +157 -0
- package/README.md +0 -60
- package/assets/404.html +0 -14
- package/assets/__stencil-dev-server__/favicon.ico +0 -0
- package/assets/index.html +0 -39
- package/bin/stencil-dev-server +0 -16
- package/dist/definitions.js +0 -2
- package/dist/index.js +0 -275
- package/dist/middlewares.js +0 -106
- package/dist/promisify.js +0 -18
- package/dist/utils.js +0 -139
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} ⚡" 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 };
|