@jsenv/core 41.0.2 → 41.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "41.0.2",
3
+ "version": "41.0.4",
4
4
  "description": "Tool to develop, test and build js projects",
5
5
  "repository": {
6
6
  "type": "git",
@@ -81,7 +81,7 @@
81
81
  "@jsenv/plugin-minification": "1.7.3",
82
82
  "@jsenv/plugin-supervisor": "1.7.15",
83
83
  "@jsenv/plugin-transpilation": "1.5.70",
84
- "@jsenv/server": "17.0.1",
84
+ "@jsenv/server": "17.0.3",
85
85
  "@jsenv/sourcemap": "1.3.17",
86
86
  "react-table": "7.8.0"
87
87
  },
@@ -14,16 +14,14 @@
14
14
  */
15
15
 
16
16
  import { Abort, raceProcessTeardownEvents } from "@jsenv/abort";
17
- import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem";
18
17
  import { createLogger, createTaskLog } from "@jsenv/humanize";
19
18
  import {
19
+ createFileSystemFetch,
20
20
  jsenvAccessControlAllowedHeaders,
21
21
  serverPluginCORS,
22
22
  serverPluginErrorHandler,
23
- serverPluginStaticFiles,
24
23
  startServer,
25
24
  } from "@jsenv/server";
26
- import { existsSync } from "node:fs";
27
25
 
