Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 111 additions & 14 deletions dist/build/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -2364,11 +2364,21 @@ ${urlInfo.url}`,

const injectionSymbol = Symbol.for("jsenv_injection");
const INJECTIONS = {
/**
* Inject `Object.assign(window, { [key]: value })` at the top of the file
* (into a script for html, into the module itself for js) instead of
* replacing a placeholder: the value is read at runtime as a global.
*/
global: (value) => {
return { [injectionSymbol]: "global", value };
},
/**
* Replace the placeholder when the file contains it, stay silent when it does not
* (without this a missing placeholder is reported as a warning).
*/
optional: (value) => {
if (value && value[injectionSymbol] === "optional") {
if (value && value[injectionSymbol]) {
// a global injection is not a placeholder, it can't be missing from the file
return value;
}
return { [injectionSymbol]: "optional", value };
Expand Down Expand Up @@ -2473,19 +2483,27 @@ return {
magicSource.replace({
start,
end,
replacement:
urlInfo.type === "js_classic" ||
urlInfo.type === "js_module" ||
urlInfo.type === "html"
? JSON.stringify(value, null, " ")
: value,
replacement: asReplacement(value, urlInfo),
});
index = content.indexOf(key, end);
}
}
return magicSource.toContentAndSourcemap();
};

// In JS the placeholder stands for a value, so it must be substituted by a literal.
// Everywhere else (html attributes and text, css, ...) it stands for a piece of text
// and is substituted as-is, so it can be concatenated: href="__BACKEND_URL__/users/me"
const asReplacement = (value, urlInfo) => {
if (urlInfo.type === "js_classic" || urlInfo.type === "js_module") {
return JSON.stringify(value, null, " ");
}
if (typeof value === "string") {
return value;
}
return JSON.stringify(value, null, " ");
};

const injectGlobals = (content, globals, urlInfo) => {
if (urlInfo.type === "html") {
return globalInjectorOnHtml(content, globals, urlInfo);
Expand Down Expand Up @@ -6560,7 +6578,9 @@ const jsenvPluginVersionSearchParam = () => {
* of them, the page switcher (cmd+K) opens one in the current tab. Whoever asks
* gets the same answer.
*
* Each page comes with what kind of page it is, read from where it sits and
* Each page comes with where its file is (so it can be opened in an editor as
* well as in the browser) and with what kind of page it is, read from where it
* sits and
* what it is called — the two conventions this repo already follows:
* - "experiment": something tried out, under a lab/ directory or named
* *_experiment.html;
Expand Down Expand Up @@ -6659,6 +6679,10 @@ const createHtmlPageLister = ({ rootDirectoryUrl }) => {
);
return {
url: `/${relativeUrl}`,
// Where the file actually is, so whoever wants to open it in an editor
// rather than in the browser has what GET /.internal/open_file/* asks
// for (a file url) without having to know the root directory.
fileUrl,
kind: readKind(meta),
// Relative to the root and without its trailing slash, which is how a
// tree names its own nodes.
Expand Down Expand Up @@ -7212,6 +7236,10 @@ const jsenvPluginFsRedirection = ({
}
const { requestedUrl, rootDirectoryUrl, mainFilePath } =
reference.ownerUrlInfo.context;
if (!requestedUrl) {
// the SPA fallback answers a request; during build there is none
return null;
}
const closestHtmlRootFile = getClosestHtmlRootFile(
requestedUrl,
rootDirectoryUrl,
Expand Down Expand Up @@ -7624,18 +7652,45 @@ const jsenvPluginInjections = (rawAssociations) => {
{ injectionsGetter: rawAssociations },
context.rootDirectoryUrl,
);
getInjections = (urlInfo) => {
const findInjectionsGetter = (urlInfo) => {
const { injectionsGetter } = URL_META.applyAssociations({
url: asUrlWithoutSearch(urlInfo.url),
associations: resolvedAssociations,
});
if (!injectionsGetter) {
if (injectionsGetter) {
return { injectionsGetter, isInherited: false };
}
if (urlInfo.isInline) {
// content inlined into a file (a <script> inside html) is authored in that
// file, so injections configured for the file must reach it too
const found = findInjectionsGetter(
urlInfo.firstReference.ownerUrlInfo,
);
if (found) {
return {
injectionsGetter: found.injectionsGetter,
isInherited: true,
};
}
}
return null;
};
getInjections = async (urlInfo) => {
const found = findInjectionsGetter(urlInfo);
if (!found) {
return null;
}
const { injectionsGetter, isInherited } = found;
if (typeof injectionsGetter !== "function") {
throw new TypeError("injectionsGetter must be a function");
}
return injectionsGetter(urlInfo);
const injections = await injectionsGetter(urlInfo);
if (!injections || !isInherited) {
return injections;
}
// the file holds several inline contents; a placeholder configured for the file
// is expected in one of them, not in each
return asOptionalInjections(injections);
};
}
},
Expand All @@ -7646,13 +7701,12 @@ const jsenvPluginInjections = (rawAssociations) => {
contentInjections: defaultInjections,
};
}
const injectionsResult = getInjections(urlInfo);
if (!injectionsResult) {
const injections = await getInjections(urlInfo);
if (!injections) {
return {
contentInjections: defaultInjections,
};
}
const injections = await injectionsResult;
return {
contentInjections: {
...defaultInjections,
Expand All @@ -7663,6 +7717,14 @@ const jsenvPluginInjections = (rawAssociations) => {
};
};

const asOptionalInjections = (injections) => {
const optionalInjections = {};
for (const key of Object.keys(injections)) {
optionalInjections[key] = INJECTIONS.optional(injections[key]);
}
return optionalInjections;
};

/*
* Some code uses globals specific to Node.js in code meant to run in browsers...
* This plugin will replace some node globals to things compatible with web:
Expand Down Expand Up @@ -11064,6 +11126,11 @@ const createBuildSpecifierManager = ({
registerHtmlRefine((htmlAst, { registerHtmlMutation }) => {
visitHtmlNodes(htmlAst, {
link: (node) => {
if (getHtmlNodeAttribute(node, "jsenv-ignore") !== undefined) {
// reference analysis skipped this node, so it has no urlInfo in the graph
// and there is nothing to resync
return;
}
const href = getHtmlNodeAttribute(node, "href");
if (href === undefined || href.startsWith("data:")) {
return;
Expand Down Expand Up @@ -11902,6 +11969,25 @@ const jsenvPluginMappings = (mappings) => {
* How URLs are versioned for this entry point (defaults to "search_param")
* @param {('none'|'inline'|'file'|'programmatic')} [entryPoint.sourcemaps]
* Sourcemap generation strategy for this entry point (defaults to "none")
* @param {object} [entryPoint.injections]
* Values to inject into files, as { urlPattern: getInjections }.
* Keys are url patterns relative to sourceDirectoryUrl ("./index.html", "**\/*.js"),
* values are functions receiving urlInfo and returning (or resolving to)
* an object of placeholders to replace, named `__LIKE_THIS__` by convention:
*
* injections: {
* "./index.html": () => ({ __BACKEND_URL__: "https://api.example.com" }),
* }
*
* In JS files the value is injected as a JS literal (a string value brings its own quotes),
* everywhere else it is injected as-is, so it can be concatenated:
* `href="__BACKEND_URL__/users/me"`.
* An html url pattern also covers what is inlined in that html: an inline
* `<script>window.backendUrl = __BACKEND_URL__;</script>` gets the JS literal,
* which is how a value is shared with every js file of the page.
* Use INJECTIONS.optional(value) for a placeholder that may be absent from the file
* and INJECTIONS.global(value) to inject `Object.assign(window, { ... })` instead of
* replacing a placeholder.
*
* @return {Promise<Object>} buildReturnValue
* @return {Promise<Object>} [buildReturnValue.buildInlineContents]
Expand Down Expand Up @@ -11939,6 +12025,17 @@ const build = async ({
{
const unexpectedParamNames = Object.keys(rest);
if (unexpectedParamNames.length > 0) {
const entryPointParamNames = unexpectedParamNames.filter((name) =>
Object.hasOwn(entryPointDefaultParams, name),
);
if (entryPointParamNames.length > 0) {
throw new TypeError(
`${entryPointParamNames.join(",")}: param(s) configured per entry point, move them into entryPoints, as in:
entryPoints: {
"./index.html": { ${entryPointParamNames.map((name) => `${name}: ...`).join(", ")} },
}`,
);
}
throw new TypeError(
`${unexpectedParamNames.join(",")}: there is no such param`,
);
Expand Down
Loading
Loading