@@ -2364,11 +2364,21 @@ ${urlInfo.url}`,
23642364
23652365const injectionSymbol = Symbol . for ( "jsenv_injection" ) ;
23662366const INJECTIONS = {
2367+ /**
2368+ * Inject `Object.assign(window, { [key]: value })` at the top of the file
2369+ * (into a script for html, into the module itself for js) instead of
2370+ * replacing a placeholder: the value is read at runtime as a global.
2371+ */
23672372 global : ( value ) => {
23682373 return { [ injectionSymbol ] : "global" , value } ;
23692374 } ,
2375+ /**
2376+ * Replace the placeholder when the file contains it, stay silent when it does not
2377+ * (without this a missing placeholder is reported as a warning).
2378+ */
23702379 optional : ( value ) => {
2371- if ( value && value [ injectionSymbol ] === "optional" ) {
2380+ if ( value && value [ injectionSymbol ] ) {
2381+ // a global injection is not a placeholder, it can't be missing from the file
23722382 return value ;
23732383 }
23742384 return { [ injectionSymbol ] : "optional" , value } ;
@@ -2473,19 +2483,27 @@ return {
24732483 magicSource . replace ( {
24742484 start,
24752485 end,
2476- replacement :
2477- urlInfo . type === "js_classic" ||
2478- urlInfo . type === "js_module" ||
2479- urlInfo . type === "html"
2480- ? JSON . stringify ( value , null , " " )
2481- : value ,
2486+ replacement : asReplacement ( value , urlInfo ) ,
24822487 } ) ;
24832488 index = content . indexOf ( key , end ) ;
24842489 }
24852490 }
24862491 return magicSource . toContentAndSourcemap ( ) ;
24872492} ;
24882493
2494+ // In JS the placeholder stands for a value, so it must be substituted by a literal.
2495+ // Everywhere else (html attributes and text, css, ...) it stands for a piece of text
2496+ // and is substituted as-is, so it can be concatenated: href="__BACKEND_URL__/users/me"
2497+ const asReplacement = ( value , urlInfo ) => {
2498+ if ( urlInfo . type === "js_classic" || urlInfo . type === "js_module" ) {
2499+ return JSON . stringify ( value , null , " " ) ;
2500+ }
2501+ if ( typeof value === "string" ) {
2502+ return value ;
2503+ }
2504+ return JSON . stringify ( value , null , " " ) ;
2505+ } ;
2506+
24892507const injectGlobals = ( content , globals , urlInfo ) => {
24902508 if ( urlInfo . type === "html" ) {
24912509 return globalInjectorOnHtml ( content , globals , urlInfo ) ;
@@ -6560,7 +6578,9 @@ const jsenvPluginVersionSearchParam = () => {
65606578 * of them, the page switcher (cmd+K) opens one in the current tab. Whoever asks
65616579 * gets the same answer.
65626580 *
6563- * Each page comes with what kind of page it is, read from where it sits and
6581+ * Each page comes with where its file is (so it can be opened in an editor as
6582+ * well as in the browser) and with what kind of page it is, read from where it
6583+ * sits and
65646584 * what it is called — the two conventions this repo already follows:
65656585 * - "experiment": something tried out, under a lab/ directory or named
65666586 * *_experiment.html;
@@ -6659,6 +6679,10 @@ const createHtmlPageLister = ({ rootDirectoryUrl }) => {
66596679 ) ;
66606680 return {
66616681 url : `/${ relativeUrl } ` ,
6682+ // Where the file actually is, so whoever wants to open it in an editor
6683+ // rather than in the browser has what GET /.internal/open_file/* asks
6684+ // for (a file url) without having to know the root directory.
6685+ fileUrl,
66626686 kind : readKind ( meta ) ,
66636687 // Relative to the root and without its trailing slash, which is how a
66646688 // tree names its own nodes.
@@ -7212,6 +7236,10 @@ const jsenvPluginFsRedirection = ({
72127236 }
72137237 const { requestedUrl, rootDirectoryUrl, mainFilePath } =
72147238 reference . ownerUrlInfo . context ;
7239+ if ( ! requestedUrl ) {
7240+ // the SPA fallback answers a request; during build there is none
7241+ return null ;
7242+ }
72157243 const closestHtmlRootFile = getClosestHtmlRootFile (
72167244 requestedUrl ,
72177245 rootDirectoryUrl ,
@@ -7624,18 +7652,45 @@ const jsenvPluginInjections = (rawAssociations) => {
76247652 { injectionsGetter : rawAssociations } ,
76257653 context . rootDirectoryUrl ,
76267654 ) ;
7627- getInjections = ( urlInfo ) => {
7655+ const findInjectionsGetter = ( urlInfo ) => {
76287656 const { injectionsGetter } = URL_META . applyAssociations ( {
76297657 url : asUrlWithoutSearch ( urlInfo . url ) ,
76307658 associations : resolvedAssociations ,
76317659 } ) ;
7632- if ( ! injectionsGetter ) {
7660+ if ( injectionsGetter ) {
7661+ return { injectionsGetter, isInherited : false } ;
7662+ }
7663+ if ( urlInfo . isInline ) {
7664+ // content inlined into a file (a <script> inside html) is authored in that
7665+ // file, so injections configured for the file must reach it too
7666+ const found = findInjectionsGetter (
7667+ urlInfo . firstReference . ownerUrlInfo ,
7668+ ) ;
7669+ if ( found ) {
7670+ return {
7671+ injectionsGetter : found . injectionsGetter ,
7672+ isInherited : true ,
7673+ } ;
7674+ }
7675+ }
7676+ return null ;
7677+ } ;
7678+ getInjections = async ( urlInfo ) => {
7679+ const found = findInjectionsGetter ( urlInfo ) ;
7680+ if ( ! found ) {
76337681 return null ;
76347682 }
7683+ const { injectionsGetter, isInherited } = found ;
76357684 if ( typeof injectionsGetter !== "function" ) {
76367685 throw new TypeError ( "injectionsGetter must be a function" ) ;
76377686 }
7638- return injectionsGetter ( urlInfo ) ;
7687+ const injections = await injectionsGetter ( urlInfo ) ;
7688+ if ( ! injections || ! isInherited ) {
7689+ return injections ;
7690+ }
7691+ // the file holds several inline contents; a placeholder configured for the file
7692+ // is expected in one of them, not in each
7693+ return asOptionalInjections ( injections ) ;
76397694 } ;
76407695 }
76417696 } ,
@@ -7646,13 +7701,12 @@ const jsenvPluginInjections = (rawAssociations) => {
76467701 contentInjections : defaultInjections ,
76477702 } ;
76487703 }
7649- const injectionsResult = getInjections ( urlInfo ) ;
7650- if ( ! injectionsResult ) {
7704+ const injections = await getInjections ( urlInfo ) ;
7705+ if ( ! injections ) {
76517706 return {
76527707 contentInjections : defaultInjections ,
76537708 } ;
76547709 }
7655- const injections = await injectionsResult ;
76567710 return {
76577711 contentInjections : {
76587712 ...defaultInjections ,
@@ -7663,6 +7717,14 @@ const jsenvPluginInjections = (rawAssociations) => {
76637717 } ;
76647718} ;
76657719
7720+ const asOptionalInjections = ( injections ) => {
7721+ const optionalInjections = { } ;
7722+ for ( const key of Object . keys ( injections ) ) {
7723+ optionalInjections [ key ] = INJECTIONS . optional ( injections [ key ] ) ;
7724+ }
7725+ return optionalInjections ;
7726+ } ;
7727+
76667728/*
76677729 * Some code uses globals specific to Node.js in code meant to run in browsers...
76687730 * This plugin will replace some node globals to things compatible with web:
@@ -11064,6 +11126,11 @@ const createBuildSpecifierManager = ({
1106411126 registerHtmlRefine ( ( htmlAst , { registerHtmlMutation } ) => {
1106511127 visitHtmlNodes ( htmlAst , {
1106611128 link : ( node ) => {
11129+ if ( getHtmlNodeAttribute ( node , "jsenv-ignore" ) !== undefined ) {
11130+ // reference analysis skipped this node, so it has no urlInfo in the graph
11131+ // and there is nothing to resync
11132+ return ;
11133+ }
1106711134 const href = getHtmlNodeAttribute ( node , "href" ) ;
1106811135 if ( href === undefined || href . startsWith ( "data:" ) ) {
1106911136 return ;
@@ -11902,6 +11969,25 @@ const jsenvPluginMappings = (mappings) => {
1190211969 * How URLs are versioned for this entry point (defaults to "search_param")
1190311970 * @param {('none'|'inline'|'file'|'programmatic') } [entryPoint.sourcemaps]
1190411971 * Sourcemap generation strategy for this entry point (defaults to "none")
11972+ * @param {object } [entryPoint.injections]
11973+ * Values to inject into files, as { urlPattern: getInjections }.
11974+ * Keys are url patterns relative to sourceDirectoryUrl ("./index.html", "**\/*.js"),
11975+ * values are functions receiving urlInfo and returning (or resolving to)
11976+ * an object of placeholders to replace, named `__LIKE_THIS__` by convention:
11977+ *
11978+ * injections: {
11979+ * "./index.html": () => ({ __BACKEND_URL__: "https://api.example.com" }),
11980+ * }
11981+ *
11982+ * In JS files the value is injected as a JS literal (a string value brings its own quotes),
11983+ * everywhere else it is injected as-is, so it can be concatenated:
11984+ * `href="__BACKEND_URL__/users/me"`.
11985+ * An html url pattern also covers what is inlined in that html: an inline
11986+ * `<script>window.backendUrl = __BACKEND_URL__;</script>` gets the JS literal,
11987+ * which is how a value is shared with every js file of the page.
11988+ * Use INJECTIONS.optional(value) for a placeholder that may be absent from the file
11989+ * and INJECTIONS.global(value) to inject `Object.assign(window, { ... })` instead of
11990+ * replacing a placeholder.
1190511991 *
1190611992 * @return {Promise<Object> } buildReturnValue
1190711993 * @return {Promise<Object> } [buildReturnValue.buildInlineContents]
@@ -11939,6 +12025,17 @@ const build = async ({
1193912025 {
1194012026 const unexpectedParamNames = Object . keys ( rest ) ;
1194112027 if ( unexpectedParamNames . length > 0 ) {
12028+ const entryPointParamNames = unexpectedParamNames . filter ( ( name ) =>
12029+ Object . hasOwn ( entryPointDefaultParams , name ) ,
12030+ ) ;
12031+ if ( entryPointParamNames . length > 0 ) {
12032+ throw new TypeError (
12033+ `${ entryPointParamNames . join ( "," ) } : param(s) configured per entry point, move them into entryPoints, as in:
12034+ entryPoints: {
12035+ "./index.html": { ${ entryPointParamNames . map ( ( name ) => `${ name } : ...` ) . join ( ", " ) } },
12036+ }` ,
12037+ ) ;
12038+ }
1194212039 throw new TypeError (
1194312040 `${ unexpectedParamNames . join ( "," ) } : there is no such param` ,
1194412041 ) ;
0 commit comments