@jsenv/core 40.6.2 → 40.7.1

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.
Files changed (36) hide show
  1. package/dist/build/browserslist_index/browserslist_index.js +62 -48
  2. package/dist/build/build.js +412 -185
  3. package/dist/build/jsenv_core_packages.js +103 -105
  4. package/dist/client/directory_listing/js/directory_listing.js +41 -26
  5. package/dist/client/ribbon/ribbon.js +40 -37
  6. package/dist/jsenv_core.js +4 -0
  7. package/dist/start_build_server/jsenv_core_packages.js +29 -29
  8. package/dist/start_dev_server/jsenv_core_packages.js +103 -105
  9. package/dist/start_dev_server/start_dev_server.js +412 -182
  10. package/package.json +21 -12
  11. package/src/build/build.js +9 -9
  12. package/src/build/build_specifier_manager.js +3 -3
  13. package/src/build/build_urls_generator.js +2 -2
  14. package/src/dev/start_dev_server.js +11 -8
  15. package/src/helpers/web_url_converter.js +2 -2
  16. package/src/kitchen/errors.js +1 -1
  17. package/src/kitchen/kitchen.js +2 -0
  18. package/src/kitchen/out_directory_url.js +2 -2
  19. package/src/kitchen/url_graph/url_graph.js +1 -0
  20. package/src/kitchen/url_graph/url_info_injections.js +172 -0
  21. package/src/kitchen/url_graph/url_info_transformations.js +28 -7
  22. package/src/main.js +1 -1
  23. package/src/plugins/autoreload/jsenv_plugin_autoreload_server.js +2 -2
  24. package/src/plugins/chrome_devtools_json/jsenv_plugin_chrome_devtools_json.js +1 -0
  25. package/src/plugins/global_scenarios/jsenv_plugin_global_scenarios.js +4 -9
  26. package/src/plugins/import_meta_scenarios/jsenv_plugin_import_meta_scenarios.js +2 -0
  27. package/src/plugins/injections/jsenv_plugin_injections.js +51 -85
  28. package/src/plugins/plugin_controller.js +28 -7
  29. package/src/plugins/plugins.js +3 -1
  30. package/src/plugins/protocol_file/client/directory_listing.jsx +42 -23
  31. package/src/plugins/protocol_file/file_and_server_urls_converter.js +2 -5
  32. package/src/plugins/protocol_file/jsenv_plugin_directory_listing.js +65 -49
  33. package/src/plugins/protocol_file/jsenv_plugin_fs_redirection.js +36 -3
  34. package/src/plugins/protocol_file/jsenv_plugin_protocol_file.js +3 -0
  35. package/src/plugins/ribbon/client/ribbon.js +40 -37
  36. package/src/plugins/injections/internal/inject_globals.js +0 -52
@@ -1,10 +1,10 @@
1
1
  import { WebSocketResponse, pickContentType, ServerEvents, jsenvServiceCORS, jsenvAccessControlAllowedHeaders, composeTwoResponses, serveDirectory, jsenvServiceErrorHandler, startServer } from "@jsenv/server";
2
2
  import { convertFileSystemErrorToResponseProperties } from "@jsenv/server/src/internal/convertFileSystemErrorToResponseProperties.js";
3
- import { lookupPackageDirectory, registerDirectoryLifecycle, urlToRelativeUrl, moveUrl, urlIsInsideOf, ensureWindowsDriveLetter, createDetailedMessage, stringifyUrlSite, generateContentFrame, validateResponseIntegrity, setUrlFilename, getCallerPosition, urlToBasename, urlToExtension, asSpecifierWithoutSearch, asUrlWithoutSearch, injectQueryParamsIntoSpecifier, bufferToEtag, isFileSystemPath, urlToPathname, setUrlBasename, urlToFileSystemPath, writeFileSync, createLogger, URL_META, applyNodeEsmResolution, RUNTIME_COMPAT, normalizeUrl, ANSI, CONTENT_TYPE, errorToHTML, DATA_URL, normalizeImportMap, composeTwoImportMaps, resolveImport, JS_QUOTES, defaultLookupPackageScope, defaultReadPackageJson, readCustomConditionsFromProcessArgs, readEntryStatSync, urlToFilename, ensurePathnameTrailingSlash, compareFileUrls, applyFileSystemMagicResolution, getExtensionsToTry, setUrlExtension, isSpecifierForNodeBuiltin, memoizeByFirstArgument, assertAndNormalizeDirectoryUrl, createTaskLog, formatError, readPackageAtOrNull } from "./jsenv_core_packages.js";
3
+ import { lookupPackageDirectory, registerDirectoryLifecycle, urlToRelativeUrl, moveUrl, urlIsOrIsInsideOf, ensureWindowsDriveLetter, createDetailedMessage, stringifyUrlSite, generateContentFrame, validateResponseIntegrity, setUrlFilename, getCallerPosition, urlToBasename, urlToExtension, asSpecifierWithoutSearch, asUrlWithoutSearch, injectQueryParamsIntoSpecifier, bufferToEtag, isFileSystemPath, urlToPathname, setUrlBasename, urlToFileSystemPath, writeFileSync, createLogger, URL_META, applyNodeEsmResolution, RUNTIME_COMPAT, normalizeUrl, ANSI, CONTENT_TYPE, errorToHTML, DATA_URL, normalizeImportMap, composeTwoImportMaps, resolveImport, JS_QUOTES, defaultLookupPackageScope, defaultReadPackageJson, readCustomConditionsFromProcessArgs, readEntryStatSync, ensurePathnameTrailingSlash, compareFileUrls, urlToFilename, applyFileSystemMagicResolution, getExtensionsToTry, setUrlExtension, isSpecifierForNodeBuiltin, memoizeByFirstArgument, assertAndNormalizeDirectoryUrl, createTaskLog, formatError, readPackageAtOrNull } from "./jsenv_core_packages.js";
4
4
  import { readFileSync, existsSync, readdirSync, lstatSync, realpathSync } from "node:fs";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import { generateSourcemapFileUrl, createMagicSource, composeTwoSourcemaps, generateSourcemapDataUrl, SOURCEMAP } from "@jsenv/sourcemap";
