@jsenv/core 38.1.0 → 38.2.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.
package/README.md CHANGED
@@ -1,32 +1,19 @@
1
1
  # @jsenv/core [![npm package](https://img.shields.io/npm/v/@jsenv/core.svg?logo=npm&label=package)](https://www.npmjs.com/package/@jsenv/core)
2
2
 
3
- Jsenv was created to provide a tool that can be used both for the web and Node.js.
4
- It has naturally evolved to cover the core needs of a JavaScript project: developement, testing and building for production.
3
+ Jsenv is a tool to develop test and build projects using JavaScript. Jsenv is simple, easy to understand and well documented.
5
4
 
6
- - :ok_hand: Seamless integration with standard HTML, CSS and JS.
7
- - :sparkles: Same developer experience on source files and test files.
8
- - :exploding_head: Can execute tests on Chrome, Firefox, Safari and Node.js.
5
+ [Documentation](<https://github.com/jsenv/core/wiki/A)-Introduction>) · [Changelog](./CHANGELOG.md)
9
6
 
10
- [Documentation](<https://github.com/jsenv/core/wiki/A)-Getting-started>) · [Changelog](./CHANGELOG.md)
7
+ See also
11
8
 
12
- # Installation
9
+ - [Documentation for contributors](./docs/contributors/README.md)
10
+ - [Documentation for maintainers](./docs/maintainers/README.md)
11
+
12
+ <!-- # Installation
13
13
 
14
14
  ```console
15
15
  npm install --save-dev @jsenv/core
16
16
  ```
17
17
 
18
18
  _@jsenv/core_ is tested on Mac, Windows, Linux with Node.js 18.
19
- Other operating systems and Node.js versions are not tested.
20
-
21
- # Name
22
-
23
- The name "jsenv" stands for JavaScript environments.<br />
24
- "jsenv" without capital letter because "JSEnv" would be too painful to type.
25
-
26
- # Logo
27
-
28
- The logo is composed by the name at the center and two circles orbiting around it.
29
- One of the circle is web browsers, the other is Node.js.
30
- It represents the two JavaScript runtimes supported by jsenv.
31
-
32
- ![jsenv logo with legend](./docs/jsenv_logo_legend.png)
19
+ Other operating systems and Node.js versions are not tested. -->
@@ -14797,214 +14797,6 @@ const createRepartitionMessage = ({ html, css, js, json, other, total }) => {
14797
14797
  - `)}`;
14798
14798
  };
14799
14799
 
14800
- const placeholderSymbol = Symbol.for("jsenv_placeholder");
14801
- const PLACEHOLDER = {
14802
- optional: (value) => {
14803
- return { [placeholderSymbol]: "optional", value };
14804
- },
14805
- };
14806
-
14807
- const replacePlaceholders = (content, replacements, urlInfo) => {
14808
- const magicSource = createMagicSource(content);
14809
- for (const key of Object.keys(replacements)) {
14810
- let index = content.indexOf(key);
14811
- const replacement = replacements[key];
14812
- let isOptional;
14813
- let value;
14814
- if (replacement && replacement[placeholderSymbol]) {
14815
- const valueBehindSymbol = replacement[placeholderSymbol];
14816
- isOptional = valueBehindSymbol === "optional";
14817
- value = replacement.value;
14818
- } else {
14819
- value = replacement;
14820
- }
14821
- if (index === -1) {
14822
- if (!isOptional) {
14823
- urlInfo.context.logger.warn(
14824
- `placeholder "${key}" not found in ${urlInfo.url}.
14825
- --- suggestion a ---
14826
- Add "${key}" in that file.
14827
- --- suggestion b ---
14828
- Fix eventual typo in "${key}"?
14829
- --- suggestion c ---
14830
- Mark injection as optional using PLACEHOLDER.optional()
14831
-
14832
- import { PLACEHOLDER } from "@jsenv/core"
14833
-
14834
- return {
14835
- "${key}": PLACEHOLDER.optional(${JSON.stringify(value)})
14836
- }`,
14837
- );
14838
- }
14839
- continue;
14840
- }
14841
-
14842
- while (index !== -1) {
14843
- const start = index;
14844
- const end = index + key.length;
14845
- magicSource.replace({
14846
- start,
14847
- end,
14848
- replacement:
14849
- urlInfo.type === "js_classic" ||
14850
- urlInfo.type === "js_module" ||
14851
- urlInfo.type === "html"
14852
- ? JSON.stringify(value, null, " ")
14853
- : value,
14854
- });
14855
- index = content.indexOf(key, end);
14856
- }
14857
- }
14858
- return magicSource.toContentAndSourcemap();
14859
- };
14860
-
14861
- const injectGlobals = (content, globals, urlInfo) => {
14862
- if (urlInfo.type === "html") {
14863
- return globalInjectorOnHtml(content, globals);
14864
- }
14865
- if (urlInfo.type === "js_classic" || urlInfo.type === "js_module") {
14866
- return globalsInjectorOnJs(content, globals, urlInfo);
14867
- }
14868
- throw new Error(`cannot inject globals into "${urlInfo.type}"`);
14869
- };
14870
-
14871
- const globalInjectorOnHtml = (content, globals) => {
14872
- // ideally we would inject an importmap but browser support is too low
14873
- // (even worse for worker/service worker)
14874
- // so for now we inject code into entry points
14875
- const htmlAst = parseHtmlString(content, {
14876
- storeOriginalPositions: false,
14877
- });
14878
- const clientCode = generateClientCodeForGlobals(globals, {
14879
- isWebWorker: false,
14880
- });
14881
- injectHtmlNodeAsEarlyAsPossible(
14882
- htmlAst,
14883
- createHtmlNode({
14884
- tagName: "script",
14885
- textContent: clientCode,
14886
- }),
14887
- "jsenv:inject_globals",
14888
- );
14889
- return stringifyHtmlAst(htmlAst);
14890
- };
14891
-
14892
- const globalsInjectorOnJs = (content, globals, urlInfo) => {
14893
- const clientCode = generateClientCodeForGlobals(globals, {
14894
- isWebWorker:
14895
- urlInfo.subtype === "worker" ||
14896
- urlInfo.subtype === "service_worker" ||
14897
- urlInfo.subtype === "shared_worker",
14898
- });
14899
- const magicSource = createMagicSource(content);
14900
- magicSource.prepend(clientCode);
14901
- return magicSource.toContentAndSourcemap();
14902
- };
14903
-
14904
- const generateClientCodeForGlobals = (globals, { isWebWorker = false }) => {
14905
- const globalName = isWebWorker ? "self" : "window";
14906
- return `Object.assign(${globalName}, ${JSON.stringify(
14907
- globals,
14908
- null,
14909
- " ",
14910
- )});`;
14911
- };
14912
-
14913
- const jsenvPluginInjections = (rawAssociations) => {
14914
- let resolvedAssociations;
14915
-
14916
- return {
14917
- name: "jsenv:injections",
14918
- appliesDuring: "*",
14919
- init: (context) => {
14920
- resolvedAssociations = URL_META.resolveAssociations(
14921
- { injectionsGetter: rawAssociations },
14922
- context.rootDirectoryUrl,
14923
- );
14924
- },
14925
- transformUrlContent: async (urlInfo) => {
14926
- const { injectionsGetter } = URL_META.applyAssociations({
14927
- url: asUrlWithoutSearch(urlInfo.url),
14928
- associations: resolvedAssociations,
14929
- });
14930
- if (!injectionsGetter) {
14931
- return null;
14932
- }
14933
- if (typeof injectionsGetter !== "function") {
14934
- throw new TypeError("injectionsGetter must be a function");
14935
- }
14936
- const injections = await injectionsGetter(urlInfo);
14937
- if (!injections) {
14938
- return null;
14939
- }
14940
- const keys = Object.keys(injections);
14941
- if (keys.length === 0) {
14942
- return null;
14943
- }
14944
- let someGlobal = false;
14945
- let someReplacement = false;
14946
- const globals = {};
14947
- const replacements = {};
14948
- for (const key of keys) {
14949
- const { type, name, value } = createInjection(injections[key], key);
14950
- if (type === "global") {
14951
- globals[name] = value;
14952
- someGlobal = true;
14953
- } else {
14954
- replacements[name] = value;
14955
- someReplacement = true;
14956
- }
14957
- }
14958
-
14959
- if (!someGlobal && !someReplacement) {
14960
- return null;
14961
- }
14962
-
14963
- let content = urlInfo.content;
14964
- let sourcemap;
14965
- if (someGlobal) {
14966
- const globalInjectionResult = injectGlobals(content, globals, urlInfo);
14967
- content = globalInjectionResult.content;
14968
- sourcemap = globalInjectionResult.sourcemap;
14969
- }
14970
- if (someReplacement) {
14971
- const replacementResult = replacePlaceholders(
14972
- content,
14973
- replacements,
14974
- urlInfo,
14975
- );
14976
- content = replacementResult.content;
14977
- sourcemap = sourcemap
14978
- ? composeTwoSourcemaps(sourcemap, replacementResult.sourcemap)
14979
- : replacementResult.sourcemap;
14980
- }
14981
- return {
14982
- content,
14983
- sourcemap,
14984
- };
14985
- },
14986
- };
14987
- };
14988
-
14989
- const wellKnowGlobalNames = ["window", "global", "globalThis", "self"];
14990
- const createInjection = (value, key) => {
14991
- for (const wellKnowGlobalName of wellKnowGlobalNames) {
14992
- const prefix = `${wellKnowGlobalName}.`;
14993
- if (key.startsWith(prefix)) {
14994
- return {
14995
- type: "global",
14996
- name: key.slice(prefix.length),
14997
- value,
14998
- };
14999
- }
15000
- }
15001
- return {
15002
- type: "replacement",
15003
- name: key,
15004
- value,
15005
- };
15006
- };
15007
-
15008
14800
  const jsenvPluginReferenceExpectedTypes = () => {
15009
14801
  const redirectJsReference = (reference) => {
15010
14802
  const urlObject = new URL(reference.url);
@@ -18353,6 +18145,104 @@ const jsenvPluginProtocolHttp = () => {
18353
18145
  };
18354
18146
  };
18355
18147
 
18148
+ const jsenvPluginInjections = (rawAssociations) => {
18149
+ let resolvedAssociations;
18150
+
18151
+ return {
18152
+ name: "jsenv:injections",
18153
+ appliesDuring: "*",
18154
+ init: (context) => {
18155
+ resolvedAssociations = URL_META.resolveAssociations(
18156
+ { injectionsGetter: rawAssociations },
18157
+ context.rootDirectoryUrl,
18158
+ );
18159
+ },
18160
+ transformUrlContent: async (urlInfo) => {
18161
+ const { injectionsGetter } = URL_META.applyAssociations({
18162
+ url: asUrlWithoutSearch(urlInfo.url),
18163
+ associations: resolvedAssociations,
18164
+ });
18165
+ if (!injectionsGetter) {
18166
+ return null;
18167
+ }
18168
+ if (typeof injectionsGetter !== "function") {
18169
+ throw new TypeError("injectionsGetter must be a function");
18170
+ }
18171
+ const injections = await injectionsGetter(urlInfo);
18172
+ if (!injections) {
18173
+ return null;
18174
+ }
18175
+ const keys = Object.keys(injections);
18176
+ if (keys.length === 0) {
18177
+ return null;
18178
+ }
18179
+ return replacePlaceholders(urlInfo.content, injections, urlInfo);
18180
+ },
18181
+ };
18182
+ };
18183
+
18184
+ const injectionSymbol = Symbol.for("jsenv_injection");
18185
+ const INJECTIONS = {
18186
+ optional: (value) => {
18187
+ return { [injectionSymbol]: "optional", value };
18188
+ },
18189
+ };
18190
+
18191
+ // we export this because it is imported by jsenv_plugin_placeholder.js and unit test
18192
+ const replacePlaceholders = (content, replacements, urlInfo) => {
18193
+ const magicSource = createMagicSource(content);
18194
+ for (const key of Object.keys(replacements)) {
18195
+ let index = content.indexOf(key);
18196
+ const replacement = replacements[key];
18197
+ let isOptional;
18198
+ let value;
18199
+ if (replacement && replacement[injectionSymbol]) {
18200
+ const valueBehindSymbol = replacement[injectionSymbol];
18201
+ isOptional = valueBehindSymbol === "optional";
18202
+ value = replacement.value;
18203
+ } else {
18204
+ value = replacement;
18205
+ }
18206
+ if (index === -1) {
18207
+ if (!isOptional) {
18208
+ urlInfo.context.logger.warn(
18209
+ `placeholder "${key}" not found in ${urlInfo.url}.
18210
+ --- suggestion a ---
18211
+ Add "${key}" in that file.
18212
+ --- suggestion b ---
18213
+ Fix eventual typo in "${key}"?
18214
+ --- suggestion c ---
18215
+ Mark injection as optional using INJECTIONS.optional()
18216
+
18217
+ import { INJECTIONS } from "@jsenv/core";
18218
+
18219
+ return {
18220
+ "${key}": INJECTIONS.optional(${JSON.stringify(value)}),
18221
+ }`,
18222
+ );
18223
+ }
18224
+ continue;
18225
+ }
18226
+
18227
+ while (index !== -1) {
18228
+ const start = index;
18229
+ const end = index + key.length;
18230
+ magicSource.replace({
18231
+ start,
18232
+ end,
18233
+ replacement:
18234
+ urlInfo.type === "js_classic" ||
18235
+ urlInfo.type === "js_module" ||
18236
+ urlInfo.type === "html"
18237
+ ? JSON.stringify(value, null, " ")
18238
+ : value,
18239
+ });
18240
+ index = content.indexOf(key, end);
18241
+ }
18242
+ }
18243
+ return magicSource.toContentAndSourcemap();
18244
+ };
18245
+
18356
18246
  const jsenvPluginInliningAsDataUrl = () => {
18357
18247
  return {
18358
18248
  name: "jsenv:inlining_as_data_url",
@@ -18885,9 +18775,9 @@ const babelPluginMetadataImportMetaScenarios = () => {
18885
18775
 
18886
18776
  /*
18887
18777
  * Source code can contain the following
18888
- * - __dev__
18889
- * - __build__
18890
- * A global will be injected with true/false when needed
18778
+ * - __DEV__
18779
+ * - __BUILD__
18780
+ * That will be replaced with true/false
18891
18781
  */
18892
18782
 
18893
18783
 
@@ -18896,8 +18786,8 @@ const jsenvPluginGlobalScenarios = () => {
18896
18786
  return replacePlaceholders(
18897
18787
  urlInfo.content,
18898
18788
  {
18899
- __DEV__: PLACEHOLDER.optional(urlInfo.context.dev),
18900
- __BUILD__: PLACEHOLDER.optional(urlInfo.context.build),
18789
+ __DEV__: INJECTIONS.optional(urlInfo.context.dev),
18790
+ __BUILD__: INJECTIONS.optional(urlInfo.context.build),
18901
18791
  },
18902
18792
  urlInfo,
18903
18793
  );
@@ -20926,7 +20816,7 @@ const createBuildSpecifierManager = ({
20926
20816
  const urlInfo = finalKitchen.graph.getUrlInfo(finalUrl);
20927
20817
  if (!urlInfo) {
20928
20818
  logger.warn(
20929
- `remove resource hint because cannot find "${href}" in the graph`,
20819
+ `${UNICODE.WARNING} remove resource hint because cannot find "${href}" in the graph`,
20930
20820
  );
20931
20821
  mutations.push(() => {
20932
20822
  removeHtmlNode(node);
@@ -20937,7 +20827,7 @@ const createBuildSpecifierManager = ({
20937
20827
  const rawUrlInfo = rawKitchen.graph.getUrlInfo(rawUrl);
20938
20828
  if (rawUrlInfo && rawUrlInfo.data.bundled) {
20939
20829
  logger.warn(
20940
- `remove resource hint on "${href}" because it was bundled`,
20830
+ `${UNICODE.WARNING} remove resource hint on "${href}" because it was bundled`,
20941
20831
  );
20942
20832
  mutations.push(() => {
20943
20833
  removeHtmlNode(node);
@@ -20945,7 +20835,7 @@ const createBuildSpecifierManager = ({
20945
20835
  return;
20946
20836
  }
20947
20837
  logger.warn(
20948
- `remove resource hint on "${href}" because it is not used anymore`,
20838
+ `${UNICODE.WARNING} remove resource hint on "${href}" because it is not used anymore`,
20949
20839
  );
20950
20840
  mutations.push(() => {
20951
20841
  removeHtmlNode(node);
@@ -22982,4 +22872,4 @@ const createBuildFilesService = ({ buildDirectoryUrl, buildMainFilePath }) => {
22982
22872
 
22983
22873
  const SECONDS_IN_30_DAYS = 60 * 60 * 24 * 30;
22984
22874
 
22985
- export { build, startBuildServer, startDevServer };
22875
+ export { INJECTIONS, build, startBuildServer, startDevServer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "38.1.0",
3
+ "version": "38.2.0",
4
4
  "description": "Tool to develop, test and build js projects",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -71,8 +71,7 @@
71
71
  "@jsenv/runtime-compat": "1.2.0",
72
72
  "@jsenv/server": "15.1.0",
73
73
  "@jsenv/sourcemap": "1.2.1",
74
- "@jsenv/plugin-injections": "1.0.0",
75
- "@jsenv/plugin-bundling": "2.5.1",
74
+ "@jsenv/plugin-bundling": "2.5.2",
76
75
  "@jsenv/plugin-minification": "1.5.0",
77
76
  "@jsenv/plugin-transpilation": "1.3.2",
78
77
  "@jsenv/plugin-supervisor": "1.3.2",
@@ -99,6 +98,6 @@
99
98
  "eslint-plugin-react": "7.33.1",
100
99
  "open": "9.1.0",
101
100
  "playwright": "1.36.2",
102
- "prettier": "3.0.0"
101
+ "prettier": "3.0.1"
103
102
  }
104
103
  }
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { createDetailedMessage } from "@jsenv/log";
2
+ import { createDetailedMessage, UNICODE } from "@jsenv/log";
3
3
  import { comparePathnames } from "@jsenv/filesystem";
4
4
  import { createMagicSource, generateSourcemapFileUrl } from "@jsenv/sourcemap";
5
5
  import {
@@ -790,7 +790,7 @@ export const createBuildSpecifierManager = ({
790
790
  const urlInfo = finalKitchen.graph.getUrlInfo(finalUrl);
791
791
  if (!urlInfo) {
792
792
  logger.warn(
793
- `remove resource hint because cannot find "${href}" in the graph`,
793
+ `${UNICODE.WARNING} remove resource hint because cannot find "${href}" in the graph`,
794
794
  );
795
795
  mutations.push(() => {
796
796
  removeHtmlNode(node);
@@ -801,7 +801,7 @@ export const createBuildSpecifierManager = ({
801
801
  const rawUrlInfo = rawKitchen.graph.getUrlInfo(rawUrl);
802
802
  if (rawUrlInfo && rawUrlInfo.data.bundled) {
803
803
  logger.warn(
804
- `remove resource hint on "${href}" because it was bundled`,
804
+ `${UNICODE.WARNING} remove resource hint on "${href}" because it was bundled`,
805
805
  );
806
806
  mutations.push(() => {
807
807
  removeHtmlNode(node);
@@ -809,7 +809,7 @@ export const createBuildSpecifierManager = ({
809
809
  return;
810
810
  }
811
811
  logger.warn(
812
- `remove resource hint on "${href}" because it is not used anymore`,
812
+ `${UNICODE.WARNING} remove resource hint on "${href}" because it is not used anymore`,
813
813
  );
814
814
  mutations.push(() => {
815
815
  removeHtmlNode(node);
package/src/main.js CHANGED
@@ -3,3 +3,6 @@ export { startDevServer } from "./dev/start_dev_server.js";
3
3
  // build
4
4
  export { build } from "./build/build.js";
5
5
  export { startBuildServer } from "./build/start_build_server.js";
6
+
7
+ // others
8
+ export { INJECTIONS } from "./plugins/injections/jsenv_plugin_injections.js";
@@ -1,19 +1,22 @@
1
1
  /*
2
2
  * Source code can contain the following
3
- * - __dev__
4
- * - __build__
5
- * A global will be injected with true/false when needed
3
+ * - __DEV__
4
+ * - __BUILD__
5
+ * That will be replaced with true/false
6
6
  */
7
7
 
8
- import { replacePlaceholders, PLACEHOLDER } from "@jsenv/plugin-injections";
8
+ import {
9
+ replacePlaceholders,
10
+ INJECTIONS,
11
+ } from "../injections/jsenv_plugin_injections.js";
9
12
 
10
13
  export const jsenvPluginGlobalScenarios = () => {
11
14
  const transformIfNeeded = (urlInfo) => {
12
15
  return replacePlaceholders(
13
16
  urlInfo.content,
14
17
  {
15
- __DEV__: PLACEHOLDER.optional(urlInfo.context.dev),
16
- __BUILD__: PLACEHOLDER.optional(urlInfo.context.build),
18
+ __DEV__: INJECTIONS.optional(urlInfo.context.dev),
19
+ __BUILD__: INJECTIONS.optional(urlInfo.context.build),
17
20
  },
18
21
  urlInfo,
19
22
  );
@@ -0,0 +1,59 @@
1
+ import { createMagicSource } from "@jsenv/sourcemap";
2
+ import {
3
+ parseHtmlString,
4
+ injectHtmlNodeAsEarlyAsPossible,
5
+ createHtmlNode,
6
+ stringifyHtmlAst,
7
+ } from "@jsenv/ast";
8
+
9
+ export const injectGlobals = (content, globals, urlInfo) => {
10
+ if (urlInfo.type === "html") {
11
+ return globalInjectorOnHtml(content, globals, urlInfo);
12
+ }
13
+ if (urlInfo.type === "js_classic" || urlInfo.type === "js_module") {
14
+ return globalsInjectorOnJs(content, globals, urlInfo);
15
+ }
16
+ throw new Error(`cannot inject globals into "${urlInfo.type}"`);
17
+ };
18
+
19
+ const globalInjectorOnHtml = (content, globals) => {
20
+ // ideally we would inject an importmap but browser support is too low
21
+ // (even worse for worker/service worker)
22
+ // so for now we inject code into entry points
23
+ const htmlAst = parseHtmlString(content, {
24
+ storeOriginalPositions: false,
25
+ });
26
+ const clientCode = generateClientCodeForGlobals(globals, {
27
+ isWebWorker: false,
28
+ });
29
+ injectHtmlNodeAsEarlyAsPossible(
30
+ htmlAst,
31
+ createHtmlNode({
32
+ tagName: "script",
33
+ textContent: clientCode,
34
+ }),
35
+ "jsenv:inject_globals",
36
+ );
37
+ return stringifyHtmlAst(htmlAst);
38
+ };
39
+
40
+ const globalsInjectorOnJs = (content, globals, urlInfo) => {
41
+ const clientCode = generateClientCodeForGlobals(globals, {
42
+ isWebWorker:
43
+ urlInfo.subtype === "worker" ||
44
+ urlInfo.subtype === "service_worker" ||
45
+ urlInfo.subtype === "shared_worker",
46
+ });
47
+ const magicSource = createMagicSource(content);
48
+ magicSource.prepend(clientCode);
49
+ return magicSource.toContentAndSourcemap();
50
+ };
51
+
52
+ const generateClientCodeForGlobals = (globals, { isWebWorker = false }) => {
53
+ const globalName = isWebWorker ? "self" : "window";
54
+ return `Object.assign(${globalName}, ${JSON.stringify(
55
+ globals,
56
+ null,
57
+ " ",
58
+ )});`;
59
+ };
@@ -0,0 +1,101 @@
1
+ import { URL_META } from "@jsenv/url-meta";
2
+ import { asUrlWithoutSearch } from "@jsenv/urls";
3
+ import { createMagicSource } from "@jsenv/sourcemap";
4
+
5
+ export const jsenvPluginInjections = (rawAssociations) => {
6
+ let resolvedAssociations;
7
+
8
+ return {
9
+ name: "jsenv:injections",
10
+ appliesDuring: "*",
11
+ init: (context) => {
12
+ resolvedAssociations = URL_META.resolveAssociations(
13
+ { injectionsGetter: rawAssociations },
14
+ context.rootDirectoryUrl,
15
+ );
16
+ },
17
+ transformUrlContent: async (urlInfo) => {
18
+ const { injectionsGetter } = URL_META.applyAssociations({
19
+ url: asUrlWithoutSearch(urlInfo.url),
20
+ associations: resolvedAssociations,
21
+ });
22
+ if (!injectionsGetter) {
23
+ return null;
24
+ }
25
+ if (typeof injectionsGetter !== "function") {
26
+ throw new TypeError("injectionsGetter must be a function");
27
+ }
28
+ const injections = await injectionsGetter(urlInfo);
29
+ if (!injections) {
30
+ return null;
31
+ }
32
+ const keys = Object.keys(injections);
33
+ if (keys.length === 0) {
34
+ return null;
35
+ }
36
+ return replacePlaceholders(urlInfo.content, injections, urlInfo);
37
+ },
38
+ };
39
+ };
40
+
41
+ const injectionSymbol = Symbol.for("jsenv_injection");
42
+ export const INJECTIONS = {
43
+ optional: (value) => {
44
+ return { [injectionSymbol]: "optional", value };
45
+ },
46
+ };
47
+
48
+ // we export this because it is imported by jsenv_plugin_placeholder.js and unit test
49
+ export const replacePlaceholders = (content, replacements, urlInfo) => {
50
+ const magicSource = createMagicSource(content);
51
+ for (const key of Object.keys(replacements)) {
52
+ let index = content.indexOf(key);
53
+ const replacement = replacements[key];
54
+ let isOptional;
55
+ let value;
56
+ if (replacement && replacement[injectionSymbol]) {
57
+ const valueBehindSymbol = replacement[injectionSymbol];
58
+ isOptional = valueBehindSymbol === "optional";
59
+ value = replacement.value;
60
+ } else {
61
+ value = replacement;
62
+ }
63
+ if (index === -1) {
64
+ if (!isOptional) {
65
+ urlInfo.context.logger.warn(
66
+ `placeholder "${key}" not found in ${urlInfo.url}.
67
+ --- suggestion a ---
68
+ Add "${key}" in that file.
69
+ --- suggestion b ---
70
+ Fix eventual typo in "${key}"?
71
+ --- suggestion c ---
72
+ Mark injection as optional using INJECTIONS.optional()
73
+
74
+ import { INJECTIONS } from "@jsenv/core";
75
+
76
+ return {
77
+ "${key}": INJECTIONS.optional(${JSON.stringify(value)}),
78
+ }`,
79
+ );
80
+ }
81
+ continue;
82
+ }
83
+
84
+ while (index !== -1) {
85
+ const start = index;
86
+ const end = index + key.length;
87
+ magicSource.replace({
88
+ start,
89
+ end,
90
+ replacement:
91
+ urlInfo.type === "js_classic" ||
92
+ urlInfo.type === "js_module" ||
93
+ urlInfo.type === "html"
94
+ ? JSON.stringify(value, null, " ")
95
+ : value,
96
+ });
97
+ index = content.indexOf(key, end);
98
+ }
99
+ }
100
+ return magicSource.toContentAndSourcemap();
101
+ };
@@ -1,5 +1,4 @@
1
1
  import { jsenvPluginSupervisor } from "@jsenv/plugin-supervisor";
2
- import { jsenvPluginInjections } from "@jsenv/plugin-injections";
3
2
  import { jsenvPluginTranspilation } from "@jsenv/plugin-transpilation";
4
3
 
5
4
  import { jsenvPluginReferenceAnalysis } from "./reference_analysis/jsenv_plugin_reference_analysis.js";
@@ -9,6 +8,7 @@ import { jsenvPluginWebResolution } from "./resolution_web/jsenv_plugin_web_reso
9
8
  import { jsenvPluginVersionSearchParam } from "./version_search_param/jsenv_plugin_version_search_param.js";
10
9
  import { jsenvPluginProtocolFile } from "./protocol_file/jsenv_plugin_protocol_file.js";
11
10
  import { jsenvPluginProtocolHttp } from "./protocol_http/jsenv_plugin_protocol_http.js";
11
+ import { jsenvPluginInjections } from "./injections/jsenv_plugin_injections.js";
12
12
  import { jsenvPluginInlining } from "./inlining/jsenv_plugin_inlining.js";
13
13
  import { jsenvPluginCommonJsGlobals } from "./commonjs_globals/jsenv_plugin_commonjs_globals.js";
14
14
  import { jsenvPluginImportMetaScenarios } from "./import_meta_scenarios/jsenv_plugin_import_meta_scenarios.js";