@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 +8 -21
- package/dist/jsenv_core.js +107 -217
- package/package.json +3 -4
- package/src/build/build_specifier_manager.js +4 -4
- package/src/main.js +3 -0
- package/src/plugins/global_scenarios/jsenv_plugin_global_scenarios.js +9 -6
- package/src/plugins/injections/internal/inject_globals.js +59 -0
- package/src/plugins/injections/jsenv_plugin_injections.js +101 -0
- package/src/plugins/plugins.js +1 -1
package/README.md
CHANGED
|
@@ -1,32 +1,19 @@
|
|
|
1
1
|
# @jsenv/core [](https://www.npmjs.com/package/@jsenv/core)
|
|
2
2
|
|
|
3
|
-
Jsenv
|
|
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
|
-
-
|
|
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
|
-
|
|
7
|
+
See also
|
|
11
8
|
|
|
12
|
-
|
|
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
|
-

|
|
19
|
+
Other operating systems and Node.js versions are not tested. -->
|
package/dist/jsenv_core.js
CHANGED
|
@@ -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
|
-
* -
|
|
18889
|
-
* -
|
|
18890
|
-
*
|
|
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__:
|
|
18900
|
-
__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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
* -
|
|
4
|
-
* -
|
|
5
|
-
*
|
|
3
|
+
* - __DEV__
|
|
4
|
+
* - __BUILD__
|
|
5
|
+
* That will be replaced with true/false
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
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__:
|
|
16
|
-
__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
|
+
};
|
package/src/plugins/plugins.js
CHANGED
|
@@ -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";
|