7
- import { parseHtml, injectHtmlNodeAsEarlyAsPossible, createHtmlNode, stringifyHtmlAst, applyBabelPlugins, generateUrlForInlineContent, parseJsWithAcorn, parseCssUrls, getHtmlNodeAttribute, getHtmlNodePosition, getHtmlNodeAttributePosition, setHtmlNodeAttributes, parseSrcSet, getUrlForContentInsideHtml, removeHtmlNodeText, setHtmlNodeText, getHtmlNodeText, analyzeScriptNode, visitHtmlNodes, parseJsUrls, getUrlForContentInsideJs, analyzeLinkNode, injectJsenvScript } from "@jsenv/ast";
7
+ import { parseHtml, injectHtmlNodeAsEarlyAsPossible, createHtmlNode, stringifyHtmlAst, applyBabelPlugins, generateUrlForInlineContent, injectJsenvScript, parseJsWithAcorn, parseCssUrls, getHtmlNodeAttribute, getHtmlNodePosition, getHtmlNodeAttributePosition, setHtmlNodeAttributes, parseSrcSet, getUrlForContentInsideHtml, removeHtmlNodeText, setHtmlNodeText, getHtmlNodeText, analyzeScriptNode, visitHtmlNodes, parseJsUrls, getUrlForContentInsideJs, analyzeLinkNode } from "@jsenv/ast";
8
8
  import { performance } from "node:perf_hooks";
9
9
  import { jsenvPluginSupervisor } from "@jsenv/plugin-supervisor";
10
10
  import { jsenvPluginTranspilation } from "@jsenv/plugin-transpilation";
