@jsenv/core 40.6.2 → 40.7.0

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.
@@ -1,4 +1,4 @@
1
- import { parseHtml, injectHtmlNodeAsEarlyAsPossible, createHtmlNode, stringifyHtmlAst, applyBabelPlugins, generateUrlForInlineContent, parseJsWithAcorn, visitHtmlNodes, analyzeScriptNode, getHtmlNodeText, getHtmlNodeAttribute, getHtmlNodePosition, getUrlForContentInsideHtml, setHtmlNodeAttributes, setHtmlNodeText, parseCssUrls, getHtmlNodeAttributePosition, parseSrcSet, removeHtmlNodeText, parseJsUrls, getUrlForContentInsideJs, analyzeLinkNode, injectJsenvScript, findHtmlNode, removeHtmlNode, insertHtmlNodeAfter } from "@jsenv/ast";
1
+ import { parseHtml, injectHtmlNodeAsEarlyAsPossible, createHtmlNode, stringifyHtmlAst, applyBabelPlugins, generateUrlForInlineContent, injectJsenvScript, parseJsWithAcorn, visitHtmlNodes, analyzeScriptNode, getHtmlNodeText, getHtmlNodeAttribute, getHtmlNodePosition, getUrlForContentInsideHtml, setHtmlNodeAttributes, setHtmlNodeText, parseCssUrls, getHtmlNodeAttributePosition, parseSrcSet, removeHtmlNodeText, parseJsUrls, getUrlForContentInsideJs, analyzeLinkNode, findHtmlNode, removeHtmlNode, insertHtmlNodeAfter } from "@jsenv/ast";
2
2
  import { jsenvPluginBundling } from "@jsenv/plugin-bundling";
3
3
  import { jsenvPluginMinification } from "@jsenv/plugin-minification";
4
4
  import { jsenvPluginTranspilation, jsenvPluginJsModuleFallback } from "@jsenv/plugin-transpilation";