28
26
  /**
29
27
  * Start a server for build files.
@@ -35,7 +33,7 @@ export const startBuildServer = async ({
35
33
  buildDirectoryUrl,
36
34
  buildDirectoryMainFileRelativeUrl = "index.html",
37
35
  port = 9779,
38
- routes,
36
+ routes = [],
39
37
  serverPlugins = [],
40
38
  acceptAnyIp,
41
39
  hostname,
@@ -47,55 +45,7 @@ export const startBuildServer = async ({
47
45
  signal = new AbortController().signal,
48
46
  handleSIGINT = true,
49
47
  keepProcessAlive = true,
50
-
51
- ...rest
52
48
  }) => {
53
- // params validation
54
- {
55
- const unexpectedParamNames = Object.keys(rest);
56
- if (unexpectedParamNames.length > 0) {
57
- throw new TypeError(
58
- `${unexpectedParamNames.join(",")}: there is no such param`,
59
- );
60
- }
61
- buildDirectoryUrl = assertAndNormalizeDirectoryUrl(
62
- buildDirectoryUrl,
63
- "buildDirectoryUrl",
64
- );
65
-
66
- if (buildDirectoryMainFileRelativeUrl) {
67
- if (typeof buildDirectoryMainFileRelativeUrl !== "string") {
68
- throw new TypeError(
69
- `buildDirectoryMainFileRelativeUrl must be a string, got ${buildDirectoryMainFileRelativeUrl}`,
70
- );
71
- }
72
- if (buildDirectoryMainFileRelativeUrl[0] === "/") {
73
- buildDirectoryMainFileRelativeUrl =
74
- buildDirectoryMainFileRelativeUrl.slice(1);
75
- } else {
76
- const buildMainFileUrl = new URL(
77
- buildDirectoryMainFileRelativeUrl,
78
- buildDirectoryUrl,
79
- ).href;
80
- if (!buildMainFileUrl.startsWith(buildDirectoryUrl)) {
81
- throw new Error(
82
- `buildDirectoryMainFileRelativeUrl must be relative, got ${buildDirectoryMainFileRelativeUrl}`,
83
- );
84
- }
85
- buildDirectoryMainFileRelativeUrl = buildMainFileUrl.slice(
86
- buildDirectoryUrl.length,
87
- );
88
- }
89
- if (
90
- !existsSync(
91
- new URL(buildDirectoryMainFileRelativeUrl, buildDirectoryUrl),
92
- )
93
- ) {
94
- buildDirectoryMainFileRelativeUrl = null;
95
- }
96
- }
97
- }
98
-
99
49
  const logger = createLogger({ logLevel });
100
50
  const operation = Abort.startOperation();
101
51
  operation.addAbortSignal(signal);
@@ -129,7 +79,6 @@ export const startBuildServer = async ({
129
79
  port,
130
80
  serverTiming: true,
131
81
  requestWaitingMs: 60_000,
132
- routes,
133
82
  plugins: [
134
83
  serverPluginCORS({
135
84
  accessControlAllowRequestOrigin: true,
@@ -140,15 +89,20 @@ export const startBuildServer = async ({
140
89
  timingAllowOrigin: true,
141
90
  }),
142
91
  ...serverPlugins,
143
- serverPluginStaticFiles({
144
- serverRelativeUrl: "/",
145
- directoryUrl: buildDirectoryUrl,
146
- directoryMainFileRelativeUrl: buildDirectoryMainFileRelativeUrl,
147
- }),
148
92
  serverPluginErrorHandler({
149
93
  sendErrorDetails: false,
150
94
  }),
151
95
  ],
96
+ routes: [
97
+ ...routes,
98
+ {
99
+ endpoint: "GET /",
100
+ description: "Serve build files",
101
+ fetch: createFileSystemFetch(buildDirectoryUrl, {
102
+ mainFileRelativeUrl: buildDirectoryMainFileRelativeUrl,
103
+ }),
104
+ },
105
+ ],
152
106
  });
153
107
  startBuildServerTask.done();
154
108
  if (hostname) {
@@ -9,7 +9,7 @@ import { urlToFileSystemPath } from "@jsenv/urls";
9
9
  import { randomUUID } from "node:crypto";
10
10
  import { existsSync, readFileSync } from "node:fs";
11
11
 
12
- export const jsenvPluginChromeDevtoolsJson = () => {
12
+ export const devServerPluginChromeDevToolsJson = ({ sourceDirectoryUrl }) => {
13
13
  const getOrCreateUUID = (kitchen) => {
14
14
  const { outDirectoryUrl } = kitchen.context;
15
15
  const uuidFileUrl = new URL("./uuid.json", outDirectoryUrl);
@@ -23,17 +23,15 @@ export const jsenvPluginChromeDevtoolsJson = () => {
23
23
  };
24
24
 
25
25
  return {
26
- name: "jsenv_plugin_chrome_devtools_json",
27
- appliesDuring: "dev",
28
- serverRoutes: [
26
+ name: "jsenv:chrome_devtools_json",
27
+ routes: [
29
28
  {
30
29
  endpoint: "GET /.well-known/appspecific/com.chrome.devtools.json",
31
30
  declarationSource: import.meta.url,
32
31
  fetch: (request, { kitchen }) => {
33
- const { rootDirectoryUrl } = kitchen.context;
34
32
  return Response.json({
35
33
  workspace: {
36
- root: urlToFileSystemPath(rootDirectoryUrl),
34
+ root: urlToFileSystemPath(sourceDirectoryUrl),
37
35
  uuid: getOrCreateUUID(kitchen),
38
36
  },
39
37
  });
@@ -0,0 +1,27 @@
1
+ export const devServerPluginInjectServerResponseHeader = ({
2
+ sourceDirectoryUrl,
3
+ }) => {
4
+ return {
5
+ name: "jsenv:jsenv_inject_server_response_header",
6
+ routes: [
7
+ {
8
+ endpoint: "GET /.internal/server.json",
9
+ description: "Get information about jsenv dev server",
10
+ availableMediaTypes: ["application/json"],
11
+ declarationSource: import.meta.url,
12
+ fetch: () =>
13
+ Response.json({
14
+ server: "jsenv_dev_server/1",
15
+ sourceDirectoryUrl,
16
+ }),
17
+ },
18
+ ],
19
+ injectResponseProperties: () => {
20
+ return {
21
+ headers: {
22
+ server: "jsenv_dev_server/1",
23
+ },
24
+ };
25
+ },
26
+ };
27
+ };
@@ -0,0 +1,44 @@
1
+ import { serverPluginErrorHandler } from "@jsenv/server";
2
+ import { convertFileSystemErrorToResponseProperties } from "@jsenv/server/src/plugins/filesystem/filesystem_error_to_response.js";
3
+
4
+ export const devServerPluginOmegaErrorHandler = () => {
5
+ return [
6
+ {
7
+ name: "jsenv:omega_error_handler",
8
+ handleError: (error) => {
9
+ const getResponseForError = () => {
10
+ if (error && error.asResponse) {
11
+ return error.asResponse();
12
+ }
13
+ if (error && error.statusText === "Unexpected directory operation") {
14
+ return {
15
+ status: 403,
16
+ };
17
+ }
18
+ return convertFileSystemErrorToResponseProperties(error);
19
+ };
20
+ const response = getResponseForError();
21
+ if (!response) {
22
+ return null;
23
+ }
24
+ const body = JSON.stringify({
25
+ status: response.status,
26
+ statusText: response.statusText,
27
+ headers: response.headers,
28
+ body: response.body,
29
+ });
30
+ return {
31
+ status: response.status,
32
+ headers: {
33
+ "content-type": "application/json",
34
+ "content-length": Buffer.byteLength(body),
35
+ },
36
+ body,
37
+ };
38
+ },
39
+ },
40
+ serverPluginErrorHandler({
41
+ sendErrorDetails: true,
42
+ }),
43
+ ];
44
+ };
@@ -0,0 +1,410 @@
1
+ import { bufferToEtag } from "@jsenv/filesystem";
2
+ import { formatError } from "@jsenv/humanize";
3
+ import { composeTwoResponses, fetchDirectory } from "@jsenv/server";
4
+ import { URL_META } from "@jsenv/url-meta";
5
+ import { readFileSync } from "node:fs";
6
+
7
+ import { watchSourceFiles } from "../../helpers/watch_source_files.js";
8
+ import { WEB_URL_CONVERTER } from "../../helpers/web_url_converter.js";
9
+ import { createKitchen } from "../../kitchen/kitchen.js";
10
+ import { createJsenvPluginsController } from "../../plugins/jsenv_plugins_controller.js";
11
+
12
+ import { parseUserAgentHeader } from "./user_agent.js";
13
+
14
+ export const devServerPluginServeSourceFiles = ({
15
+ packageDirectory,
16
+ sourceDirectoryUrl,
17
+ sourceMainFilePath,
18
+ ignore,
19
+ sourceFilesConfig,
20
+ clientAutoreload,
21
+ logLevel,
22
+
23
+ runtimeCompat,
24
+ onKitchenCreated,
25
+
26
+ supervisor,
27
+ sourcemaps,
28
+ sourcemapsSourcesContent,
29
+ outDirectoryUrl,
30
+
31
+ serverStopAbortSignal,
32
+ serverStopCallbackSet,
33
+ devServerJsenvPluginStore,
34
+ kitchenCache,
35
+ }) => {
36
+ const { clientFileChangeEventEmitter, clientFileDereferencedEventEmitter } =
37
+ clientAutoreload;
38
+
39
+ const stopWatchingSourceFiles = watchSourceFiles(
40
+ sourceDirectoryUrl,
41
+ (fileInfo) => {
42
+ clientFileChangeEventEmitter.emit(fileInfo);
43
+ },
44
+ {
45
+ sourceFilesConfig,
46
+ keepProcessAlive: false,
47
+ cooldownBetweenFileEvents: clientAutoreload.cooldownBetweenFileEvents,
48
+ },
49
+ );
50
+ serverStopCallbackSet.add(stopWatchingSourceFiles);
51
+
52
+ const getOrCreateKitchen = async (request) => {
53
+ const { runtimeName, runtimeVersion } = parseUserAgentHeader(
54
+ request.headers["user-agent"] || "",
55
+ );
56
+ const runtimeId = `${runtimeName}@${runtimeVersion}`;
57
+ const existing = kitchenCache.get(runtimeId);
58
+ if (existing) {
59
+ return existing;
60
+ }
61
+ const watchAssociations = URL_META.resolveAssociations(
62
+ { watch: stopWatchingSourceFiles.watchPatterns },
63
+ sourceDirectoryUrl,
64
+ );
65
+ let kitchen;
66
+ clientFileChangeEventEmitter.on(({ url, event }) => {
67
+ const urlInfo = kitchen.graph.getUrlInfo(url);
68
+ if (urlInfo) {
69
+ if (event === "removed") {
70
+ urlInfo.onRemoved();
71
+ } else {
72
+ urlInfo.onModified();
73
+ }
74
+ }
75
+ });
76
+ const clientRuntimeCompat = { [runtimeName]: runtimeVersion };
77
+
78
+ kitchen = createKitchen({
79
+ name: runtimeId,
80
+ signal: serverStopAbortSignal,
81
+ logLevel,
82
+ rootDirectoryUrl: sourceDirectoryUrl,
83
+ mainFilePath: sourceMainFilePath,
84
+ ignore,
85
+ dev: true,
86
+ runtimeCompat,
87
+ clientRuntimeCompat,
88
+ supervisor,
89
+ sourcemaps,
90
+ sourcemapsSourcesContent,
91
+ outDirectoryUrl: outDirectoryUrl
92
+ ? new URL(`${runtimeName}@${runtimeVersion}/`, outDirectoryUrl)
93
+ : undefined,
94
+ packageDirectory,
95
+ });
96
+ kitchen.graph.urlInfoCreatedEventEmitter.on((urlInfoCreated) => {
97
+ const { watch } = URL_META.applyAssociations({
98
+ url: urlInfoCreated.url,
99
+ associations: watchAssociations,
100
+ });
101
+ urlInfoCreated.isWatched = watch;
102
+ // when an url depends on many others, we check all these (like package.json)
103
+ urlInfoCreated.isValid = () => {
104
+ const seenSet = new Set();
105
+ const checkValidity = (urlInfo) => {
106
+ if (seenSet.has(urlInfo)) {
107
+ return true;
108
+ }
109
+ seenSet.add(urlInfo);
110
+ if (!urlInfo.url.startsWith("file:")) {
111
+ return false;
112
+ }
113
+ if (urlInfo.content === undefined) {
114
+ // urlInfo content is undefined when:
115
+ // - url info content never fetched
116
+ // - it is considered as modified because undelying file is watched and got saved
117
+ // - it is considered as modified because underlying file content
118
+ // was compared using etag and it has changed
119
+ return false;
120
+ }
121
+ if (!urlInfo.isWatched) {
122
+ // file is not watched, check the filesystem
123
+ let fileContentAsBuffer;
124
+ try {
125
+ fileContentAsBuffer = readFileSync(new URL(urlInfo.url));
126
+ } catch (e) {
127
+ if (e.code === "ENOENT") {
128
+ urlInfo.onModified();
129
+ return false;
130
+ }
131
+ return false;
132
+ }
133
+ const fileContentEtag = bufferToEtag(fileContentAsBuffer);
134
+ if (fileContentEtag !== urlInfo.originalContentEtag) {
135
+ urlInfo.onModified();
136
+ // restore content to be able to compare it again later
137
+ urlInfo.kitchen.urlInfoTransformer.setContent(
138
+ urlInfo,
139
+ String(fileContentAsBuffer),
140
+ {
141
+ contentEtag: fileContentEtag,
142
+ },
143
+ );
144
+ return false;
145
+ }
146
+ }
147
+ for (const implicitUrl of urlInfo.implicitUrlSet) {
148
+ const implicitUrlInfo = urlInfo.graph.getUrlInfo(implicitUrl);
149
+ if (!implicitUrlInfo) {
150
+ continue;
151
+ }
152
+ if (implicitUrlInfo.content === undefined) {
153
+ // happens when we explicitely load an url with a search param
154
+ // - it creates an implicit url info to the url without params
155
+ // - we never explicitely request the url without search param so it has no content
156
+ // in that case the underlying urlInfo cannot be invalidate by the implicit
157
+ // we use modifiedTimestamp to detect if the url was loaded once
158
+ // or is just here to be used later
159
+ if (implicitUrlInfo.modifiedTimestamp) {
160
+ return false;
161
+ }
162
+ continue;
163
+ }
164
+ if (!checkValidity(implicitUrlInfo)) {
165
+ return false;
166
+ }
167
+ }
168
+ return true;
169
+ };
170
+ const valid = checkValidity(urlInfoCreated);
171
+ return valid;
172
+ };
173
+ });
174
+ kitchen.graph.urlInfoDereferencedEventEmitter.on(
175
+ (urlInfoDereferenced, lastReferenceFromOther) => {
176
+ clientFileDereferencedEventEmitter.emit(
177
+ urlInfoDereferenced,
178
+ lastReferenceFromOther,
179
+ );
180
+ },
181
+ );
182
+ const devServerJsenvPluginController = await createJsenvPluginsController(
183
+ devServerJsenvPluginStore,
184
+ kitchen,
185
+ );
186
+ kitchen.setJsenvPluginsController(devServerJsenvPluginController);
187
+
188
+ serverStopCallbackSet.add(() => {
189
+ devServerJsenvPluginController.callHooks("destroy", kitchen.context);
190
+ });
191
+ kitchenCache.set(runtimeId, kitchen);
192
+ onKitchenCreated(kitchen);
193
+ return kitchen;
194
+ };
195
+
196
+ const devServerPluginRoutes = {
197
+ name: "jsenv:dev_server_routes",
198
+ augmentRouteFetchSecondArg: async (request) => {
199
+ const kitchen = await getOrCreateKitchen(request);
200
+ return { kitchen };
201
+ },
202
+ routes: [
203
+ ...devServerJsenvPluginStore.allServerRoutes,
204
+ {
205
+ endpoint: "GET *",
206
+ description: "Serve project files.",
207
+ declarationSource: import.meta.url,
208
+ fetch: async (request, { kitchen }) => {
209
+ const { rootDirectoryUrl, mainFilePath } = kitchen.context;
210
+ let requestResource = request.resource;
211
+ let requestedUrl;
212
+ if (requestResource.startsWith("/@fs/")) {
213
+ const fsRootRelativeUrl = requestResource.slice("/@fs/".length);
214
+ requestedUrl = `file:///${fsRootRelativeUrl}`;
215
+ } else {
216
+ const requestedUrlObject = new URL(
217
+ requestResource === "/" ? mainFilePath : requestResource.slice(1),
218
+ rootDirectoryUrl,
219
+ );
220
+ requestedUrlObject.searchParams.delete("hot");
221
+ requestedUrl = requestedUrlObject.href;
222
+ }
223
+ const { referer } = request.headers;
224
+ const parentUrl = referer
225
+ ? WEB_URL_CONVERTER.asFileUrl(referer, {
226
+ origin: request.origin,
227
+ rootDirectoryUrl: sourceDirectoryUrl,
228
+ })
229
+ : sourceDirectoryUrl;
230
+ let reference = kitchen.graph.inferReference(
231
+ request.resource,
232
+ parentUrl,
233
+ );
234
+ if (reference) {
235
+ reference.urlInfo.context.request = request;
236
+ reference.urlInfo.context.requestedUrl = requestedUrl;
237
+ } else {
238
+ const rootUrlInfo = kitchen.graph.rootUrlInfo;
239
+ rootUrlInfo.context.request = request;
240
+ rootUrlInfo.context.requestedUrl = requestedUrl;
241
+ reference = rootUrlInfo.dependencies.createResolveAndFinalize({
242
+ trace: { message: parentUrl },
243
+ type: "http_request",
244
+ specifier: request.resource,
245
+ });
246
+ reference.urlInfo.context.requestedUrl = requestedUrl;
247
+ rootUrlInfo.context.request = null;
248
+ rootUrlInfo.context.requestedUrl = null;
249
+ }
250
+ const urlInfo = reference.urlInfo;
251
+ const ifNoneMatch = request.headers["if-none-match"];
252
+ const urlInfoTargetedByCache =
253
+ urlInfo.findParentIfInline() || urlInfo;
254
+
255
+ try {
256
+ if (!urlInfo.error && ifNoneMatch) {
257
+ const [clientOriginalContentEtag, clientContentEtag] =
258
+ ifNoneMatch.split("_");
259
+ if (
260
+ urlInfoTargetedByCache.originalContentEtag ===
261
+ clientOriginalContentEtag &&
262
+ urlInfoTargetedByCache.contentEtag === clientContentEtag &&
263
+ urlInfoTargetedByCache.isValid()
264
+ ) {
265
+ const headers = {
266
+ "cache-control": `private,max-age=0,must-revalidate`,
267
+ };
268
+ Object.keys(urlInfo.headers).forEach((key) => {
269
+ if (key !== "content-length") {
270
+ headers[key] = urlInfo.headers[key];
271
+ }
272
+ });
273
+ return {
274
+ status: 304,
275
+ headers,
276
+ };
277
+ }
278
+ }
279
+ await urlInfo.cook({ request, reference });
280
+ let { response } = urlInfo;
281
+ if (response) {
282
+ return response;
283
+ }
284
+ response = {
285
+ url: reference.url,
286
+ status: 200,
287
+ headers: {
288
+ // when we send eTag to the client the next request to the server
289
+ // will send etag in request headers.
290
+ // If they match jsenv bypass cooking and returns 304
291
+ // This must not happen when a plugin uses "no-store" or "no-cache" as it means
292
+ // plugin logic wants to happens for every request to this url
293
+ ...(cacheIsDisabledInResponseHeader(urlInfoTargetedByCache)
294
+ ? {
295
+ "cache-control": "no-store", // for inline file we force no-store when parent is no-store
296
+ }
297
+ : {
298
+ "cache-control": `private,max-age=0,must-revalidate`,
299
+ // it's safe to use "_" separator because etag is encoded with base64 (see https://stackoverflow.com/a/13195197)
300
+ "eTag": `${urlInfoTargetedByCache.originalContentEtag}_${urlInfoTargetedByCache.contentEtag}`,
301
+ }),
302
+ ...urlInfo.headers,
303
+ "content-type": urlInfo.contentType,
304
+ "content-length": urlInfo.contentLength,
305
+ },
306
+ body: urlInfo.content,
307
+ timing: urlInfo.timing, // TODO: use something else
308
+ };
309
+ const augmentResponseInfo = {
310
+ ...kitchen.context,
311
+ reference,
312
+ urlInfo,
313
+ };
314
+ kitchen.jsenvPluginsController.callHooks(
315
+ "augmentResponse",
316
+ augmentResponseInfo,
317
+ (returnValue) => {
318
+ response = composeTwoResponses(response, returnValue);
319
+ },
320
+ );
321
+ return response;
322
+ } catch (error) {
323
+ const originalError = error ? error.cause || error : error;
324
+ if (originalError.asResponse) {
325
+ return originalError.asResponse();
326
+ }
327
+ const code = originalError.code;
328
+ if (code === "PARSE_ERROR") {
329
+ // when possible let browser re-throw the syntax error
330
+ // it's not possible to do that when url info content is not available
331
+ // (happens for js_module_fallback for instance)
332
+ if (urlInfo.content !== undefined) {
333
+ kitchen.context.logger
334
+ .error(`Error while handling ${request.url}:
335
+ ${originalError.reasonCode || originalError.code}
336
+ ${error.trace?.message}`);
337
+ return {
338
+ url: reference.url,
339
+ status: 200,
340
+ // reason becomes the http response statusText, it must not contain invalid chars
341
+ // https://github.com/nodejs/node/blob/0c27ca4bc9782d658afeaebcec85ec7b28f1cc35/lib/_http_common.js#L221
342
+ statusText: error.reason,
343
+ statusMessage: originalError.message,
344
+ headers: {
345
+ "content-type": urlInfo.contentType,
346
+ "content-length": urlInfo.contentLength,
347
+ "cache-control": "no-store",
348
+ },
349
+ body: urlInfo.content,
350
+ };
351
+ }
352
+ return {
353
+ url: reference.url,
354
+ status: 500,
355
+ statusText: error.reason,
356
+ statusMessage: originalError.message,
357
+ headers: {
358
+ "cache-control": "no-store",
359
+ },
360
+ body: urlInfo.content,
361
+ };
362
+ }
363
+ if (code === "DIRECTORY_REFERENCE_NOT_ALLOWED") {
364
+ return fetchDirectory(reference.url, {
365
+ headers: {
366
+ accept: "text/html",
367
+ },
368
+ canReadDirectory: true,
369
+ rootDirectoryUrl: sourceDirectoryUrl,
370
+ });
371
+ }
372
+ if (code === "NOT_ALLOWED") {
373
+ return {
374
+ url: reference.url,
375
+ status: 403,
376
+ statusText: originalError.reason,
377
+ };
378
+ }
379
+ if (code === "NOT_FOUND") {
380
+ return {
381
+ url: reference.url,
382
+ status: 404,
383
+ statusText: originalError.reason,
384
+ statusMessage: originalError.message,
385
+ };
386
+ }
387
+ return {
388
+ url: reference.url,
389
+ status: 500,
390
+ statusText: error.reason,
391
+ statusMessage: formatError(error),
392
+ headers: {
393
+ "cache-control": "no-store",
394
+ },
395
+ };
396
+ }
397
+ },
398
+ },
399
+ ],
400
+ };
401
+
402
+ return [devServerPluginRoutes, ...devServerJsenvPluginStore.allServerPlugins];
403
+ };
404
+
405
+ const cacheIsDisabledInResponseHeader = (urlInfo) => {
406
+ return (
407
+ urlInfo.headers["cache-control"] === "no-store" ||
408
+ urlInfo.headers["cache-control"] === "no-cache"
409
+ );
410
+ };