@@ -165,7 +165,7 @@ const watchSourceFiles = (
165
165
 
166
166
  const WEB_URL_CONVERTER = {
167
167
  asWebUrl: (fileUrl, webServer) => {
168
- if (urlIsInsideOf(fileUrl, webServer.rootDirectoryUrl)) {
168
+ if (urlIsOrIsInsideOf(fileUrl, webServer.rootDirectoryUrl)) {
169
169
  return moveUrl({
170
170
  url: fileUrl,
171
171
  from: webServer.rootDirectoryUrl,
@@ -365,7 +365,7 @@ ${error.message}`,
365
365
  name: "TRANSFORM_URL_CONTENT_ERROR",
366
366
  code: "PARSE_ERROR",
367
367
  reason: error.message,
368
- stack: error.stack,
368
+ stack: transformError.stack,
369
369
  trace,
370
370
  asResponse: error.asResponse,
371
371
  });
@@ -590,7 +590,7 @@ const determineFileUrlForOutDirectory = (urlInfo) => {
590
590
  if (!url.startsWith("file:")) {
591
591
  return url;
592
592
  }
593
- if (!urlIsInsideOf(url, rootDirectoryUrl)) {
593
+ if (!urlIsOrIsInsideOf(url, rootDirectoryUrl)) {
594
594
  const fsRootUrl = ensureWindowsDriveLetter("file:///", url);
595
595
  url = `${rootDirectoryUrl}@fs/${url.slice(fsRootUrl.length)}`;
596
596
  }
@@ -1898,6 +1898,7 @@ const createUrlInfo = (url, context) => {
1898
1898
  contentLength: undefined,
1899
1899
  contentFinalized: false,
1900
1900
  contentSideEffects: [],
1901
+ contentInjections: {},
1901
1902
 
1902
1903
  sourcemap: null,
1903
1904
  sourcemapIsWrong: false,
@@ -2162,6 +2163,176 @@ ${urlInfo.url}`,
2162
2163
  return urlInfo;
2163
2164
  };
2164
2165
 
2166
+ const injectionSymbol = Symbol.for("jsenv_injection");
2167
+ const INJECTIONS = {
2168
+ global: (value) => {
2169
+ return { [injectionSymbol]: "global", value };
2170
+ },
2171
+ optional: (value) => {
2172
+ return { [injectionSymbol]: "optional", value };
2173
+ },
2174
+ };
2175
+
2176
+ const isPlaceholderInjection = (value) => {
2177
+ return (
2178
+ !value || !value[injectionSymbol] || value[injectionSymbol] !== "global"
2179
+ );
2180
+ };
2181
+
2182
+ const applyContentInjections = (content, contentInjections, urlInfo) => {
2183
+ const keys = Object.keys(contentInjections);
2184
+ const globals = {};
2185
+ const placeholderReplacements = [];
2186
+ for (const key of keys) {
2187
+ const contentInjection = contentInjections[key];
2188
+ if (contentInjection && contentInjection[injectionSymbol]) {
2189
+ const valueBehindSymbol = contentInjection[injectionSymbol];
2190
+ if (valueBehindSymbol === "global") {
2191
+ globals[key] = contentInjection.value;
2192
+ } else if (valueBehindSymbol === "optional") {
2193
+ placeholderReplacements.push({
2194
+ key,
2195
+ isOptional: true,
2196
+ value: contentInjection.value,
2197
+ });
2198
+ } else {
2199
+ throw new Error(`unknown injection type "${valueBehindSymbol}"`);
2200
+ }
2201
+ } else {
2202
+ placeholderReplacements.push({
2203
+ key,
2204
+ value: contentInjection,
2205
+ });
2206
+ }
2207
+ }
2208
+
2209
+ const needGlobalsInjection = Object.keys(globals).length > 0;
2210
+ const needPlaceholderReplacements = placeholderReplacements.length > 0;
2211
+
2212
+ if (needGlobalsInjection && needPlaceholderReplacements) {
2213
+ const globalInjectionResult = injectGlobals(content, globals, urlInfo);
2214
+ const replaceInjectionResult = injectPlaceholderReplacements(
2215
+ globalInjectionResult.content,
2216
+ placeholderReplacements,
2217
+ urlInfo,
2218
+ );
2219
+ return {
2220
+ content: replaceInjectionResult.content,
2221
+ sourcemap: composeTwoSourcemaps(
2222
+ globalInjectionResult.sourcemap,
2223
+ replaceInjectionResult.sourcemap,
2224
+ ),
2225
+ };
2226
+ }
2227
+ if (needGlobalsInjection) {
2228
+ return injectGlobals(content, globals, urlInfo);
2229
+ }
2230
+ if (needPlaceholderReplacements) {
2231
+ return injectPlaceholderReplacements(
2232
+ content,
2233
+ placeholderReplacements,
2234
+ urlInfo,
2235
+ );
2236
+ }
2237
+ return null;
2238
+ };
2239
+
2240
+ const injectPlaceholderReplacements = (
2241
+ content,
2242
+ placeholderReplacements,
2243
+ urlInfo,
2244
+ ) => {
2245
+ const magicSource = createMagicSource(content);
2246
+ for (const { key, isOptional, value } of placeholderReplacements) {
2247
+ let index = content.indexOf(key);
2248
+ if (index === -1) {
2249
+ if (!isOptional) {
2250
+ urlInfo.context.logger.warn(
2251
+ `placeholder "${key}" not found in ${urlInfo.url}.
2252
+ --- suggestion a ---
2253
+ Add "${key}" in that file.
2254
+ --- suggestion b ---
2255
+ Fix eventual typo in "${key}"?
2256
+ --- suggestion c ---
2257
+ Mark injection as optional using INJECTIONS.optional():
2258
+ import { INJECTIONS } from "@jsenv/core";
2259
+
2260
+ return {
2261
+ "${key}": INJECTIONS.optional(${JSON.stringify(value)}),
2262
+ };`,
2263
+ );
2264
+ }
2265
+ continue;
2266
+ }
2267
+
2268
+ while (index !== -1) {
2269
+ const start = index;
2270
+ const end = index + key.length;
2271
+ magicSource.replace({
2272
+ start,
2273
+ end,
2274
+ replacement:
2275
+ urlInfo.type === "js_classic" ||
2276
+ urlInfo.type === "js_module" ||
2277
+ urlInfo.type === "html"
2278
+ ? JSON.stringify(value, null, " ")
2279
+ : value,
2280
+ });
2281
+ index = content.indexOf(key, end);
2282
+ }
2283
+ }
2284
+ return magicSource.toContentAndSourcemap();
2285
+ };
2286
+
2287
+ const injectGlobals = (content, globals, urlInfo) => {
2288
+ if (urlInfo.type === "html") {
2289
+ return globalInjectorOnHtml(content, globals, urlInfo);
2290
+ }
2291
+ if (urlInfo.type === "js_classic" || urlInfo.type === "js_module") {
2292
+ return globalsInjectorOnJs(content, globals, urlInfo);
2293
+ }
2294
+ throw new Error(`cannot inject globals into "${urlInfo.type}"`);
2295
+ };
2296
+ const globalInjectorOnHtml = (content, globals, urlInfo) => {
2297
+ // ideally we would inject an importmap but browser support is too low
2298
+ // (even worse for worker/service worker)
2299
+ // so for now we inject code into entry points
2300
+ const htmlAst = parseHtml({
2301
+ html: content,
2302
+ url: urlInfo.url,
2303
+ storeOriginalPositions: false,
2304
+ });
2305
+ const clientCode = generateClientCodeForGlobals(globals, {
2306
+ isWebWorker: false,
2307
+ });
2308
+ injectJsenvScript(htmlAst, {
2309
+ content: clientCode,
2310
+ pluginName: "jsenv:inject_globals",
2311
+ });
2312
+ return {
2313
+ content: stringifyHtmlAst(htmlAst),
2314
+ };
2315
+ };
2316
+ const globalsInjectorOnJs = (content, globals, urlInfo) => {
2317
+ const clientCode = generateClientCodeForGlobals(globals, {
2318
+ isWebWorker:
2319
+ urlInfo.subtype === "worker" ||
2320
+ urlInfo.subtype === "service_worker" ||
2321
+ urlInfo.subtype === "shared_worker",
2322
+ });
2323
+ const magicSource = createMagicSource(content);
2324
+ magicSource.prepend(clientCode);
2325
+ return magicSource.toContentAndSourcemap();
2326
+ };
2327
+ const generateClientCodeForGlobals = (globals, { isWebWorker = false }) => {
2328
+ const globalName = isWebWorker ? "self" : "window";
2329
+ return `Object.assign(${globalName}, ${JSON.stringify(
2330
+ globals,
2331
+ null,
2332
+ " ",
2333
+ )});`;
2334
+ };
2335
+
2165
2336
  const defineGettersOnPropertiesDerivedFromOriginalContent = (
2166
2337
  urlInfo,
2167
2338
  ) => {
@@ -2442,6 +2613,7 @@ const createUrlInfoTransformer = ({
2442
2613
  contentLength,
2443
2614
  sourcemap,
2444
2615
  sourcemapIsWrong,
2616
+ contentInjections,
2445
2617
  } = transformations;
2446
2618
  if (type) {
2447
2619
  urlInfo.type = type;
@@ -2449,13 +2621,23 @@ const createUrlInfoTransformer = ({
2449
2621
  if (contentType) {
2450
2622
  urlInfo.contentType = contentType;
2451
2623
  }
2452
- const contentModified = setContentProperties(urlInfo, {
2453
- content,
2454
- contentAst,
2455
- contentEtag,
2456
- contentLength,
2457
- });
2458
-
2624
+ if (Object.hasOwn(transformations, "contentInjections")) {
2625
+ if (contentInjections) {
2626
+ Object.assign(urlInfo.contentInjections, contentInjections);
2627
+ }
2628
+ if (content === undefined) {
2629
+ return;
2630
+ }
2631
+ }
2632
+ let contentModified;
2633
+ if (Object.hasOwn(transformations, "content")) {
2634
+ contentModified = setContentProperties(urlInfo, {
2635
+ content,
2636
+ contentAst,
2637
+ contentEtag,
2638
+ contentLength,
2639
+ });
2640
+ }
2459
2641
  if (
2460
2642
  sourcemap &&
2461
2643
  mayHaveSourcemap(urlInfo) &&
@@ -2647,6 +2829,15 @@ const createUrlInfoTransformer = ({
2647
2829
  if (transformations) {
2648
2830
  applyTransformations(urlInfo, transformations);
2649
2831
  }
2832
+ const { contentInjections } = urlInfo;
2833
+ if (contentInjections && Object.keys(contentInjections).length > 0) {
2834
+ const injectionTransformations = applyContentInjections(
2835
+ urlInfo.content,
2836
+ contentInjections,
2837
+ urlInfo,
2838
+ );
2839
+ applyTransformations(urlInfo, injectionTransformations);
2840
+ }
2650
2841
  applyContentEffects(urlInfo);
2651
2842
  urlInfo.contentFinalized = true;
2652
2843
  };
@@ -2792,6 +2983,7 @@ const createKitchen = ({
2792
2983
  inlineContentClientFileUrl,
2793
2984
  isSupportedOnCurrentClients: memoizeIsSupported(clientRuntimeCompat),
2794
2985
  isSupportedOnFutureClients: memoizeIsSupported(runtimeCompat),
2986
+ isPlaceholderInjection,
2795
2987
  getPluginMeta: null,
2796
2988
  sourcemaps,
2797
2989
  outDirectoryUrl,
@@ -3632,10 +3824,10 @@ const generateHtmlForSyntaxError = (
3632
3824
  errorLinkText: `${htmlRelativeUrl}:${line}:${column}`,
3633
3825
  syntaxErrorHTML: errorToHTML(htmlErrorContentFrame),
3634
3826
  };
3635
- const html = replacePlaceholders$1(htmlForSyntaxError, replacers);
3827
+ const html = replacePlaceholders(htmlForSyntaxError, replacers);
3636
3828
  return html;
3637
3829
  };
3638
- const replacePlaceholders$1 = (html, replacers) => {
3830
+ const replacePlaceholders = (html, replacers) => {
3639
3831
  return html.replace(/\$\{(\w+)\}/g, (match, name) => {
3640
3832
  const replacer = replacers[name];
3641
3833
  if (replacer === undefined) {
@@ -3648,10 +3840,19 @@ const replacePlaceholders$1 = (html, replacers) => {
3648
3840
  });
3649
3841
  };
3650
3842
 
3651
- const createPluginStore = (plugins) => {
3843
+ const createPluginStore = async (plugins) => {
3652
3844
  const allDevServerRoutes = [];
3845
+ const allDevServerServices = [];
3653
3846
  const pluginArray = [];
3654
- const addPlugin = (plugin) => {
3847
+
3848
+ const pluginPromises = [];
3849
+ const addPlugin = async (plugin) => {
3850
+ if (plugin && typeof plugin.then === "function") {
3851
+ pluginPromises.push(plugin);
3852
+ const value = await plugin;
3853
+ addPlugin(value);
3854
+ return;
3855
+ }
3655
3856
  if (Array.isArray(plugin)) {
3656
3857
  for (const subplugin of plugin) {
3657
3858
  addPlugin(subplugin);
@@ -3670,21 +3871,28 @@ const createPluginStore = (plugins) => {
3670
3871
  allDevServerRoutes.push(devServerRoute);
3671
3872
  }
3672
3873
  }
3874
+ if (plugin.devServerServices) {
3875
+ const devServerServices = plugin.devServerServices;
3876
+ for (const devServerService of devServerServices) {
3877
+ allDevServerServices.push(devServerService);
3878
+ }
3879
+ }
3673
3880
  pluginArray.push(plugin);
3674
3881
  };
3675
3882
  addPlugin(jsenvPluginHtmlSyntaxErrorFallback());
3676
3883
  for (const plugin of plugins) {
3677
3884
  addPlugin(plugin);
3678
3885
  }
3886
+ await Promise.all(pluginPromises);
3679
3887
 
3680
3888
  return {
3681
3889
  pluginArray,
3682
-
3683
3890
  allDevServerRoutes,
3891
+ allDevServerServices,
3684
3892
  };
3685
3893
  };
3686
3894
 
3687
- const createPluginController = (
3895
+ const createPluginController = async (
3688
3896
  pluginStore,
3689
3897
  kitchen,
3690
3898
  { initialPuginsMeta = {} } = {},
@@ -3707,7 +3915,7 @@ const createPluginController = (
3707
3915
  pluginCandidate.destroy?.();
3708
3916
  continue;
3709
3917
  }
3710
- const initPluginResult = initPlugin(pluginCandidate, kitchen);
3918
+ const initPluginResult = await initPlugin(pluginCandidate, kitchen);
3711
3919
  if (!initPluginResult) {
3712
3920
  pluginCandidate.destroy?.();
3713
3921
  continue;
@@ -3759,6 +3967,7 @@ const createPluginController = (
3759
3967
  key === "serverEvents" ||
3760
3968
  key === "mustStayFirst" ||
3761
3969
  key === "devServerRoutes" ||
3970
+ key === "devServerServices" ||
3762
3971
  key === "effect"
3763
3972
  ) {
3764
3973
  continue;
@@ -3932,6 +4141,7 @@ const createPluginController = (
3932
4141
  const HOOK_NAMES = [
3933
4142
  "init",
3934
4143
  "devServerRoutes", // is called only during dev/tests
4144
+ "devServerServices", // is called only during dev/tests
3935
4145
  "resolveReference",
3936
4146
  "redirectReference",
3937
4147
  "transformReferenceSearchParams",
@@ -3986,12 +4196,12 @@ const testAppliesDuring = (plugin, kitchen) => {
3986
4196
  `"appliesDuring" must be an object or a string, got ${appliesDuring}`,
3987
4197
  );
3988
4198
  };
3989
- const initPlugin = (plugin, kitchen) => {
4199
+ const initPlugin = async (plugin, kitchen) => {
3990
4200
  const { init } = plugin;
3991
4201
  if (!init) {
3992
4202
  return true;
3993
4203
  }
3994
- const initReturnValue = init(kitchen.context, { plugin });
4204
+ const initReturnValue = await init(kitchen.context, { plugin });
3995
4205
  if (initReturnValue === false) {
3996
4206
  return false;
3997
4207
  }
@@ -4070,6 +4280,9 @@ const returnValueAssertions = [
4070
4280
  return undefined;
4071
4281
  }
4072
4282
  if (typeof content !== "string" && !Buffer.isBuffer(content) && !body) {
4283
+ if (Object.hasOwn(valueReturned, "contentInjections")) {
4284
+ return undefined;
4285
+ }
4073
4286
  throw new Error(
4074
4287
  `Unexpected "content" returned by "${hook.plugin.name}" ${hook.name} hook: it must be a string or a buffer; got ${content}`,
4075
4288
  );
@@ -5848,10 +6061,7 @@ const jsenvPluginVersionSearchParam = () => {
5848
6061
 
5849
6062
  const FILE_AND_SERVER_URLS_CONVERTER = {
5850
6063
  asServerUrl: (fileUrl, serverRootDirectoryUrl) => {
5851
- if (fileUrl === serverRootDirectoryUrl) {
5852
- return "/";
5853
- }
5854
- if (urlIsInsideOf(fileUrl, serverRootDirectoryUrl)) {
6064
+ if (urlIsOrIsInsideOf(fileUrl, serverRootDirectoryUrl)) {
5855
6065
  const urlRelativeToServer = urlToRelativeUrl(
5856
6066
  fileUrl,
5857
6067
  serverRootDirectoryUrl,
@@ -5877,103 +6087,6 @@ const FILE_AND_SERVER_URLS_CONVERTER = {
5877
6087
  },
5878
6088
  };
5879
6089
 
5880
- const jsenvPluginInjections = (rawAssociations) => {
5881
- let resolvedAssociations;
5882
-
5883
- return {
5884
- name: "jsenv:injections",
5885
- appliesDuring: "*",
5886
- init: (context) => {
5887
- resolvedAssociations = URL_META.resolveAssociations(
5888
- { injectionsGetter: rawAssociations },
5889
- context.rootDirectoryUrl,
5890
- );
5891
- },
5892
- transformUrlContent: async (urlInfo) => {
5893
- const { injectionsGetter } = URL_META.applyAssociations({
5894
- url: asUrlWithoutSearch(urlInfo.url),
5895
- associations: resolvedAssociations,
5896
- });
5897
- if (!injectionsGetter) {
5898
- return null;
5899
- }
5900
- if (typeof injectionsGetter !== "function") {
5901
- throw new TypeError("injectionsGetter must be a function");
5902
- }
5903
- const injections = await injectionsGetter(urlInfo);
5904
- if (!injections) {
5905
- return null;
5906
- }
5907
- const keys = Object.keys(injections);
5908
- if (keys.length === 0) {
5909
- return null;
5910
- }
5911
- return replacePlaceholders(urlInfo.content, injections, urlInfo);
5912
- },
5913
- };
5914
- };
5915
-
5916
- const injectionSymbol = Symbol.for("jsenv_injection");
5917
- const INJECTIONS = {
5918
- optional: (value) => {
5919
- return { [injectionSymbol]: "optional", value };
5920
- },
5921
- };
5922
-
5923
- // we export this because it is imported by jsenv_plugin_placeholder.js and unit test
5924
- const replacePlaceholders = (content, replacements, urlInfo) => {
5925
- const magicSource = createMagicSource(content);
5926
- for (const key of Object.keys(replacements)) {
5927
- let index = content.indexOf(key);
5928
- const replacement = replacements[key];
5929
- let isOptional;
5930
- let value;
5931
- if (replacement && replacement[injectionSymbol]) {
5932
- const valueBehindSymbol = replacement[injectionSymbol];
5933
- isOptional = valueBehindSymbol === "optional";
5934
- value = replacement.value;
5935
- } else {
5936
- value = replacement;
5937
- }
5938
- if (index === -1) {
5939
- if (!isOptional) {
5940
- urlInfo.context.logger.warn(
5941
- `placeholder "${key}" not found in ${urlInfo.url}.
5942
- --- suggestion a ---
5943
- Add "${key}" in that file.
5944
- --- suggestion b ---
5945
- Fix eventual typo in "${key}"?
5946
- --- suggestion c ---
5947
- Mark injection as optional using INJECTIONS.optional():
5948
- import { INJECTIONS } from "@jsenv/core";
5949
-
5950
- return {
5951
- "${key}": INJECTIONS.optional(${JSON.stringify(value)}),
5952
- };`,
5953
- );
5954
- }
5955
- continue;
5956
- }
5957
-
5958
- while (index !== -1) {
5959
- const start = index;
5960
- const end = index + key.length;
5961
- magicSource.replace({
5962
- start,
5963
- end,
5964
- replacement:
5965
- urlInfo.type === "js_classic" ||
5966
- urlInfo.type === "js_module" ||
5967
- urlInfo.type === "html"
5968
- ? JSON.stringify(value, null, " ")
5969
- : value,
5970
- });
5971
- index = content.indexOf(key, end);
5972
- }
5973
- }
5974
- return magicSource.toContentAndSourcemap();
5975
- };
5976
-
5977
6090
  /*
5978
6091
  * NICE TO HAVE:
5979
6092
  *
@@ -6004,6 +6117,7 @@ const htmlFileUrlForDirectory = import.meta.resolve(
6004
6117
  );
6005
6118
 
6006
6119
  const jsenvPluginDirectoryListing = ({
6120
+ spa,
6007
6121
  urlMocks = false,
6008
6122
  autoreload = true,
6009
6123
  directoryContentMagicName,
@@ -6045,7 +6159,7 @@ const jsenvPluginDirectoryListing = ({
6045
6159
  return null;
6046
6160
  }
6047
6161
  }
6048
- return `${htmlFileUrlForDirectory}?url=${encodeURIComponent(url)}&enoent`;
6162
+ return `${htmlFileUrlForDirectory}?url=${encodeURIComponent(requestedUrl)}&enoent`;
6049
6163
  }
6050
6164
  const isDirectory = fsStat?.isDirectory();
6051
6165
  if (!isDirectory) {
@@ -6071,34 +6185,35 @@ const jsenvPluginDirectoryListing = ({
6071
6185
  if (urlWithoutSearch !== String(htmlFileUrlForDirectory)) {
6072
6186
  return null;
6073
6187
  }
6074
- const requestedUrl = urlInfo.searchParams.get("url");
6075
- if (!requestedUrl) {
6188
+ const urlNotFound = urlInfo.searchParams.get("url");
6189
+ if (!urlNotFound) {
6076
6190
  return null;
6077
6191
  }
6192
+
6078
6193
  urlInfo.headers["cache-control"] = "no-cache";
6079
6194
  const enoent = urlInfo.searchParams.has("enoent");
6080
6195
  if (enoent) {
6081
6196
  urlInfo.status = 404;
6082
- urlInfo.headers["cache-control"] = "no-cache";
6083
6197
  }
6084
6198
  const request = urlInfo.context.request;
6085
6199
  const { rootDirectoryUrl, mainFilePath } = urlInfo.context;
6086
- return replacePlaceholders(
6087
- urlInfo.content,
6200
+ const directoryListingInjections = generateDirectoryListingInjection(
6201
+ urlNotFound,
6088
6202
  {
6089
- ...generateDirectoryListingInjection(requestedUrl, {
6090
- autoreload,
6091
- request,
6092
- urlMocks,
6093
- directoryContentMagicName,
6094
- rootDirectoryUrl,
6095
- mainFilePath,
6096
- packageDirectory,
6097
- enoent,
6098
- }),
6203
+ spa,
6204
+ autoreload,
6205
+ request,
6206
+ urlMocks,
6207
+ directoryContentMagicName,
6208
+ rootDirectoryUrl,
6209
+ mainFilePath,
6210
+ packageDirectory,
6211
+ enoent,
6099
6212
  },
6100
- urlInfo,
6101
6213
  );
6214
+ return {
6215
+ contentInjections: directoryListingInjections,
6216
+ };
6102
6217
  },
6103
6218
  },
6104
6219
  devServerRoutes: [
@@ -6117,8 +6232,10 @@ const jsenvPluginDirectoryListing = ({
6117
6232
  directoryRelativeUrl,
6118
6233
  rootDirectoryUrl,
6119
6234
  );
6120
- const closestDirectoryUrl =
6121
- getFirstExistingDirectoryUrl(requestedUrl);
6235
+ const closestDirectoryUrl = getFirstExistingDirectoryUrl(
6236
+ requestedUrl,
6237
+ rootDirectoryUrl,
6238
+ );
6122
6239
  const sendMessage = (message) => {
6123
6240
  websocket.send(JSON.stringify(message));
6124
6241
  };
@@ -6176,8 +6293,9 @@ const jsenvPluginDirectoryListing = ({
6176
6293
  };
6177
6294
 
6178
6295
  const generateDirectoryListingInjection = (
6179
- requestedUrl,
6296
+ urlNotFound,
6180
6297
  {
6298
+ spa,
6181
6299
  rootDirectoryUrl,
6182
6300
  mainFilePath,
6183
6301
  packageDirectory,
@@ -6190,7 +6308,7 @@ const generateDirectoryListingInjection = (
6190
6308
  ) => {
6191
6309
  let serverRootDirectoryUrl = rootDirectoryUrl;
6192
6310
  const firstExistingDirectoryUrl = getFirstExistingDirectoryUrl(
6193
- requestedUrl,
6311
+ urlNotFound,
6194
6312
  serverRootDirectoryUrl,
6195
6313
  );
6196
6314
  const directoryContentItems = getDirectoryContentItems({
@@ -6241,8 +6359,8 @@ const generateDirectoryListingInjection = (
6241
6359
  const { host } = new URL(request.url);
6242
6360
  const websocketUrl = `${websocketScheme}://${host}/.internal/directory_content.websocket?directory=${encodeURIComponent(directoryUrlRelativeToServer)}`;
6243
6361
 
6244
- const navItems = [];
6245
- {
6362
+ const generateBreadcrumb = () => {
6363
+ const breadcrumb = [];
6246
6364
  const lastItemUrl = firstExistingDirectoryUrl;
6247
6365
  const lastItemRelativeUrl = urlToRelativeUrl(lastItemUrl, rootDirectoryUrl);
6248
6366
  const rootDirectoryUrlName = urlToFilename(rootDirectoryUrl);
@@ -6252,7 +6370,6 @@ const generateDirectoryListingInjection = (
6252
6370
  } else {
6253
6371
  parts = [rootDirectoryUrlName];
6254
6372
  }
6255
-
6256
6373
  let i = 0;
6257
6374
  while (i < parts.length) {
6258
6375
  const part = parts[i];
@@ -6273,7 +6390,7 @@ const generateDirectoryListingInjection = (
6273
6390
  navItemUrl,
6274
6391
  serverRootDirectoryUrl,
6275
6392
  );
6276
- let urlRelativeToDocument = urlToRelativeUrl(navItemUrl, requestedUrl);
6393
+ let urlRelativeToDocument = urlToRelativeUrl(navItemUrl, urlNotFound);
6277
6394
  const isServerRootDirectory = navItemUrl === serverRootDirectoryUrl;
6278
6395
  if (isServerRootDirectory) {
6279
6396
  urlRelativeToServer = `/${directoryContentMagicName}`;
@@ -6281,7 +6398,7 @@ const generateDirectoryListingInjection = (
6281
6398
  }
6282
6399
  const name = part;
6283
6400
  const isCurrent = navItemUrl === String(firstExistingDirectoryUrl);
6284
- navItems.push({
6401
+ breadcrumb.push({
6285
6402
  url: navItemUrl,
6286
6403
  urlRelativeToServer,
6287
6404
  urlRelativeToDocument,
@@ -6291,34 +6408,47 @@ const generateDirectoryListingInjection = (
6291
6408
  });
6292
6409
  i++;
6293
6410
  }
6294
- }
6411
+ return breadcrumb;
6412
+ };
6413
+ const breadcrumb = generateBreadcrumb();
6295
6414
 
6296
6415
  let enoentDetails = null;
6297
6416
  if (enoent) {
6417
+ const buildEnoentPathInfo = (urlBase, closestExistingUrl) => {
6418
+ let filePathExisting;
6419
+ let filePathNotFound;
6420
+ const existingIndex = String(closestExistingUrl).length;
6421
+ filePathExisting = urlToRelativeUrl(
6422
+ closestExistingUrl,
6423
+ serverRootDirectoryUrl,
6424
+ );
6425
+ filePathNotFound = urlBase.slice(existingIndex);
6426
+ return [filePathExisting, filePathNotFound];
6427
+ };
6298
6428
  const fileRelativeUrl = urlToRelativeUrl(
6299
- requestedUrl,
6429
+ urlNotFound,
6300
6430
  serverRootDirectoryUrl,
6301
6431
  );
6302
- let filePathExisting;
6303
- let filePathNotFound;
6304
- const existingIndex = String(firstExistingDirectoryUrl).length;
6305
- filePathExisting = urlToRelativeUrl(
6306
- firstExistingDirectoryUrl,
6307
- serverRootDirectoryUrl,
6308
- );
6309
- filePathNotFound = requestedUrl.slice(existingIndex);
6310
6432
  enoentDetails = {
6311
- fileUrl: requestedUrl,
6433
+ fileUrl: urlNotFound,
6312
6434
  fileRelativeUrl,
6435
+ };
6436
+
6437
+ const [filePathExisting, filePathNotFound] = buildEnoentPathInfo(
6438
+ urlNotFound,
6439
+ firstExistingDirectoryUrl,
6440
+ );
6441
+ Object.assign(enoentDetails, {
6313
6442
  filePathExisting: `/${filePathExisting}`,
6314
6443
  filePathNotFound,
6315
- };
6444
+ });
6316
6445
  }
6317
6446
 
6318
6447
  return {
6319
6448
  __DIRECTORY_LISTING__: {
6449
+ spa,
6320
6450
  enoentDetails,
6321
- navItems,
6451
+ breadcrumb,
6322
6452
  urlMocks,
6323
6453
  directoryContentMagicName,
6324
6454
  directoryUrl: firstExistingDirectoryUrl,
@@ -6331,16 +6461,16 @@ const generateDirectoryListingInjection = (
6331
6461
  },
6332
6462
  };
6333
6463
  };
6334
- const getFirstExistingDirectoryUrl = (requestedUrl, serverRootDirectoryUrl) => {
6335
- let firstExistingDirectoryUrl = new URL("./", requestedUrl);
6336
- while (!existsSync(firstExistingDirectoryUrl)) {
6337
- firstExistingDirectoryUrl = new URL("../", firstExistingDirectoryUrl);
6338
- if (!urlIsInsideOf(firstExistingDirectoryUrl, serverRootDirectoryUrl)) {
6339
- firstExistingDirectoryUrl = new URL(serverRootDirectoryUrl);
6464
+ const getFirstExistingDirectoryUrl = (urlBase, serverRootDirectoryUrl) => {
6465
+ let directoryUrlCandidate = new URL("./", urlBase);
6466
+ while (!existsSync(directoryUrlCandidate)) {
6467
+ directoryUrlCandidate = new URL("../", directoryUrlCandidate);
6468
+ if (!urlIsOrIsInsideOf(directoryUrlCandidate, serverRootDirectoryUrl)) {
6469
+ directoryUrlCandidate = new URL(serverRootDirectoryUrl);
6340
6470
  break;
6341
6471
  }
6342
6472
  }
6343
- return firstExistingDirectoryUrl;
6473
+ return directoryUrlCandidate;
6344
6474
  };
6345
6475
  const getDirectoryContentItems = ({
6346
6476
  serverRootDirectoryUrl,
@@ -6384,6 +6514,7 @@ const getDirectoryContentItems = ({
6384
6514
  };
6385
6515
 
6386
6516
  const jsenvPluginFsRedirection = ({
6517
+ spa,
6387
6518
  directoryContentMagicName,
6388
6519
  magicExtensions = ["inherit", ".js"],
6389
6520
  magicDirectoryIndex = true,
@@ -6473,11 +6604,19 @@ const jsenvPluginFsRedirection = ({
6473
6604
  // 3. The url pathname does not ends with "/"
6474
6605
  // In that case we assume client explicitely asks to load a directory
6475
6606
  if (
6607
+ spa &&
6476
6608
  !urlToExtension(urlObject) &&
6477
6609
  !urlToPathname(urlObject).endsWith("/")
6478
6610
  ) {
6479
- const { mainFilePath, rootDirectoryUrl } =
6611
+ const { requestedUrl, rootDirectoryUrl, mainFilePath } =
6480
6612
  reference.ownerUrlInfo.context;
6613
+ const closestHtmlRootFile = getClosestHtmlRootFile(
6614
+ requestedUrl,
6615
+ rootDirectoryUrl,
6616
+ );
6617
+ if (closestHtmlRootFile) {
6618
+ return closestHtmlRootFile;
6619
+ }
6481
6620
  return new URL(mainFilePath, rootDirectoryUrl);
6482
6621
  }
6483
6622
  return null;
@@ -6527,9 +6666,29 @@ const resolveSymlink = (fileUrl) => {
6527
6666
  return realUrlObject.href;
6528
6667
  };
6529
6668
 
6669
+ const getClosestHtmlRootFile = (requestedUrl, serverRootDirectoryUrl) => {
6670
+ let directoryUrl = new URL("./", requestedUrl);
6671
+ while (true) {
6672
+ const indexHtmlFileUrl = new URL(`index.html`, directoryUrl);
6673
+ if (existsSync(indexHtmlFileUrl)) {
6674
+ return indexHtmlFileUrl.href;
6675
+ }
6676
+ const filename = urlToFilename(directoryUrl);
6677
+ const htmlFileUrlCandidate = new URL(`${filename}.html`, directoryUrl);
6678
+ if (existsSync(htmlFileUrlCandidate)) {
6679
+ return htmlFileUrlCandidate.href;
6680
+ }
6681
+ if (!urlIsOrIsInsideOf(directoryUrl, serverRootDirectoryUrl)) {
6682
+ return null;
6683
+ }
6684
+ directoryUrl = new URL("../", directoryUrl);
6685
+ }
6686
+ };
6687
+
6530
6688
  const directoryContentMagicName = "...";
6531
6689
 
6532
6690
  const jsenvPluginProtocolFile = ({
6691
+ spa = true,
6533
6692
  magicExtensions,
6534
6693
  magicDirectoryIndex,
6535
6694
  preserveSymlinks,
@@ -6541,6 +6700,7 @@ const jsenvPluginProtocolFile = ({
6541
6700
  }) => {
6542
6701
  return [
6543
6702
  jsenvPluginFsRedirection({
6703
+ spa,
6544
6704
  directoryContentMagicName,
6545
6705
  magicExtensions,
6546
6706
  magicDirectoryIndex,
@@ -6596,6 +6756,7 @@ const jsenvPluginProtocolFile = ({
6596
6756
  ...(directoryListing
6597
6757
  ? [
6598
6758
  jsenvPluginDirectoryListing({
6759
+ spa,
6599
6760
  ...directoryListing,
6600
6761
  directoryContentMagicName,
6601
6762
  rootDirectoryUrl,
@@ -6872,6 +7033,69 @@ const jsenvPluginDirectoryReferenceEffect = (
6872
7033
  };
6873
7034
  };
6874
7035
 
7036
+ const jsenvPluginInjections = (rawAssociations) => {
7037
+ const getDefaultInjections = (urlInfo) => {
7038
+ if (urlInfo.context.dev && urlInfo.type === "html") {
7039
+ const relativeUrl = urlToRelativeUrl(
7040
+ urlInfo.url,
7041
+ urlInfo.context.rootDirectoryUrl,
7042
+ );
7043
+ return {
7044
+ HTML_ROOT_PATHNAME: INJECTIONS.global(`/${relativeUrl}`),
7045
+ };
7046
+ }
7047
+ return null;
7048
+ };
7049
+ let getInjections = null;
7050
+
7051
+ return {
7052
+ name: "jsenv:injections",
7053
+ appliesDuring: "*",
7054
+ init: (context) => {
7055
+ if (rawAssociations && Object.keys(rawAssociations).length > 0) {
7056
+ const resolvedAssociations = URL_META.resolveAssociations(
7057
+ { injectionsGetter: rawAssociations },
7058
+ context.rootDirectoryUrl,
7059
+ );
7060
+ getInjections = (urlInfo) => {
7061
+ const { injectionsGetter } = URL_META.applyAssociations({
7062
+ url: asUrlWithoutSearch(urlInfo.url),
7063
+ associations: resolvedAssociations,
7064
+ });
7065
+ if (!injectionsGetter) {
7066
+ return null;
7067
+ }
7068
+ if (typeof injectionsGetter !== "function") {
7069
+ throw new TypeError("injectionsGetter must be a function");
7070
+ }
7071
+ return injectionsGetter(urlInfo);
7072
+ };
7073
+ }
7074
+ },
7075
+ transformUrlContent: async (urlInfo) => {
7076
+ const defaultInjections = getDefaultInjections(urlInfo);
7077
+ if (!getInjections) {
7078
+ return {
7079
+ contentInjections: defaultInjections,
7080
+ };
7081
+ }
7082
+ const injectionsResult = getInjections(urlInfo);
7083
+ if (!injectionsResult) {
7084
+ return {
7085
+ contentInjections: defaultInjections,
7086
+ };
7087
+ }
7088
+ const injections = await injectionsResult;
7089
+ return {
7090
+ contentInjections: {
7091
+ ...defaultInjections,
7092
+ ...injections,
7093
+ },
7094
+ };
7095
+ },
7096
+ };
7097
+ };
7098
+
6875
7099
  const jsenvPluginInliningAsDataUrl = () => {
6876
7100
  return {
6877
7101
  name: "jsenv:inlining_as_data_url",
@@ -7317,6 +7541,8 @@ const babelPluginMetadataExpressionPaths = (
7317
7541
  * - replaced by true: When scenario matches (import.meta.dev and it's the dev server)
7318
7542
  * - left as is to be evaluated to undefined (import.meta.build but it's the dev server)
7319
7543
  * - replaced by undefined (import.meta.dev but it's build; the goal is to ensure it's tree-shaked)
7544
+ *
7545
+ * TODO: ideally during dev we would keep import.meta.dev and ensure we set it to true rather than replacing it with true?
7320
7546
  */
7321
7547
 
7322
7548
 
@@ -7422,14 +7648,12 @@ const babelPluginMetadataImportMetaScenarios = () => {
7422
7648
 
7423
7649
  const jsenvPluginGlobalScenarios = () => {
7424
7650
  const transformIfNeeded = (urlInfo) => {
7425
- return replacePlaceholders(
7426
- urlInfo.content,
7427
- {
7651
+ return {
7652
+ contentInjections: {
7428
7653
  __DEV__: INJECTIONS.optional(urlInfo.context.dev),
7429
7654
  __BUILD__: INJECTIONS.optional(urlInfo.context.build),
7430
7655
  },
7431
- urlInfo,
7432
- );
7656
+ };
7433
7657
  };
7434
7658
 
7435
7659
  return {
@@ -7862,7 +8086,7 @@ const jsenvPluginAutoreloadServer = ({
7862
8086
  serverEvents: {
7863
8087
  reload: (serverEventInfo) => {
7864
8088
  const formatUrlForClient = (url) => {
7865
- if (urlIsInsideOf(url, serverEventInfo.rootDirectoryUrl)) {
8089
+ if (urlIsOrIsInsideOf(url, serverEventInfo.rootDirectoryUrl)) {
7866
8090
  return urlToRelativeUrl(url, serverEventInfo.rootDirectoryUrl);
7867
8091
  }
7868
8092
  if (url.startsWith("file:")) {
@@ -8437,6 +8661,7 @@ const jsenvPluginChromeDevtoolsJson = () => {
8437
8661
  devServerRoutes: [
8438
8662
  {
8439
8663
  endpoint: "GET /.well-known/appspecific/com.chrome.devtools.json",
8664
+ declarationSource: import.meta.url,
8440
8665
  fetch: (request, { kitchen }) => {
8441
8666
  const { rootDirectoryUrl } = kitchen.context;
8442
8667
  return Response.json({
@@ -8672,6 +8897,7 @@ const getCorePlugins = ({
8672
8897
  transpilation = true,
8673
8898
  inlining = true,
8674
8899
  http = false,
8900
+ spa,
8675
8901
 
8676
8902
  clientAutoreload,
8677
8903
  clientAutoreloadOnServerRestart,
@@ -8701,7 +8927,7 @@ const getCorePlugins = ({
8701
8927
 
8702
8928
  return [
8703
8929
  jsenvPluginReferenceAnalysis(referenceAnalysis),
8704
- ...(injections ? [jsenvPluginInjections(injections)] : []),
8930
+ jsenvPluginInjections(injections),
8705
8931
  jsenvPluginTranspilation(transpilation),
8706
8932
  // "jsenvPluginInlining" must be very soon because all other plugins will react differently once they see the file is inlined
8707
8933
  ...(inlining ? [jsenvPluginInlining()] : []),
@@ -8714,6 +8940,7 @@ const getCorePlugins = ({
8714
8940
  */
8715
8941
  jsenvPluginProtocolHttp(http),
8716
8942
  jsenvPluginProtocolFile({
8943
+ spa,
8717
8944
  magicExtensions,
8718
8945
  magicDirectoryIndex,
8719
8946
  directoryListing,
@@ -8956,6 +9183,7 @@ const startDevServer = async ({
8956
9183
  ribbon = true,
8957
9184
  // toolbar = false,
8958
9185
  onKitchenCreated = () => {},
9186
+ spa,
8959
9187
 
8960
9188
  sourcemaps = "inline",
8961
9189
  sourcemapsSourcesContent,
@@ -9106,7 +9334,7 @@ const startDevServer = async ({
9106
9334
  read: readPackageAtOrNull,
9107
9335
  };
9108
9336
 
9109
- const devServerPluginStore = createPluginStore([
9337
+ const devServerPluginStore = await createPluginStore([
9110
9338
  jsenvPluginServerEvents({ clientAutoreload }),
9111
9339
  ...plugins,
9112
9340
  ...getCorePlugins({
@@ -9124,6 +9352,7 @@ const startDevServer = async ({
9124
9352
  supervisor,
9125
9353
  injections,
9126
9354
  transpilation,
9355
+ spa,
9127
9356
 
9128
9357
  clientAutoreload,
9129
9358
  clientAutoreloadOnServerRestart,
@@ -9131,7 +9360,7 @@ const startDevServer = async ({
9131
9360
  ribbon,
9132
9361
  }),
9133
9362
  ]);
9134
- const getOrCreateKitchen = (request) => {
9363
+ const getOrCreateKitchen = async (request) => {
9135
9364
  const { runtimeName, runtimeVersion } = parseUserAgentHeader(
9136
9365
  request.headers["user-agent"] || "",
9137
9366
  );
@@ -9253,7 +9482,7 @@ const startDevServer = async ({
9253
9482
  );
9254
9483
  },
9255
9484
  );
9256
- const devServerPluginController = createPluginController(
9485
+ const devServerPluginController = await createPluginController(
9257
9486
  devServerPluginStore,
9258
9487
  kitchen,
9259
9488
  );
@@ -9269,8 +9498,8 @@ const startDevServer = async ({
9269
9498
 
9270
9499
  finalServices.push({
9271
9500
  name: "jsenv:dev_server_routes",
9272
- augmentRouteFetchSecondArg: (request) => {
9273
- const kitchen = getOrCreateKitchen(request);
9501
+ augmentRouteFetchSecondArg: async (request) => {
9502
+ const kitchen = await getOrCreateKitchen(request);
9274
9503
  return { kitchen };
9275
9504
  },
9276
9505
  routes: [
@@ -9473,6 +9702,7 @@ const startDevServer = async ({
9473
9702
  },
9474
9703
  ],
9475
9704
  });
9705
+ finalServices.push(...devServerPluginStore.allDevServerServices);
9476
9706
  }
9477
9707
  // jsenv error handler service
9478
9708
  {
@@ -9501,7 +9731,7 @@ const startDevServer = async ({
9501
9731
  body: response.body,
9502
9732
  });
9503
9733
  return {
9504
- status: 200,
9734
+ status: response.status,
9505
9735
  headers: {
9506
9736
  "content-type": "application/json",
9507
9737
  "content-length": Buffer.byteLength(body),