@@ -1858,6 +1858,7 @@ const createUrlInfo = (url, context) => {
1858
1858
  contentLength: undefined,
1859
1859
  contentFinalized: false,
1860
1860
  contentSideEffects: [],
1861
+ contentInjections: {},
1861
1862
 
1862
1863
  sourcemap: null,
1863
1864
  sourcemapIsWrong: false,
@@ -2122,6 +2123,176 @@ ${urlInfo.url}`,
2122
2123
  return urlInfo;
2123
2124
  };
2124
2125
 
2126
+ const injectionSymbol = Symbol.for("jsenv_injection");
2127
+ const INJECTIONS = {
2128
+ global: (value) => {
2129
+ return { [injectionSymbol]: "global", value };
2130
+ },
2131
+ optional: (value) => {
2132
+ return { [injectionSymbol]: "optional", value };
2133
+ },
2134
+ };
2135
+
2136
+ const isPlaceholderInjection = (value) => {
2137
+ return (
2138
+ !value || !value[injectionSymbol] || value[injectionSymbol] !== "global"
2139
+ );
2140
+ };
2141
+
2142
+ const applyContentInjections = (content, contentInjections, urlInfo) => {
2143
+ const keys = Object.keys(contentInjections);
2144
+ const globals = {};
2145
+ const placeholderReplacements = [];
2146
+ for (const key of keys) {
2147
+ const contentInjection = contentInjections[key];
2148
+ if (contentInjection && contentInjection[injectionSymbol]) {
2149
+ const valueBehindSymbol = contentInjection[injectionSymbol];
2150
+ if (valueBehindSymbol === "global") {
2151
+ globals[key] = contentInjection.value;
2152
+ } else if (valueBehindSymbol === "optional") {
2153
+ placeholderReplacements.push({
2154
+ key,
2155
+ isOptional: true,
2156
+ value: contentInjection.value,
2157
+ });
2158
+ } else {
2159
+ throw new Error(`unknown injection type "${valueBehindSymbol}"`);
2160
+ }
2161
+ } else {
2162
+ placeholderReplacements.push({
2163
+ key,
2164
+ value: contentInjection,
2165
+ });
2166
+ }
2167
+ }
2168
+
2169
+ const needGlobalsInjection = Object.keys(globals).length > 0;
2170
+ const needPlaceholderReplacements = placeholderReplacements.length > 0;
2171
+
2172
+ if (needGlobalsInjection && needPlaceholderReplacements) {
2173
+ const globalInjectionResult = injectGlobals(content, globals, urlInfo);
2174
+ const replaceInjectionResult = injectPlaceholderReplacements(
2175
+ globalInjectionResult.content,
2176
+ placeholderReplacements,
2177
+ urlInfo,
2178
+ );
2179
+ return {
2180
+ content: replaceInjectionResult.content,
2181
+ sourcemap: composeTwoSourcemaps(
2182
+ globalInjectionResult.sourcemap,
2183
+ replaceInjectionResult.sourcemap,
2184
+ ),
2185
+ };
2186
+ }
2187
+ if (needGlobalsInjection) {
2188
+ return injectGlobals(content, globals, urlInfo);
2189
+ }
2190
+ if (needPlaceholderReplacements) {
2191
+ return injectPlaceholderReplacements(
2192
+ content,
2193
+ placeholderReplacements,
2194
+ urlInfo,
2195
+ );
2196
+ }
2197
+ return null;
2198
+ };
2199
+
2200
+ const injectPlaceholderReplacements = (
2201
+ content,
2202
+ placeholderReplacements,
2203
+ urlInfo,
2204
+ ) => {
2205
+ const magicSource = createMagicSource(content);
2206
+ for (const { key, isOptional, value } of placeholderReplacements) {
2207
+ let index = content.indexOf(key);
2208
+ if (index === -1) {
2209
+ if (!isOptional) {
2210
+ urlInfo.context.logger.warn(
2211
+ `placeholder "${key}" not found in ${urlInfo.url}.
2212
+ --- suggestion a ---
2213
+ Add "${key}" in that file.
2214
+ --- suggestion b ---
2215
+ Fix eventual typo in "${key}"?
2216
+ --- suggestion c ---
2217
+ Mark injection as optional using INJECTIONS.optional():
2218
+ import { INJECTIONS } from "@jsenv/core";
2219
+
2220
+ return {
2221
+ "${key}": INJECTIONS.optional(${JSON.stringify(value)}),
2222
+ };`,
2223
+ );
2224
+ }
2225
+ continue;
2226
+ }
2227
+
2228
+ while (index !== -1) {
2229
+ const start = index;
2230
+ const end = index + key.length;
2231
+ magicSource.replace({
2232
+ start,
2233
+ end,
2234
+ replacement:
2235
+ urlInfo.type === "js_classic" ||
2236
+ urlInfo.type === "js_module" ||
2237
+ urlInfo.type === "html"
2238
+ ? JSON.stringify(value, null, " ")
2239
+ : value,
2240
+ });
2241
+ index = content.indexOf(key, end);
2242
+ }
2243
+ }
2244
+ return magicSource.toContentAndSourcemap();
2245
+ };
2246
+
2247
+ const injectGlobals = (content, globals, urlInfo) => {
2248
+ if (urlInfo.type === "html") {
2249
+ return globalInjectorOnHtml(content, globals, urlInfo);
2250
+ }
2251
+ if (urlInfo.type === "js_classic" || urlInfo.type === "js_module") {
2252
+ return globalsInjectorOnJs(content, globals, urlInfo);
2253
+ }
2254
+ throw new Error(`cannot inject globals into "${urlInfo.type}"`);
2255
+ };
2256
+ const globalInjectorOnHtml = (content, globals, urlInfo) => {
2257
+ // ideally we would inject an importmap but browser support is too low
2258
+ // (even worse for worker/service worker)
2259
+ // so for now we inject code into entry points
2260
+ const htmlAst = parseHtml({
2261
+ html: content,
2262
+ url: urlInfo.url,
2263
+ storeOriginalPositions: false,
2264
+ });
2265
+ const clientCode = generateClientCodeForGlobals(globals, {
2266
+ isWebWorker: false,
2267
+ });
2268
+ injectJsenvScript(htmlAst, {
2269
+ content: clientCode,
2270
+ pluginName: "jsenv:inject_globals",
2271
+ });
2272
+ return {
2273
+ content: stringifyHtmlAst(htmlAst),
2274
+ };
2275
+ };
2276
+ const globalsInjectorOnJs = (content, globals, urlInfo) => {
2277
+ const clientCode = generateClientCodeForGlobals(globals, {
2278
+ isWebWorker:
2279
+ urlInfo.subtype === "worker" ||
2280
+ urlInfo.subtype === "service_worker" ||
2281
+ urlInfo.subtype === "shared_worker",
2282
+ });
2283
+ const magicSource = createMagicSource(content);
2284
+ magicSource.prepend(clientCode);
2285
+ return magicSource.toContentAndSourcemap();
2286
+ };
2287
+ const generateClientCodeForGlobals = (globals, { isWebWorker = false }) => {
2288
+ const globalName = isWebWorker ? "self" : "window";
2289
+ return `Object.assign(${globalName}, ${JSON.stringify(
2290
+ globals,
2291
+ null,
2292
+ " ",
2293
+ )});`;
2294
+ };
2295
+
2125
2296
  const defineGettersOnPropertiesDerivedFromOriginalContent = (
2126
2297
  urlInfo,
2127
2298
  ) => {
@@ -2402,6 +2573,7 @@ const createUrlInfoTransformer = ({
2402
2573
  contentLength,
2403
2574
  sourcemap,
2404
2575
  sourcemapIsWrong,
2576
+ contentInjections,
2405
2577
  } = transformations;
2406
2578
  if (type) {
2407
2579
  urlInfo.type = type;
@@ -2409,13 +2581,23 @@ const createUrlInfoTransformer = ({
2409
2581
  if (contentType) {
2410
2582
  urlInfo.contentType = contentType;
2411
2583
  }
2412
- const contentModified = setContentProperties(urlInfo, {
2413
- content,
2414
- contentAst,
2415
- contentEtag,
2416
- contentLength,
2417
- });
2418
-
2584
+ if (Object.hasOwn(transformations, "contentInjections")) {
2585
+ if (contentInjections) {
2586
+ Object.assign(urlInfo.contentInjections, contentInjections);
2587
+ }
2588
+ if (content === undefined) {
2589
+ return;
2590
+ }
2591
+ }
2592
+ let contentModified;
2593
+ if (Object.hasOwn(transformations, "content")) {
2594
+ contentModified = setContentProperties(urlInfo, {
2595
+ content,
2596
+ contentAst,
2597
+ contentEtag,
2598
+ contentLength,
2599
+ });
2600
+ }
2419
2601
  if (
2420
2602
  sourcemap &&
2421
2603
  mayHaveSourcemap(urlInfo) &&
@@ -2607,6 +2789,15 @@ const createUrlInfoTransformer = ({
2607
2789
  if (transformations) {
2608
2790
  applyTransformations(urlInfo, transformations);
2609
2791
  }
2792
+ const { contentInjections } = urlInfo;
2793
+ if (contentInjections && Object.keys(contentInjections).length > 0) {
2794
+ const injectionTransformations = applyContentInjections(
2795
+ urlInfo.content,
2796
+ contentInjections,
2797
+ urlInfo,
2798
+ );
2799
+ applyTransformations(urlInfo, injectionTransformations);
2800
+ }
2610
2801
  applyContentEffects(urlInfo);
2611
2802
  urlInfo.contentFinalized = true;
2612
2803
  };
@@ -2752,6 +2943,7 @@ const createKitchen = ({
2752
2943
  inlineContentClientFileUrl,
2753
2944
  isSupportedOnCurrentClients: memoizeIsSupported(clientRuntimeCompat),
2754
2945
  isSupportedOnFutureClients: memoizeIsSupported(runtimeCompat),
2946
+ isPlaceholderInjection,
2755
2947
  getPluginMeta: null,
2756
2948
  sourcemaps,
2757
2949
  outDirectoryUrl,
@@ -3927,10 +4119,10 @@ const generateHtmlForSyntaxError = (
3927
4119
  errorLinkText: `${htmlRelativeUrl}:${line}:${column}`,
3928
4120
  syntaxErrorHTML: errorToHTML(htmlErrorContentFrame),
3929
4121
  };
3930
- const html = replacePlaceholders$1(htmlForSyntaxError, replacers);
4122
+ const html = replacePlaceholders(htmlForSyntaxError, replacers);
3931
4123
  return html;
3932
4124
  };
3933
- const replacePlaceholders$1 = (html, replacers) => {
4125
+ const replacePlaceholders = (html, replacers) => {
3934
4126
  return html.replace(/\$\{(\w+)\}/g, (match, name) => {
3935
4127
  const replacer = replacers[name];
3936
4128
  if (replacer === undefined) {
@@ -4365,6 +4557,9 @@ const returnValueAssertions = [
4365
4557
  return undefined;
4366
4558
  }
4367
4559
  if (typeof content !== "string" && !Buffer.isBuffer(content) && !body) {
4560
+ if (Object.hasOwn(valueReturned, "contentInjections")) {
4561
+ return undefined;
4562
+ }
4368
4563
  throw new Error(
4369
4564
  `Unexpected "content" returned by "${hook.plugin.name}" ${hook.name} hook: it must be a string or a buffer; got ${content}`,
4370
4565
  );
@@ -6172,103 +6367,6 @@ const FILE_AND_SERVER_URLS_CONVERTER = {
6172
6367
  },
6173
6368
  };
6174
6369
 
6175
- const jsenvPluginInjections = (rawAssociations) => {
6176
- let resolvedAssociations;
6177
-
6178
- return {
6179
- name: "jsenv:injections",
6180
- appliesDuring: "*",
6181
- init: (context) => {
6182
- resolvedAssociations = URL_META.resolveAssociations(
6183
- { injectionsGetter: rawAssociations },
6184
- context.rootDirectoryUrl,
6185
- );
6186
- },
6187
- transformUrlContent: async (urlInfo) => {
6188
- const { injectionsGetter } = URL_META.applyAssociations({
6189
- url: asUrlWithoutSearch(urlInfo.url),
6190
- associations: resolvedAssociations,
6191
- });
6192
- if (!injectionsGetter) {
6193
- return null;
6194
- }
6195
- if (typeof injectionsGetter !== "function") {
6196
- throw new TypeError("injectionsGetter must be a function");
6197
- }
6198
- const injections = await injectionsGetter(urlInfo);
6199
- if (!injections) {
6200
- return null;
6201
- }
6202
- const keys = Object.keys(injections);
6203
- if (keys.length === 0) {
6204
- return null;
6205
- }
6206
- return replacePlaceholders(urlInfo.content, injections, urlInfo);
6207
- },
6208
- };
6209
- };
6210
-
6211
- const injectionSymbol = Symbol.for("jsenv_injection");
6212
- const INJECTIONS = {
6213
- optional: (value) => {
6214
- return { [injectionSymbol]: "optional", value };
6215
- },
6216
- };
6217
-
6218
- // we export this because it is imported by jsenv_plugin_placeholder.js and unit test
6219
- const replacePlaceholders = (content, replacements, urlInfo) => {
6220
- const magicSource = createMagicSource(content);
6221
- for (const key of Object.keys(replacements)) {
6222
- let index = content.indexOf(key);
6223
- const replacement = replacements[key];
6224
- let isOptional;
6225
- let value;
6226
- if (replacement && replacement[injectionSymbol]) {
6227
- const valueBehindSymbol = replacement[injectionSymbol];
6228
- isOptional = valueBehindSymbol === "optional";
6229
- value = replacement.value;
6230
- } else {
6231
- value = replacement;
6232
- }
6233
- if (index === -1) {
6234
- if (!isOptional) {
6235
- urlInfo.context.logger.warn(
6236
- `placeholder "${key}" not found in ${urlInfo.url}.
6237
- --- suggestion a ---
6238
- Add "${key}" in that file.
6239
- --- suggestion b ---
6240
- Fix eventual typo in "${key}"?
6241
- --- suggestion c ---
6242
- Mark injection as optional using INJECTIONS.optional():
6243
- import { INJECTIONS } from "@jsenv/core";
6244
-
6245
- return {
6246
- "${key}": INJECTIONS.optional(${JSON.stringify(value)}),
6247
- };`,
6248
- );
6249
- }
6250
- continue;
6251
- }
6252
-
6253
- while (index !== -1) {
6254
- const start = index;
6255
- const end = index + key.length;
6256
- magicSource.replace({
6257
- start,
6258
- end,
6259
- replacement:
6260
- urlInfo.type === "js_classic" ||
6261
- urlInfo.type === "js_module" ||
6262
- urlInfo.type === "html"
6263
- ? JSON.stringify(value, null, " ")
6264
- : value,
6265
- });
6266
- index = content.indexOf(key, end);
6267
- }
6268
- }
6269
- return magicSource.toContentAndSourcemap();
6270
- };
6271
-
6272
6370
  /*
6273
6371
  * NICE TO HAVE:
6274
6372
  *
@@ -6378,22 +6476,22 @@ const jsenvPluginDirectoryListing = ({
6378
6476
  }
6379
6477
  const request = urlInfo.context.request;
6380
6478
  const { rootDirectoryUrl, mainFilePath } = urlInfo.context;
6381
- return replacePlaceholders(
6382
- urlInfo.content,
6479
+ const directoryListingInjections = generateDirectoryListingInjection(
6480
+ requestedUrl,
6383
6481
  {
6384
- ...generateDirectoryListingInjection(requestedUrl, {
6385
- autoreload,
6386
- request,
6387
- urlMocks,
6388
- directoryContentMagicName,
6389
- rootDirectoryUrl,
6390
- mainFilePath,
6391
- packageDirectory,
6392
- enoent,
6393
- }),
6482
+ autoreload,
6483
+ request,
6484
+ urlMocks,
6485
+ directoryContentMagicName,
6486
+ rootDirectoryUrl,
6487
+ mainFilePath,
6488
+ packageDirectory,
6489
+ enoent,
6394
6490
  },
6395
- urlInfo,
6396
6491
  );
6492
+ return {
6493
+ contentInjections: directoryListingInjections,
6494
+ };
6397
6495
  },
6398
6496
  },
6399
6497
  devServerRoutes: [
@@ -6412,8 +6510,10 @@ const jsenvPluginDirectoryListing = ({
6412
6510
  directoryRelativeUrl,
6413
6511
  rootDirectoryUrl,
6414
6512
  );
6415
- const closestDirectoryUrl =
6416
- getFirstExistingDirectoryUrl(requestedUrl);
6513
+ const closestDirectoryUrl = getFirstExistingDirectoryUrl(
6514
+ requestedUrl,
6515
+ rootDirectoryUrl,
6516
+ );
6417
6517
  const sendMessage = (message) => {
6418
6518
  websocket.send(JSON.stringify(message));
6419
6519
  };
@@ -6627,15 +6727,15 @@ const generateDirectoryListingInjection = (
6627
6727
  };
6628
6728
  };
6629
6729
  const getFirstExistingDirectoryUrl = (requestedUrl, serverRootDirectoryUrl) => {
6630
- let firstExistingDirectoryUrl = new URL("./", requestedUrl);
6631
- while (!existsSync(firstExistingDirectoryUrl)) {
6632
- firstExistingDirectoryUrl = new URL("../", firstExistingDirectoryUrl);
6633
- if (!urlIsInsideOf(firstExistingDirectoryUrl, serverRootDirectoryUrl)) {
6634
- firstExistingDirectoryUrl = new URL(serverRootDirectoryUrl);
6730
+ let directoryUrlCandidate = new URL("./", requestedUrl);
6731
+ while (!existsSync(directoryUrlCandidate)) {
6732
+ directoryUrlCandidate = new URL("../", directoryUrlCandidate);
6733
+ if (!urlIsInsideOf(directoryUrlCandidate, serverRootDirectoryUrl)) {
6734
+ directoryUrlCandidate = new URL(serverRootDirectoryUrl);
6635
6735
  break;
6636
6736
  }
6637
6737
  }
6638
- return firstExistingDirectoryUrl;
6738
+ return directoryUrlCandidate;
6639
6739
  };
6640
6740
  const getDirectoryContentItems = ({
6641
6741
  serverRootDirectoryUrl,
@@ -6679,6 +6779,7 @@ const getDirectoryContentItems = ({
6679
6779
  };
6680
6780
 
6681
6781
  const jsenvPluginFsRedirection = ({
6782
+ spa = true,
6682
6783
  directoryContentMagicName,
6683
6784
  magicExtensions = ["inherit", ".js"],
6684
6785
  magicDirectoryIndex = true,
@@ -6768,11 +6869,19 @@ const jsenvPluginFsRedirection = ({
6768
6869
  // 3. The url pathname does not ends with "/"
6769
6870
  // In that case we assume client explicitely asks to load a directory
6770
6871
  if (
6872
+ spa &&
6771
6873
  !urlToExtension(urlObject) &&
6772
6874
  !urlToPathname(urlObject).endsWith("/")
6773
6875
  ) {
6774
- const { mainFilePath, rootDirectoryUrl } =
6876
+ const { requestedUrl, rootDirectoryUrl, mainFilePath } =
6775
6877
  reference.ownerUrlInfo.context;
6878
+ const closestHtmlRootFile = getClosestHtmlRootFile(
6879
+ requestedUrl,
6880
+ rootDirectoryUrl,
6881
+ );
6882
+ if (closestHtmlRootFile) {
6883
+ return closestHtmlRootFile;
6884
+ }
6776
6885
  return new URL(mainFilePath, rootDirectoryUrl);
6777
6886
  }
6778
6887
  return null;
@@ -6822,9 +6931,34 @@ const resolveSymlink = (fileUrl) => {
6822
6931
  return realUrlObject.href;
6823
6932
  };
6824
6933
 
6934
+ const getClosestHtmlRootFile = (requestedUrl, serverRootDirectoryUrl) => {
6935
+ let directoryUrl = new URL("./", requestedUrl);
6936
+ while (true) {
6937
+ const indexHtmlFileUrl = new URL(`index.html`, directoryUrl);
6938
+ if (existsSync(indexHtmlFileUrl)) {
6939
+ return indexHtmlFileUrl.href;
6940
+ }
6941
+ const htmlFileUrlCandidate = new URL(
6942
+ `${urlToFilename(directoryUrl)}.html`,
6943
+ directoryUrl,
6944
+ );
6945
+ if (existsSync(htmlFileUrlCandidate)) {
6946
+ return htmlFileUrlCandidate.href;
6947
+ }
6948
+ if (
6949
+ !urlIsInsideOf(directoryUrl, serverRootDirectoryUrl) ||
6950
+ directoryUrl.href === serverRootDirectoryUrl
6951
+ ) {
6952
+ return null;
6953
+ }
6954
+ directoryUrl = new URL("../", directoryUrl);
6955
+ }
6956
+ };
6957
+
6825
6958
  const directoryContentMagicName = "...";
6826
6959
 
6827
6960
  const jsenvPluginProtocolFile = ({
6961
+ spa,
6828
6962
  magicExtensions,
6829
6963
  magicDirectoryIndex,
6830
6964
  preserveSymlinks,
@@ -6836,6 +6970,7 @@ const jsenvPluginProtocolFile = ({
6836
6970
  }) => {
6837
6971
  return [
6838
6972
  jsenvPluginFsRedirection({
6973
+ spa,
6839
6974
  directoryContentMagicName,
6840
6975
  magicExtensions,
6841
6976
  magicDirectoryIndex,
@@ -7085,6 +7220,69 @@ const asValidFilename = (string) => {
7085
7220
  return string;
7086
7221
  };
7087
7222
 
7223
+ const jsenvPluginInjections = (rawAssociations) => {
7224
+ const getDefaultInjections = (urlInfo) => {
7225
+ if (urlInfo.context.dev && urlInfo.type === "html") {
7226
+ const relativeUrl = urlToRelativeUrl(
7227
+ urlInfo.url,
7228
+ urlInfo.context.rootDirectoryUrl,
7229
+ );
7230
+ return {
7231
+ HTML_ROOT_PATHNAME: INJECTIONS.global(`/${relativeUrl}`),
7232
+ };
7233
+ }
7234
+ return null;
7235
+ };
7236
+ let getInjections = null;
7237
+
7238
+ return {
7239
+ name: "jsenv:injections",
7240
+ appliesDuring: "*",
7241
+ init: (context) => {
7242
+ if (rawAssociations && Object.keys(rawAssociations).length > 0) {
7243
+ const resolvedAssociations = URL_META.resolveAssociations(
7244
+ { injectionsGetter: rawAssociations },
7245
+ context.rootDirectoryUrl,
7246
+ );
7247
+ getInjections = (urlInfo) => {
7248
+ const { injectionsGetter } = URL_META.applyAssociations({
7249
+ url: asUrlWithoutSearch(urlInfo.url),
7250
+ associations: resolvedAssociations,
7251
+ });
7252
+ if (!injectionsGetter) {
7253
+ return null;
7254
+ }
7255
+ if (typeof injectionsGetter !== "function") {
7256
+ throw new TypeError("injectionsGetter must be a function");
7257
+ }
7258
+ return injectionsGetter(urlInfo);
7259
+ };
7260
+ }
7261
+ },
7262
+ transformUrlContent: async (urlInfo) => {
7263
+ const defaultInjections = getDefaultInjections(urlInfo);
7264
+ if (!getInjections) {
7265
+ return {
7266
+ contentInjections: defaultInjections,
7267
+ };
7268
+ }
7269
+ const injectionsResult = getInjections(urlInfo);
7270
+ if (!injectionsResult) {
7271
+ return {
7272
+ contentInjections: defaultInjections,
7273
+ };
7274
+ }
7275
+ const injections = await injectionsResult;
7276
+ return {
7277
+ contentInjections: {
7278
+ ...defaultInjections,
7279
+ ...injections,
7280
+ },
7281
+ };
7282
+ },
7283
+ };
7284
+ };
7285
+
7088
7286
  /*
7089
7287
  * Some code uses globals specific to Node.js in code meant to run in browsers...
7090
7288
  * This plugin will replace some node globals to things compatible with web:
@@ -7277,6 +7475,8 @@ const babelPluginMetadataExpressionPaths = (
7277
7475
  * - replaced by true: When scenario matches (import.meta.dev and it's the dev server)
7278
7476
  * - left as is to be evaluated to undefined (import.meta.build but it's the dev server)
7279
7477
  * - replaced by undefined (import.meta.dev but it's build; the goal is to ensure it's tree-shaked)
7478
+ *
7479
+ * TODO: ideally during dev we would keep import.meta.dev and ensure we set it to true rather than replacing it with true?
7280
7480
  */
7281
7481
 
7282
7482
 
@@ -7382,14 +7582,12 @@ const babelPluginMetadataImportMetaScenarios = () => {
7382
7582
 
7383
7583
  const jsenvPluginGlobalScenarios = () => {
7384
7584
  const transformIfNeeded = (urlInfo) => {
7385
- return replacePlaceholders(
7386
- urlInfo.content,
7387
- {
7585
+ return {
7586
+ contentInjections: {
7388
7587
  __DEV__: INJECTIONS.optional(urlInfo.context.dev),
7389
7588
  __BUILD__: INJECTIONS.optional(urlInfo.context.build),
7390
7589
  },
7391
- urlInfo,
7392
- );
7590
+ };
7393
7591
  };
7394
7592
 
7395
7593
  return {
@@ -8632,6 +8830,7 @@ const getCorePlugins = ({
8632
8830
  transpilation = true,
8633
8831
  inlining = true,
8634
8832
  http = false,
8833
+ spa,
8635
8834
 
8636
8835
  clientAutoreload,
8637
8836
  clientAutoreloadOnServerRestart,
@@ -8661,7 +8860,7 @@ const getCorePlugins = ({
8661
8860
 
8662
8861
  return [
8663
8862
  jsenvPluginReferenceAnalysis(referenceAnalysis),
8664
- ...(injections ? [jsenvPluginInjections(injections)] : []),
8863
+ jsenvPluginInjections(injections),
8665
8864
  jsenvPluginTranspilation(transpilation),
8666
8865
  // "jsenvPluginInlining" must be very soon because all other plugins will react differently once they see the file is inlined
8667
8866
  ...(inlining ? [jsenvPluginInlining()] : []),
@@ -8674,6 +8873,7 @@ const getCorePlugins = ({
8674
8873
  */
8675
8874
  jsenvPluginProtocolHttp(http),
8676
8875
  jsenvPluginProtocolFile({
8876
+ spa,
8677
8877
  magicExtensions,
8678
8878
  magicDirectoryIndex,
8679
8879
  directoryListing,