diff --git a/CHANGELOG.md b/CHANGELOG.md index b8e7902d57..05f8f7754a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 29.7.0 + +- Export jsenvPluginPlaceholders + # 29.6.1 - Fix error introduced in 29.6.0 on dev server diff --git a/packages/jsenv-plugin-react/package.json b/packages/jsenv-plugin-react/package.json index 711324141a..155d5c8e00 100644 --- a/packages/jsenv-plugin-react/package.json +++ b/packages/jsenv-plugin-react/package.json @@ -1,6 +1,6 @@ { "name": "@jsenv/plugin-react", - "version": "1.1.5", + "version": "1.1.6", "license": "MIT", "repository": { "type": "git", diff --git a/packages/jsenv-plugin-react/src/jsenv_plugin_react.js b/packages/jsenv-plugin-react/src/jsenv_plugin_react.js index c92c7288e2..0a92350c76 100644 --- a/packages/jsenv-plugin-react/src/jsenv_plugin_react.js +++ b/packages/jsenv-plugin-react/src/jsenv_plugin_react.js @@ -24,6 +24,10 @@ export const jsenvPluginReact = ({ "/**/node_modules/react/jsx-runtime/": { external: ["react"] }, "/**/node_modules/react/jsx-dev-runtime": { external: ["react"] }, "/**/react-refresh/": { external: ["react"] }, + // in case redux is used + "/**/node_modules/react-is/": true, + "/**/node_modules/use-sync-external-store/": { external: ["react"] }, + "/**/node_modules/hoist-non-react-statics/": { external: ["react-is"] }, }, }), jsenvPluginReactRefreshPreamble(), diff --git a/packages/urls/package.json b/packages/urls/package.json index 14ca3a9915..c4e5b197d3 100644 --- a/packages/urls/package.json +++ b/packages/urls/package.json @@ -1,6 +1,6 @@ { "name": "@jsenv/urls", - "version": "1.2.7", + "version": "1.2.8", "license": "MIT", "repository": { "type": "git", diff --git a/packages/urls/src/url_utils.js b/packages/urls/src/url_utils.js index 58edc77dac..dae9d6b396 100644 --- a/packages/urls/src/url_utils.js +++ b/packages/urls/src/url_utils.js @@ -3,9 +3,12 @@ import { urlToExtension } from "./url_to_extension.js" import { urlToResource } from "./url_to_resource.js" export const asUrlWithoutSearch = (url) => { - const urlObject = new URL(url) - urlObject.search = "" - return urlObject.href + if (url.includes("?")) { + const urlObject = new URL(url) + urlObject.search = "" + return urlObject.href + } + return url } export const isValidUrl = (url) => { diff --git a/src/main.js b/src/main.js index 203b592deb..7fd827f519 100644 --- a/src/main.js +++ b/src/main.js @@ -23,8 +23,8 @@ export { startBuildServer } from "./build/start_build_server.js" // helpers export { pingServer } from "./ping_server.js" -export { replacePlaceholders } from "./helpers/replace_placeholders.js" // advanced -export { execute } from "./execute/execute.js" export { jsenvPluginInjectGlobals } from "./plugins/inject_globals/jsenv_plugin_inject_globals.js" +export { jsenvPluginPlaceholders } from "./plugins/placeholders/jsenv_plugin_placeholders.js" +export { execute } from "./execute/execute.js" diff --git a/src/plugins/inject_globals/jsenv_plugin_inject_globals.js b/src/plugins/inject_globals/jsenv_plugin_inject_globals.js index 86c3308bb8..f29214326a 100644 --- a/src/plugins/inject_globals/jsenv_plugin_inject_globals.js +++ b/src/plugins/inject_globals/jsenv_plugin_inject_globals.js @@ -1,21 +1,33 @@ +import { URL_META } from "@jsenv/url-meta" +import { asUrlWithoutSearch } from "@jsenv/urls" + import { injectGlobals } from "./inject_globals.js" -export const jsenvPluginInjectGlobals = (urlAssociations) => { +export const jsenvPluginInjectGlobals = (rawAssociations) => { + let resolvedAssociations + return { name: "jsenv:inject_globals", appliesDuring: "*", + init: (context) => { + resolvedAssociations = URL_META.resolveAssociations( + { injector: rawAssociations }, + context.rootDirectoryUrl, + ) + }, transformUrlContent: async (urlInfo, context) => { - const url = Object.keys(urlAssociations).find((url) => { - return url === urlInfo.url + const { injector } = URL_META.applyAssociations({ + url: asUrlWithoutSearch(urlInfo.url), + associations: resolvedAssociations, }) - if (!url) { + if (!injector) { return null } - let globals = urlAssociations[url] - if (typeof globals === "function") { - globals = await globals(urlInfo, context) + if (typeof injector !== "function") { + throw new TypeError("injector must be a function") } - if (Object.keys(globals).length === 0) { + const globals = await injector(urlInfo, context) + if (!globals || Object.keys(globals).length === 0) { return null } return injectGlobals(urlInfo, globals) diff --git a/src/plugins/minification/jsenv_plugin_minification.js b/src/plugins/minification/jsenv_plugin_minification.js index a16b5c695e..e2670ab7b9 100644 --- a/src/plugins/minification/jsenv_plugin_minification.js +++ b/src/plugins/minification/jsenv_plugin_minification.js @@ -37,6 +37,30 @@ export const jsenvPluginMinification = (minification) => { options: minification.json, }) : null + const cssOptimizer = minification.css + ? (urlInfo, context) => + minifyCss({ + cssUrlInfo: urlInfo, + context, + options: minification.css, + }) + : null + const jsClassicOptimizer = minification.js_classic + ? (urlInfo, context) => + minifyJs({ + jsUrlInfo: urlInfo, + context, + options: minification.js_classic, + }) + : null + const jsModuleOptimizer = minification.js_module + ? (urlInfo, context) => + minifyJs({ + jsUrlInfo: urlInfo, + context, + options: minification.js_module, + }) + : null return { name: "jsenv:minification", @@ -44,30 +68,9 @@ export const jsenvPluginMinification = (minification) => { optimizeUrlContent: { html: htmlOptimizer, svg: htmlOptimizer, - css: minification.css - ? (urlInfo, context) => - minifyCss({ - cssUrlInfo: urlInfo, - context, - options: minification.css, - }) - : null, - js_classic: minification.js_classic - ? (urlInfo, context) => - minifyJs({ - jsUrlInfo: urlInfo, - context, - options: minification.js_classic, - }) - : null, - js_module: minification.js_module - ? (urlInfo, context) => - minifyJs({ - jsUrlInfo: urlInfo, - context, - options: minification.js_module, - }) - : null, + css: cssOptimizer, + js_classic: jsClassicOptimizer, + js_module: jsModuleOptimizer, json: jsonOptimizer, importmap: jsonOptimizer, webmanifest: jsonOptimizer, diff --git a/src/plugins/placeholders/jsenv_plugin_placeholders.js b/src/plugins/placeholders/jsenv_plugin_placeholders.js new file mode 100644 index 0000000000..9a17a87bb7 --- /dev/null +++ b/src/plugins/placeholders/jsenv_plugin_placeholders.js @@ -0,0 +1,36 @@ +import { URL_META } from "@jsenv/url-meta" +import { asUrlWithoutSearch } from "@jsenv/urls" + +import { replacePlaceholders } from "./replace_placeholders.js" + +export const jsenvPluginPlaceholders = (rawAssociations) => { + let resolvedAssociations + + return { + name: "jsenv:placeholders", + appliesDuring: "*", + init: (context) => { + resolvedAssociations = URL_META.resolveAssociations( + { replacer: rawAssociations }, + context.rootDirectoryUrl, + ) + }, + transformUrlContent: async (urlInfo, context) => { + const { replacer } = URL_META.applyAssociations({ + url: asUrlWithoutSearch(urlInfo.url), + associations: resolvedAssociations, + }) + if (!replacer) { + return null + } + if (typeof replacer !== "function") { + throw new TypeError("replacer must be a function") + } + const replacements = await replacer(urlInfo, context) + if (!replacements || Object.keys(replacements).length === 0) { + return null + } + return replacePlaceholders(urlInfo, replacements) + }, + } +} diff --git a/src/helpers/replace_placeholders.js b/src/plugins/placeholders/replace_placeholders.js similarity index 51% rename from src/helpers/replace_placeholders.js rename to src/plugins/placeholders/replace_placeholders.js index 7c8e3c31ed..fbacd1ec8b 100644 --- a/src/helpers/replace_placeholders.js +++ b/src/plugins/placeholders/replace_placeholders.js @@ -1,13 +1,21 @@ import { createMagicSource } from "@jsenv/sourcemap" -export const replacePlaceholders = (content, replacements) => { +export const replacePlaceholders = (urlInfo, replacements) => { + const content = urlInfo.content const magicSource = createMagicSource(content) Object.keys(replacements).forEach((key) => { let index = content.indexOf(key) while (index !== -1) { const start = index const end = index + key.length - magicSource.replace({ start, end, replacement: replacements[key] }) + magicSource.replace({ + start, + end, + replacement: + urlInfo.type === "js_classic" || urlInfo.type === "js_module" + ? JSON.stringify(replacements[key], null, " ") + : replacements[key], + }) index = content.indexOf(key, end) } }) diff --git a/src/plugins/url_version/jsenv_plugin_url_version.js b/src/plugins/url_version/jsenv_plugin_url_version.js index f508fa23ae..1a6d43a456 100644 --- a/src/plugins/url_version/jsenv_plugin_url_version.js +++ b/src/plugins/url_version/jsenv_plugin_url_version.js @@ -1,7 +1,7 @@ export const jsenvPluginUrlVersion = () => { return { name: "jsenv:url_version", - appliesDuring: "*", + appliesDuring: "dev", redirectUrl: (reference) => { // "v" search param goal is to enable long-term cache // for server response headers diff --git a/tests/__internal__/replace_placerholders.test.mjs b/tests/__internal__/replace_placerholders.test.mjs index 889608635f..81a3bb31af 100644 --- a/tests/__internal__/replace_placerholders.test.mjs +++ b/tests/__internal__/replace_placerholders.test.mjs @@ -1,14 +1,17 @@ import { assert } from "@jsenv/assert" -import { replacePlaceholders } from "@jsenv/core" +import { replacePlaceholders } from "@jsenv/core/src/plugins/placeholders/replace_placeholders.js" const result = replacePlaceholders( - `const foo = __FOO__ + { + type: "js_module", + content: `const foo = __FOO__ const t = __FOO__ const bar = __BAR__`, + }, { - __FOO__: JSON.stringify("hello"), - __BAR__: JSON.stringify("world"), + __FOO__: "hello", + __BAR__: "world", }, ) const actual = result.content diff --git a/tests/dev_and_build/inject_globals/client/main.html b/tests/dev_and_build/inject_globals/client/main.html new file mode 100644 index 0000000000..92821594a0 --- /dev/null +++ b/tests/dev_and_build/inject_globals/client/main.html @@ -0,0 +1,16 @@ + + + + Title + + + + + + + + diff --git a/tests/dev_and_build/inject_globals/client/main.js b/tests/dev_and_build/inject_globals/client/main.js new file mode 100644 index 0000000000..5cb59906bf --- /dev/null +++ b/tests/dev_and_build/inject_globals/client/main.js @@ -0,0 +1 @@ +window.resolveResultPromise(window.__DEMO__) diff --git a/tests/dev_and_build/inject_globals/inject_globals_build.test.mjs b/tests/dev_and_build/inject_globals/inject_globals_build.test.mjs new file mode 100644 index 0000000000..95fea33677 --- /dev/null +++ b/tests/dev_and_build/inject_globals/inject_globals_build.test.mjs @@ -0,0 +1,37 @@ +import { assert } from "@jsenv/assert" + +import { build } from "@jsenv/core" +import { startFileServer } from "@jsenv/core/tests/start_file_server.js" +import { executeInChromium } from "@jsenv/core/tests/execute_in_chromium.js" +import { plugins } from "./jsenv_config.mjs" + +const test = async (params) => { + await build({ + logLevel: "warn", + rootDirectoryUrl: new URL("./client/", import.meta.url), + buildDirectoryUrl: new URL("./dist/", import.meta.url), + entryPoints: { + "./main.html": "main.html", + }, + plugins, + minification: false, + ...params, + }) + const server = await startFileServer({ + rootDirectoryUrl: new URL("./dist/", import.meta.url), + }) + const { returnValue } = await executeInChromium({ + url: `${server.origin}/main.html`, + /* eslint-disable no-undef */ + pageFunction: async () => { + return window.resultPromise + }, + /* eslint-enable no-undef */ + }) + + const actual = returnValue + const expected = "build" + assert({ actual, expected }) +} + +await test() diff --git a/tests/dev_and_build/inject_globals/inject_globals_dev.test.mjs b/tests/dev_and_build/inject_globals/inject_globals_dev.test.mjs new file mode 100644 index 0000000000..9e73b02066 --- /dev/null +++ b/tests/dev_and_build/inject_globals/inject_globals_dev.test.mjs @@ -0,0 +1,29 @@ +import { assert } from "@jsenv/assert" + +import { startDevServer } from "@jsenv/core" +import { executeInChromium } from "@jsenv/core/tests/execute_in_chromium.js" +import { plugins } from "./jsenv_config.mjs" + +const test = async (params) => { + const devServer = await startDevServer({ + logLevel: "warn", + rootDirectoryUrl: new URL("./client/", import.meta.url), + keepProcessAlive: false, + plugins, + ...params, + }) + const { returnValue } = await executeInChromium({ + url: `${devServer.origin}/main.html`, + /* eslint-disable no-undef */ + pageFunction: async () => { + return window.resultPromise + }, + /* eslint-enable no-undef */ + }) + + const actual = returnValue + const expected = "dev" + assert({ actual, expected }) +} + +await test() diff --git a/tests/dev_and_build/inject_globals/jsenv_config.mjs b/tests/dev_and_build/inject_globals/jsenv_config.mjs new file mode 100644 index 0000000000..7bf687dd58 --- /dev/null +++ b/tests/dev_and_build/inject_globals/jsenv_config.mjs @@ -0,0 +1,9 @@ +import { jsenvPluginInjectGlobals } from "@jsenv/core" + +export const plugins = [ + jsenvPluginInjectGlobals({ + "./main.html": (urlInfo, context) => { + return { __DEMO__: context.scenarios.dev ? "dev" : "build" } + }, + }), +] diff --git a/tests/dev_and_build/placeholders/client/main.html b/tests/dev_and_build/placeholders/client/main.html new file mode 100644 index 0000000000..3a8c6d0916 --- /dev/null +++ b/tests/dev_and_build/placeholders/client/main.html @@ -0,0 +1,16 @@ + + + + Title + + + + + + + + diff --git a/tests/dev_and_build/placeholders/client/main.js b/tests/dev_and_build/placeholders/client/main.js new file mode 100644 index 0000000000..efb50845ca --- /dev/null +++ b/tests/dev_and_build/placeholders/client/main.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-undef +window.resolveResultPromise(__DEMO__) diff --git a/tests/dev_and_build/placeholders/jsenv_config.mjs b/tests/dev_and_build/placeholders/jsenv_config.mjs new file mode 100644 index 0000000000..a8dcff6c83 --- /dev/null +++ b/tests/dev_and_build/placeholders/jsenv_config.mjs @@ -0,0 +1,11 @@ +import { jsenvPluginPlaceholders } from "@jsenv/core" + +export const plugins = [ + jsenvPluginPlaceholders({ + "./main.js": (urlInfo, context) => { + return { + __DEMO__: context.scenarios.dev ? "dev" : "build", + } + }, + }), +] diff --git a/tests/dev_and_build/placeholders/placeholders_build.test.mjs b/tests/dev_and_build/placeholders/placeholders_build.test.mjs new file mode 100644 index 0000000000..95fea33677 --- /dev/null +++ b/tests/dev_and_build/placeholders/placeholders_build.test.mjs @@ -0,0 +1,37 @@ +import { assert } from "@jsenv/assert" + +import { build } from "@jsenv/core" +import { startFileServer } from "@jsenv/core/tests/start_file_server.js" +import { executeInChromium } from "@jsenv/core/tests/execute_in_chromium.js" +import { plugins } from "./jsenv_config.mjs" + +const test = async (params) => { + await build({ + logLevel: "warn", + rootDirectoryUrl: new URL("./client/", import.meta.url), + buildDirectoryUrl: new URL("./dist/", import.meta.url), + entryPoints: { + "./main.html": "main.html", + }, + plugins, + minification: false, + ...params, + }) + const server = await startFileServer({ + rootDirectoryUrl: new URL("./dist/", import.meta.url), + }) + const { returnValue } = await executeInChromium({ + url: `${server.origin}/main.html`, + /* eslint-disable no-undef */ + pageFunction: async () => { + return window.resultPromise + }, + /* eslint-enable no-undef */ + }) + + const actual = returnValue + const expected = "build" + assert({ actual, expected }) +} + +await test() diff --git a/tests/dev_and_build/placeholders/placeholders_dev.test.mjs b/tests/dev_and_build/placeholders/placeholders_dev.test.mjs new file mode 100644 index 0000000000..9e73b02066 --- /dev/null +++ b/tests/dev_and_build/placeholders/placeholders_dev.test.mjs @@ -0,0 +1,29 @@ +import { assert } from "@jsenv/assert" + +import { startDevServer } from "@jsenv/core" +import { executeInChromium } from "@jsenv/core/tests/execute_in_chromium.js" +import { plugins } from "./jsenv_config.mjs" + +const test = async (params) => { + const devServer = await startDevServer({ + logLevel: "warn", + rootDirectoryUrl: new URL("./client/", import.meta.url), + keepProcessAlive: false, + plugins, + ...params, + }) + const { returnValue } = await executeInChromium({ + url: `${devServer.origin}/main.html`, + /* eslint-disable no-undef */ + pageFunction: async () => { + return window.resultPromise + }, + /* eslint-enable no-undef */ + }) + + const actual = returnValue + const expected = "dev" + assert({ actual, expected }) +} + +await test()