@jsenv/core 38.3.4 → 38.3.6
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/jsenv_core.js +1607 -1630
- package/package.json +8 -8
- package/src/dev/start_dev_server.js +483 -103
- package/src/plugins/inlining/jsenv_plugin_inlining_as_data_url.js +7 -2
- package/src/plugins/reference_analysis/data_urls/jsenv_plugin_data_urls_analysis.js +2 -1
- package/src/dev/file_service.js +0 -413
- package/src/plugins/import_meta_url/client/import_meta_url_browser.js +0 -51
- package/src/plugins/import_meta_url/client/import_meta_url_commonjs.mjs +0 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/core",
|
|
3
|
-
"version": "38.3.
|
|
3
|
+
"version": "38.3.6",
|
|
4
4
|
"description": "Tool to develop, test and build js projects",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"dev": "node --conditions=development ./scripts/dev/dev.mjs",
|
|
46
46
|
"test": "node --conditions=development ./scripts/test/test.mjs",
|
|
47
47
|
"test:workspace": "npm run test --workspaces --if-present -- --workspace",
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
48
|
+
"test:snapshots_core": "npm run test -- --no-snapshot-assertion",
|
|
49
|
+
"test:snapshots_packages": "npm run test --workspaces --if-present -- --workspace --no-snapshot-assertion",
|
|
50
|
+
"test:snapshots_only_dev_server_errors": "node --conditions=development ./tests/dev_server/errors/generate_snapshot_files.mjs",
|
|
51
51
|
"build": "node --conditions=development ./scripts/build/build.mjs",
|
|
52
52
|
"build:file_size": "node ./scripts/build/build_file_size.mjs --log",
|
|
53
53
|
"build:workspace": "npm run build --workspaces --if-present --conditions=developement",
|
|
@@ -68,14 +68,14 @@
|
|
|
68
68
|
"@jsenv/importmap": "1.2.1",
|
|
69
69
|
"@jsenv/integrity": "0.0.1",
|
|
70
70
|
"@jsenv/js-module-fallback": "1.3.9",
|
|
71
|
-
"@jsenv/log": "3.4.
|
|
71
|
+
"@jsenv/log": "3.4.3",
|
|
72
72
|
"@jsenv/node-esm-resolution": "1.0.1",
|
|
73
|
-
"@jsenv/plugin-bundling": "2.5.
|
|
73
|
+
"@jsenv/plugin-bundling": "2.5.9",
|
|
74
74
|
"@jsenv/plugin-minification": "1.5.4",
|
|
75
75
|
"@jsenv/plugin-supervisor": "1.3.9",
|
|
76
76
|
"@jsenv/plugin-transpilation": "1.3.8",
|
|
77
77
|
"@jsenv/runtime-compat": "1.2.0",
|
|
78
|
-
"@jsenv/server": "15.1.
|
|
78
|
+
"@jsenv/server": "15.1.6",
|
|
79
79
|
"@jsenv/sourcemap": "1.2.4",
|
|
80
80
|
"@jsenv/url-meta": "8.1.0",
|
|
81
81
|
"@jsenv/urls": "2.2.1",
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"eslint-plugin-import": "2.29.0",
|
|
100
100
|
"eslint-plugin-react": "7.33.2",
|
|
101
101
|
"open": "9.1.0",
|
|
102
|
-
"playwright": "1.
|
|
102
|
+
"playwright": "1.40.0",
|
|
103
103
|
"prettier": "3.1.0"
|
|
104
104
|
}
|
|
105
105
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { URL_META } from "@jsenv/url-meta";
|
|
3
|
+
import {
|
|
4
|
+
assertAndNormalizeDirectoryUrl,
|
|
5
|
+
bufferToEtag,
|
|
6
|
+
} from "@jsenv/filesystem";
|
|
2
7
|
import { Abort, raceProcessTeardownEvents } from "@jsenv/abort";
|
|
3
8
|
import { createLogger, createTaskLog } from "@jsenv/log";
|
|
4
9
|
import {
|
|
@@ -6,13 +11,21 @@ import {
|
|
|
6
11
|
startServer,
|
|
7
12
|
jsenvServiceCORS,
|
|
8
13
|
jsenvServiceErrorHandler,
|
|
14
|
+
serveDirectory,
|
|
15
|
+
composeTwoResponses,
|
|
9
16
|
} from "@jsenv/server";
|
|
10
17
|
import { convertFileSystemErrorToResponseProperties } from "@jsenv/server/src/internal/convertFileSystemErrorToResponseProperties.js";
|
|
11
18
|
|
|
19
|
+
import { WEB_URL_CONVERTER } from "../helpers/web_url_converter.js";
|
|
20
|
+
import { watchSourceFiles } from "../helpers/watch_source_files.js";
|
|
21
|
+
import { createEventEmitter } from "../helpers/event_emitter.js";
|
|
12
22
|
import { lookupPackageDirectory } from "../helpers/lookup_package_directory.js";
|
|
13
23
|
import { createServerEventsDispatcher } from "../plugins/server_events/server_events_dispatcher.js";
|
|
14
24
|
import { defaultRuntimeCompat } from "../build/build.js";
|
|
15
|
-
import {
|
|
25
|
+
import { createKitchen } from "../kitchen/kitchen.js";
|
|
26
|
+
import { getCorePlugins } from "../plugins/plugins.js";
|
|
27
|
+
import { jsenvPluginServerEventsClientInjection } from "../plugins/server_events/jsenv_plugin_server_events_client_injection.js";
|
|
28
|
+
import { parseUserAgentHeader } from "./user_agent.js";
|
|
16
29
|
|
|
17
30
|
/**
|
|
18
31
|
* Start a server for source files:
|
|
@@ -99,6 +112,16 @@ export const startDevServer = async ({
|
|
|
99
112
|
}
|
|
100
113
|
}
|
|
101
114
|
|
|
115
|
+
// params normalization
|
|
116
|
+
{
|
|
117
|
+
if (clientAutoreload === true) {
|
|
118
|
+
clientAutoreload = {};
|
|
119
|
+
}
|
|
120
|
+
if (clientAutoreload === false) {
|
|
121
|
+
clientAutoreload = { enabled: false };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
102
125
|
const logger = createLogger({ logLevel });
|
|
103
126
|
const operation = Abort.startOperation();
|
|
104
127
|
operation.addAbortSignal(signal);
|
|
@@ -122,48 +145,38 @@ export const startDevServer = async ({
|
|
|
122
145
|
serverEventsDispatcher.destroy();
|
|
123
146
|
});
|
|
124
147
|
const kitchenCache = new Map();
|
|
125
|
-
const server = await startServer({
|
|
126
|
-
signal,
|
|
127
|
-
stopOnExit: false,
|
|
128
|
-
stopOnSIGINT: handleSIGINT,
|
|
129
|
-
stopOnInternalError: false,
|
|
130
|
-
keepProcessAlive: process.env.IMPORTED_BY_TEST_PLAN
|
|
131
|
-
? false
|
|
132
|
-
: keepProcessAlive,
|
|
133
|
-
logLevel: serverLogLevel,
|
|
134
|
-
startLog: false,
|
|
135
148
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
return null;
|
|
162
|
-
},
|
|
163
|
-
injectResponseHeaders: () => {
|
|
164
|
-
return { server: "jsenv_dev_server/1" };
|
|
165
|
-
},
|
|
149
|
+
const finalServices = [];
|
|
150
|
+
// x-server-inspect service
|
|
151
|
+
{
|
|
152
|
+
finalServices.push({
|
|
153
|
+
handleRequest: (request) => {
|
|
154
|
+
if (request.headers["x-server-inspect"]) {
|
|
155
|
+
return { status: 200 };
|
|
156
|
+
}
|
|
157
|
+
if (request.pathname === "/__params__.json") {
|
|
158
|
+
const json = JSON.stringify({
|
|
159
|
+
sourceDirectoryUrl,
|
|
160
|
+
});
|
|
161
|
+
return {
|
|
162
|
+
status: 200,
|
|
163
|
+
headers: {
|
|
164
|
+
"content-type": "application/json",
|
|
165
|
+
"content-length": Buffer.byteLength(json),
|
|
166
|
+
},
|
|
167
|
+
body: json,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
},
|
|
172
|
+
injectResponseHeaders: () => {
|
|
173
|
+
return { server: "jsenv_dev_server/1" };
|
|
166
174
|
},
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
// cors service
|
|
178
|
+
{
|
|
179
|
+
finalServices.push(
|
|
167
180
|
jsenvServiceCORS({
|
|
168
181
|
accessControlAllowRequestOrigin: true,
|
|
169
182
|
accessControlAllowRequestMethod: true,
|
|
@@ -175,86 +188,453 @@ export const startDevServer = async ({
|
|
|
175
188
|
accessControlAllowCredentials: true,
|
|
176
189
|
timingAllowOrigin: true,
|
|
177
190
|
}),
|
|
178
|
-
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
// custom services
|
|
194
|
+
{
|
|
195
|
+
finalServices.push(...services);
|
|
196
|
+
}
|
|
197
|
+
// file_service
|
|
198
|
+
{
|
|
199
|
+
const clientFileChangeEventEmitter = createEventEmitter();
|
|
200
|
+
const clientFileDereferencedEventEmitter = createEventEmitter();
|
|
201
|
+
clientAutoreload = {
|
|
202
|
+
enabled: true,
|
|
203
|
+
clientServerEventsConfig: {},
|
|
204
|
+
clientFileChangeEventEmitter,
|
|
205
|
+
clientFileDereferencedEventEmitter,
|
|
206
|
+
...clientAutoreload,
|
|
207
|
+
};
|
|
208
|
+
const stopWatchingSourceFiles = watchSourceFiles(
|
|
209
|
+
sourceDirectoryUrl,
|
|
210
|
+
(fileInfo) => {
|
|
211
|
+
clientFileChangeEventEmitter.emit(fileInfo);
|
|
212
|
+
},
|
|
179
213
|
{
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
214
|
+
sourceFilesConfig,
|
|
215
|
+
keepProcessAlive: false,
|
|
216
|
+
cooldownBetweenFileEvents: clientAutoreload.cooldownBetweenFileEvents,
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
serverStopCallbacks.push(stopWatchingSourceFiles);
|
|
220
|
+
|
|
221
|
+
const getOrCreateKitchen = (request) => {
|
|
222
|
+
const { runtimeName, runtimeVersion } = parseUserAgentHeader(
|
|
223
|
+
request.headers["user-agent"] || "",
|
|
224
|
+
);
|
|
225
|
+
const runtimeId = `${runtimeName}@${runtimeVersion}`;
|
|
226
|
+
const existing = kitchenCache.get(runtimeId);
|
|
227
|
+
if (existing) {
|
|
228
|
+
return existing;
|
|
229
|
+
}
|
|
230
|
+
const watchAssociations = URL_META.resolveAssociations(
|
|
231
|
+
{ watch: stopWatchingSourceFiles.watchPatterns },
|
|
232
|
+
sourceDirectoryUrl,
|
|
233
|
+
);
|
|
234
|
+
let kitchen;
|
|
235
|
+
clientFileChangeEventEmitter.on(({ url }) => {
|
|
236
|
+
const urlInfo = kitchen.graph.getUrlInfo(url);
|
|
237
|
+
if (urlInfo) {
|
|
238
|
+
urlInfo.onModified();
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
const clientRuntimeCompat = { [runtimeName]: runtimeVersion };
|
|
188
242
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
243
|
+
kitchen = createKitchen({
|
|
244
|
+
name: runtimeId,
|
|
245
|
+
signal,
|
|
246
|
+
logLevel,
|
|
247
|
+
rootDirectoryUrl: sourceDirectoryUrl,
|
|
248
|
+
mainFilePath: sourceMainFilePath,
|
|
249
|
+
ignore,
|
|
250
|
+
dev: true,
|
|
251
|
+
runtimeCompat,
|
|
252
|
+
clientRuntimeCompat,
|
|
253
|
+
plugins: [
|
|
254
|
+
...plugins,
|
|
255
|
+
...getCorePlugins({
|
|
256
|
+
rootDirectoryUrl: sourceDirectoryUrl,
|
|
257
|
+
runtimeCompat,
|
|
194
258
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
259
|
+
referenceAnalysis,
|
|
260
|
+
nodeEsmResolution,
|
|
261
|
+
magicExtensions,
|
|
262
|
+
magicDirectoryIndex,
|
|
263
|
+
supervisor,
|
|
264
|
+
injections,
|
|
265
|
+
transpilation,
|
|
266
|
+
|
|
267
|
+
clientAutoreload,
|
|
268
|
+
cacheControl,
|
|
269
|
+
ribbon,
|
|
270
|
+
}),
|
|
271
|
+
],
|
|
272
|
+
supervisor,
|
|
273
|
+
minification: false,
|
|
274
|
+
sourcemaps,
|
|
275
|
+
sourcemapsSourcesContent,
|
|
276
|
+
outDirectoryUrl: outDirectoryUrl
|
|
277
|
+
? new URL(`${runtimeName}@${runtimeVersion}/`, outDirectoryUrl)
|
|
278
|
+
: undefined,
|
|
279
|
+
});
|
|
280
|
+
kitchen.graph.urlInfoCreatedEventEmitter.on((urlInfoCreated) => {
|
|
281
|
+
const { watch } = URL_META.applyAssociations({
|
|
282
|
+
url: urlInfoCreated.url,
|
|
283
|
+
associations: watchAssociations,
|
|
284
|
+
});
|
|
285
|
+
urlInfoCreated.isWatched = watch;
|
|
286
|
+
// when an url depends on many others, we check all these (like package.json)
|
|
287
|
+
urlInfoCreated.isValid = () => {
|
|
288
|
+
if (!urlInfoCreated.url.startsWith("file:")) {
|
|
289
|
+
return false;
|
|
213
290
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
291
|
+
if (urlInfoCreated.content === undefined) {
|
|
292
|
+
// urlInfo content is undefined when:
|
|
293
|
+
// - url info content never fetched
|
|
294
|
+
// - it is considered as modified because undelying file is watched and got saved
|
|
295
|
+
// - it is considered as modified because underlying file content
|
|
296
|
+
// was compared using etag and it has changed
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
if (!watch) {
|
|
300
|
+
// file is not watched, check the filesystem
|
|
301
|
+
let fileContentAsBuffer;
|
|
302
|
+
try {
|
|
303
|
+
fileContentAsBuffer = readFileSync(new URL(urlInfoCreated.url));
|
|
304
|
+
} catch (e) {
|
|
305
|
+
if (e.code === "ENOENT") {
|
|
306
|
+
urlInfoCreated.onModified();
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
const fileContentEtag = bufferToEtag(fileContentAsBuffer);
|
|
312
|
+
if (fileContentEtag !== urlInfoCreated.originalContentEtag) {
|
|
313
|
+
urlInfoCreated.onModified();
|
|
314
|
+
// restore content to be able to compare it again later
|
|
315
|
+
urlInfoCreated.kitchen.urlInfoTransformer.setContent(
|
|
316
|
+
urlInfoCreated,
|
|
317
|
+
String(fileContentAsBuffer),
|
|
318
|
+
{
|
|
319
|
+
contentEtag: fileContentEtag,
|
|
320
|
+
},
|
|
321
|
+
);
|
|
322
|
+
return false;
|
|
222
323
|
}
|
|
324
|
+
}
|
|
325
|
+
for (const implicitUrl of urlInfoCreated.implicitUrlSet) {
|
|
326
|
+
const implicitUrlInfo =
|
|
327
|
+
urlInfoCreated.graph.getUrlInfo(implicitUrl);
|
|
328
|
+
if (implicitUrlInfo && !implicitUrlInfo.isValid()) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return true;
|
|
333
|
+
};
|
|
334
|
+
});
|
|
335
|
+
kitchen.graph.urlInfoDereferencedEventEmitter.on(
|
|
336
|
+
(urlInfoDereferenced, lastReferenceFromOther) => {
|
|
337
|
+
clientFileDereferencedEventEmitter.emit(
|
|
338
|
+
urlInfoDereferenced,
|
|
339
|
+
lastReferenceFromOther,
|
|
340
|
+
);
|
|
341
|
+
},
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
serverStopCallbacks.push(() => {
|
|
345
|
+
kitchen.pluginController.callHooks("destroy", kitchen.context);
|
|
346
|
+
});
|
|
347
|
+
server_events: {
|
|
348
|
+
const allServerEvents = {};
|
|
349
|
+
kitchen.pluginController.plugins.forEach((plugin) => {
|
|
350
|
+
const { serverEvents } = plugin;
|
|
351
|
+
if (serverEvents) {
|
|
352
|
+
Object.keys(serverEvents).forEach((serverEventName) => {
|
|
353
|
+
// we could throw on serverEvent name conflict
|
|
354
|
+
// we could throw if serverEvents[serverEventName] is not a function
|
|
355
|
+
allServerEvents[serverEventName] = serverEvents[serverEventName];
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
const serverEventNames = Object.keys(allServerEvents);
|
|
360
|
+
if (serverEventNames.length > 0) {
|
|
361
|
+
Object.keys(allServerEvents).forEach((serverEventName) => {
|
|
362
|
+
const serverEventInfo = {
|
|
363
|
+
...kitchen.context,
|
|
364
|
+
sendServerEvent: (data) => {
|
|
365
|
+
serverEventsDispatcher.dispatch({
|
|
366
|
+
type: serverEventName,
|
|
367
|
+
data,
|
|
368
|
+
});
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
const serverEventInit = allServerEvents[serverEventName];
|
|
372
|
+
serverEventInit(serverEventInfo);
|
|
373
|
+
});
|
|
374
|
+
// "pushPlugin" so that event source client connection can be put as early as possible in html
|
|
375
|
+
kitchen.pluginController.pushPlugin(
|
|
376
|
+
jsenvPluginServerEventsClientInjection(
|
|
377
|
+
clientAutoreload.clientServerEventsConfig,
|
|
378
|
+
),
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
kitchenCache.set(runtimeId, kitchen);
|
|
384
|
+
onKitchenCreated(kitchen);
|
|
385
|
+
return kitchen;
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
finalServices.push({
|
|
389
|
+
name: "jsenv:omega_file_service",
|
|
390
|
+
handleRequest: async (request) => {
|
|
391
|
+
const kitchen = getOrCreateKitchen(request);
|
|
392
|
+
const serveHookInfo = {
|
|
393
|
+
...kitchen.context,
|
|
394
|
+
request,
|
|
395
|
+
};
|
|
396
|
+
const responseFromPlugin =
|
|
397
|
+
await kitchen.pluginController.callAsyncHooksUntil(
|
|
398
|
+
"serve",
|
|
399
|
+
serveHookInfo,
|
|
400
|
+
);
|
|
401
|
+
if (responseFromPlugin) {
|
|
402
|
+
return responseFromPlugin;
|
|
403
|
+
}
|
|
404
|
+
const { referer } = request.headers;
|
|
405
|
+
const parentUrl = referer
|
|
406
|
+
? WEB_URL_CONVERTER.asFileUrl(referer, {
|
|
407
|
+
origin: request.origin,
|
|
408
|
+
rootDirectoryUrl: sourceDirectoryUrl,
|
|
409
|
+
})
|
|
410
|
+
: sourceDirectoryUrl;
|
|
411
|
+
let reference = kitchen.graph.inferReference(
|
|
412
|
+
request.resource,
|
|
413
|
+
parentUrl,
|
|
414
|
+
);
|
|
415
|
+
if (!reference) {
|
|
416
|
+
reference =
|
|
417
|
+
kitchen.graph.rootUrlInfo.dependencies.createResolveAndFinalize({
|
|
418
|
+
trace: { message: parentUrl },
|
|
419
|
+
type: "http_request",
|
|
420
|
+
specifier: request.resource,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
const urlInfo = reference.urlInfo;
|
|
424
|
+
const ifNoneMatch = request.headers["if-none-match"];
|
|
425
|
+
const urlInfoTargetedByCache = urlInfo.findParentIfInline() || urlInfo;
|
|
426
|
+
|
|
427
|
+
try {
|
|
428
|
+
if (!urlInfo.error && ifNoneMatch) {
|
|
429
|
+
const [clientOriginalContentEtag, clientContentEtag] =
|
|
430
|
+
ifNoneMatch.split("_");
|
|
223
431
|
if (
|
|
224
|
-
|
|
225
|
-
|
|
432
|
+
urlInfoTargetedByCache.originalContentEtag ===
|
|
433
|
+
clientOriginalContentEtag &&
|
|
434
|
+
urlInfoTargetedByCache.contentEtag === clientContentEtag &&
|
|
435
|
+
urlInfoTargetedByCache.isValid()
|
|
226
436
|
) {
|
|
437
|
+
const headers = {
|
|
438
|
+
"cache-control": `private,max-age=0,must-revalidate`,
|
|
439
|
+
};
|
|
440
|
+
Object.keys(urlInfo.headers).forEach((key) => {
|
|
441
|
+
if (key !== "content-length") {
|
|
442
|
+
headers[key] = urlInfo.headers[key];
|
|
443
|
+
}
|
|
444
|
+
});
|
|
227
445
|
return {
|
|
228
|
-
status:
|
|
446
|
+
status: 304,
|
|
447
|
+
headers,
|
|
229
448
|
};
|
|
230
449
|
}
|
|
231
|
-
return convertFileSystemErrorToResponseProperties(error);
|
|
232
|
-
};
|
|
233
|
-
const response = getResponseForError();
|
|
234
|
-
if (!response) {
|
|
235
|
-
return null;
|
|
236
450
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
|
|
451
|
+
|
|
452
|
+
await urlInfo.cook({ request, reference });
|
|
453
|
+
let { response } = urlInfo;
|
|
454
|
+
if (response) {
|
|
455
|
+
return response;
|
|
456
|
+
}
|
|
457
|
+
response = {
|
|
458
|
+
url: reference.url,
|
|
244
459
|
status: 200,
|
|
245
460
|
headers: {
|
|
246
|
-
|
|
247
|
-
|
|
461
|
+
// when we send eTag to the client the next request to the server
|
|
462
|
+
// will send etag in request headers.
|
|
463
|
+
// If they match jsenv bypass cooking and returns 304
|
|
464
|
+
// This must not happen when a plugin uses "no-store" or "no-cache" as it means
|
|
465
|
+
// plugin logic wants to happens for every request to this url
|
|
466
|
+
...(urlInfo.headers["cache-control"] === "no-store" ||
|
|
467
|
+
urlInfo.headers["cache-control"] === "no-cache"
|
|
468
|
+
? {}
|
|
469
|
+
: {
|
|
470
|
+
"cache-control": `private,max-age=0,must-revalidate`,
|
|
471
|
+
// it's safe to use "_" separator because etag is encoded with base64 (see https://stackoverflow.com/a/13195197)
|
|
472
|
+
"eTag": `${urlInfoTargetedByCache.originalContentEtag}_${urlInfoTargetedByCache.contentEtag}`,
|
|
473
|
+
}),
|
|
474
|
+
...urlInfo.headers,
|
|
475
|
+
"content-type": urlInfo.contentType,
|
|
476
|
+
"content-length": urlInfo.contentLength,
|
|
248
477
|
},
|
|
249
|
-
body,
|
|
478
|
+
body: urlInfo.content,
|
|
479
|
+
timing: urlInfo.timing,
|
|
250
480
|
};
|
|
251
|
-
|
|
481
|
+
const augmentResponseInfo = {
|
|
482
|
+
...kitchen.context,
|
|
483
|
+
reference,
|
|
484
|
+
urlInfo,
|
|
485
|
+
};
|
|
486
|
+
kitchen.pluginController.callHooks(
|
|
487
|
+
"augmentResponse",
|
|
488
|
+
augmentResponseInfo,
|
|
489
|
+
(returnValue) => {
|
|
490
|
+
response = composeTwoResponses(response, returnValue);
|
|
491
|
+
},
|
|
492
|
+
);
|
|
493
|
+
return response;
|
|
494
|
+
} catch (e) {
|
|
495
|
+
urlInfo.error = e;
|
|
496
|
+
const originalError = e ? e.cause || e : e;
|
|
497
|
+
if (originalError.asResponse) {
|
|
498
|
+
return originalError.asResponse();
|
|
499
|
+
}
|
|
500
|
+
const code = originalError.code;
|
|
501
|
+
if (code === "PARSE_ERROR") {
|
|
502
|
+
// when possible let browser re-throw the syntax error
|
|
503
|
+
// it's not possible to do that when url info content is not available
|
|
504
|
+
// (happens for js_module_fallback for instance)
|
|
505
|
+
if (urlInfo.content !== undefined) {
|
|
506
|
+
kitchen.context.logger.error(`Error while handling ${request.url}:
|
|
507
|
+
${originalError.reasonCode || originalError.code}
|
|
508
|
+
${e.traceMessage}`);
|
|
509
|
+
return {
|
|
510
|
+
url: reference.url,
|
|
511
|
+
status: 200,
|
|
512
|
+
// reason becomes the http response statusText, it must not contain invalid chars
|
|
513
|
+
// https://github.com/nodejs/node/blob/0c27ca4bc9782d658afeaebcec85ec7b28f1cc35/lib/_http_common.js#L221
|
|
514
|
+
statusText: e.reason,
|
|
515
|
+
statusMessage: originalError.message,
|
|
516
|
+
headers: {
|
|
517
|
+
"content-type": urlInfo.contentType,
|
|
518
|
+
"content-length": urlInfo.contentLength,
|
|
519
|
+
"cache-control": "no-store",
|
|
520
|
+
},
|
|
521
|
+
body: urlInfo.content,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
url: reference.url,
|
|
526
|
+
status: 500,
|
|
527
|
+
statusText: e.reason,
|
|
528
|
+
statusMessage: originalError.message,
|
|
529
|
+
headers: {
|
|
530
|
+
"cache-control": "no-store",
|
|
531
|
+
},
|
|
532
|
+
body: urlInfo.content,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
if (code === "DIRECTORY_REFERENCE_NOT_ALLOWED") {
|
|
536
|
+
return serveDirectory(reference.url, {
|
|
537
|
+
headers: {
|
|
538
|
+
accept: "text/html",
|
|
539
|
+
},
|
|
540
|
+
canReadDirectory: true,
|
|
541
|
+
rootDirectoryUrl: sourceDirectoryUrl,
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
if (code === "NOT_ALLOWED") {
|
|
545
|
+
return {
|
|
546
|
+
url: reference.url,
|
|
547
|
+
status: 403,
|
|
548
|
+
statusText: originalError.reason,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
if (code === "NOT_FOUND") {
|
|
552
|
+
return {
|
|
553
|
+
url: reference.url,
|
|
554
|
+
status: 404,
|
|
555
|
+
statusText: originalError.reason,
|
|
556
|
+
statusMessage: originalError.message,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
url: reference.url,
|
|
561
|
+
status: 500,
|
|
562
|
+
statusText: e.reason,
|
|
563
|
+
statusMessage: e.stack,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
},
|
|
567
|
+
handleWebsocket: (websocket, { request }) => {
|
|
568
|
+
if (request.headers["sec-websocket-protocol"] === "jsenv") {
|
|
569
|
+
serverEventsDispatcher.addWebsocket(websocket, request);
|
|
570
|
+
}
|
|
252
571
|
},
|
|
253
|
-
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
// jsenv error handler service
|
|
575
|
+
{
|
|
576
|
+
finalServices.push({
|
|
577
|
+
name: "jsenv:omega_error_handler",
|
|
578
|
+
handleError: (error) => {
|
|
579
|
+
const getResponseForError = () => {
|
|
580
|
+
if (error && error.asResponse) {
|
|
581
|
+
return error.asResponse();
|
|
582
|
+
}
|
|
583
|
+
if (error && error.statusText === "Unexpected directory operation") {
|
|
584
|
+
return {
|
|
585
|
+
status: 403,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
return convertFileSystemErrorToResponseProperties(error);
|
|
589
|
+
};
|
|
590
|
+
const response = getResponseForError();
|
|
591
|
+
if (!response) {
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
const body = JSON.stringify({
|
|
595
|
+
status: response.status,
|
|
596
|
+
statusText: response.statusText,
|
|
597
|
+
headers: response.headers,
|
|
598
|
+
body: response.body,
|
|
599
|
+
});
|
|
600
|
+
return {
|
|
601
|
+
status: 200,
|
|
602
|
+
headers: {
|
|
603
|
+
"content-type": "application/json",
|
|
604
|
+
"content-length": Buffer.byteLength(body),
|
|
605
|
+
},
|
|
606
|
+
body,
|
|
607
|
+
};
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
// default error handler
|
|
612
|
+
{
|
|
613
|
+
finalServices.push(
|
|
254
614
|
jsenvServiceErrorHandler({
|
|
255
615
|
sendErrorDetails: true,
|
|
256
616
|
}),
|
|
257
|
-
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const server = await startServer({
|
|
621
|
+
signal,
|
|
622
|
+
stopOnExit: false,
|
|
623
|
+
stopOnSIGINT: handleSIGINT,
|
|
624
|
+
stopOnInternalError: false,
|
|
625
|
+
keepProcessAlive: process.env.IMPORTED_BY_TEST_PLAN
|
|
626
|
+
? false
|
|
627
|
+
: keepProcessAlive,
|
|
628
|
+
logLevel: serverLogLevel,
|
|
629
|
+
startLog: false,
|
|
630
|
+
|
|
631
|
+
https,
|
|
632
|
+
http2,
|
|
633
|
+
acceptAnyIp,
|
|
634
|
+
hostname,
|
|
635
|
+
port,
|
|
636
|
+
requestWaitingMs: 60_000,
|
|
637
|
+
services: finalServices,
|
|
258
638
|
});
|
|
259
639
|
server.stoppedPromise.then((reason) => {
|
|
260
640
|
onStop();
|