From b7055dbfb46e6a62cd97c6c87f1c03c3e4b6b3e6 Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Thu, 25 Dec 2025 00:08:21 +0800 Subject: [PATCH] Run browser tests on UMD version (#18526) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .github/workflows/prod-test.yml | 13 +- package.json | 8 +- src/standalone.js | 3 +- tests/config/browser/get-browser-prettier.js | 11 +- tests/config/browser/{esm.html => index.html} | 5 +- tests/config/browser/main.js | 172 +++++++++++++----- tests/config/browser/server.js | 4 +- tests/config/browser/utilities.js | 10 +- 8 files changed, 169 insertions(+), 57 deletions(-) rename tests/config/browser/{esm.html => index.html} (69%) diff --git a/.github/workflows/prod-test.yml b/.github/workflows/prod-test.yml index 8e4a4b014..2c328da35 100644 --- a/.github/workflows/prod-test.yml +++ b/.github/workflows/prod-test.yml @@ -145,7 +145,7 @@ jobs: working-directory: dist/prettier run: node bin/prettier.cjs --debug-benchmark package.json 2>&1 >/dev/null | grep "Latency avg" || exit 1 - browser-test: + browser: timeout-minutes: 15 strategy: fail-fast: false @@ -191,8 +191,15 @@ jobs: name: dist path: dist - - name: Run Tests - run: yarn test:production-browser + - name: Run Tests (ESM) + env: + TEST_BUNDLE_FORMAT: esm + run: yarn test:production:browser + + - name: Run Tests (UMD) + env: + TEST_BUNDLE_FORMAT: umd + run: yarn test:production:browser cli: name: CLI diff --git a/package.json b/package.json index 7de727987..7808baec3 100644 --- a/package.json +++ b/package.json @@ -191,9 +191,11 @@ "test:production-node14": "cross-env NODE_ENV=production yarn test:node14", "test:production-standalone": "cross-env TEST_STANDALONE=1 yarn test:production", "test:production-standalone-node14": "cross-env TEST_STANDALONE=1 yarn test:production-node14", - "test:production-browser": "cross-env NODE_ENV=production TEST_RUNTIME=browser yarn test", - "test:production-browser:chrome": "cross-env TEST_RUNTIME_BROWSER=chrome yarn test:production-browser", - "test:production-browser:firefox": "cross-env TEST_RUNTIME_BROWSER=firefox yarn test:production-browser", + "test:production:browser": "cross-env TEST_RUNTIME=browser yarn test:production", + "test:production:browser:chrome": "cross-env TEST_RUNTIME_BROWSER=chrome yarn test:production:browser", + "test:production:browser:firefox": "cross-env TEST_RUNTIME_BROWSER=firefox yarn test:production:browser", + "test:production:browser:chrome:umd": "cross-env TEST_BUNDLE_FORMAT=umd yarn test:production:browser:chrome", + "test:production:browser:firefox:umd": "cross-env TEST_BUNDLE_FORMAT=umd yarn test:production:browser:firefox", "test:production-lint": "eslint dist --config=./scripts/production-eslint-config.js --quiet --format friendly", "perf": "yarn && yarn build && cross-env NODE_ENV=production node ./dist/prettier/bin/prettier.cjs", "perf:inspect": "yarn && yarn build && cross-env NODE_ENV=production node --inspect-brk ./dist/prettier/bin/prettier.cjs", diff --git a/src/standalone.js b/src/standalone.js index e5ed71f06..12401b443 100644 --- a/src/standalone.js +++ b/src/standalone.js @@ -6,7 +6,6 @@ function withPlugins( optionsArgumentIndex = 1, // Usually `options` is the 2nd argument ) { // Returns Promises to consistent with functions in `index.js` - // eslint-disable-next-line require-await return async (...args) => { const options = args[optionsArgumentIndex] ?? {}; const plugins = options.plugins ?? []; @@ -16,7 +15,7 @@ function withPlugins( plugins: Array.isArray(plugins) ? plugins : Object.values(plugins), }; - return fn(...args); + return await fn(...args); }; } diff --git a/tests/config/browser/get-browser-prettier.js b/tests/config/browser/get-browser-prettier.js index 85bf3a47e..141e8286f 100644 --- a/tests/config/browser/get-browser-prettier.js +++ b/tests/config/browser/get-browser-prettier.js @@ -3,7 +3,10 @@ import { startServer } from "./server.js"; import { requestFromNode } from "./utilities.js"; async function getBrowserPrettier() { + /** @type {"chrome" | "firefox"} */ const browserName = process.env.TEST_RUNTIME_BROWSER ?? "chrome"; + /** @type {"esm" | "umd"} */ + const prettierBundleFormat = process.env.TEST_BUNDLE_FORMAT ?? "esm"; const browser = await launchBrowser({ browser: browserName }); process.once("exit", async () => { @@ -16,14 +19,14 @@ async function getBrowserPrettier() { }); const page = await browser.newPage(); - await page.goto(server.url + "/esm.html"); + await page.goto(server.url); const proxyFunction = (accessPath, optionsIndex = 1) => requestFromNode( (...arguments_) => page.evaluate( - ([arguments_, accessPath]) => { - const prettier = globalThis.__prettier; + ([arguments_, accessPath, prettierBundleFormat]) => { + const prettier = globalThis[`__prettier_${prettierBundleFormat}`]; let function_ = prettier; for (const property of accessPath.split(".")) { function_ = function_[property]; @@ -31,7 +34,7 @@ async function getBrowserPrettier() { return function_(...arguments_); }, - [arguments_, accessPath], + [arguments_, accessPath, prettierBundleFormat], ), { browser: browserName, diff --git a/tests/config/browser/esm.html b/tests/config/browser/index.html similarity index 69% rename from tests/config/browser/esm.html rename to tests/config/browser/index.html index ad94d922d..506854947 100644 --- a/tests/config/browser/esm.html +++ b/tests/config/browser/index.html @@ -6,7 +6,10 @@ Document -
Use __prettier to debug in console.
+
+ Use __prettier_esm or __prettier_umd to debug in + console. +
diff --git a/tests/config/browser/main.js b/tests/config/browser/main.js index 94c775693..44b054809 100644 --- a/tests/config/browser/main.js +++ b/tests/config/browser/main.js @@ -1,4 +1,4 @@ -import { responseInBrowser } from "./utilities.js"; +import { ensurePromise, responseInBrowser } from "./utilities.js"; const browser = navigator.userAgent.includes("Firefox/") ? "firefox" : "chrome"; @@ -22,50 +22,140 @@ const esmFiles = { "plugin-oxc/index.browser.mjs", ], }; +const umdFiles = { + prettier: "prettier/standalone.js", + plugins: [ + "prettier/plugins/angular.js", + "prettier/plugins/graphql.js", + "prettier/plugins/typescript.js", + "prettier/plugins/estree.js", + "prettier/plugins/markdown.js", + "prettier/plugins/acorn.js", + "prettier/plugins/glimmer.js", + "prettier/plugins/postcss.js", + "prettier/plugins/babel.js", + "prettier/plugins/html.js", + "prettier/plugins/yaml.js", + "prettier/plugins/flow.js", + "prettier/plugins/meriyah.js", + // These two plugins don't have umd version + "plugin-hermes/index.mjs", + "plugin-oxc/index.browser.mjs", + ], +}; -let prettier; -let builtinPlugins; +async function loadScript(url) { + const { document } = globalThis; + const { head } = document; + const script = Object.assign(document.createElement("script"), { src: url }); -function proxyFunction(accessPath, optionsIndex = 1) { - let function_; - return responseInBrowser( - async (...arguments_) => { - if (!prettier) { - [prettier, ...builtinPlugins] = await Promise.all( - [esmFiles.prettier, ...esmFiles.plugins].map( - (file) => import(`/${file}`), - ), - ); - } + function on(event, callback) { + script.addEventListener(event, callback, { once: true, passive: true }); + } - if (!function_) { - function_ = prettier; - for (const property of accessPath.split(".")) { - function_ = function_[property]; - } - } + try { + await new Promise((resolve, reject) => { + on("load", resolve); - const options = arguments_[optionsIndex] ?? {}; + on("error", () => { + reject(new Error(`Failed to load script '${url}'.`)); + }); - arguments_[optionsIndex] = { - ...options, - plugins: [...builtinPlugins, ...(options.plugins || [])], - }; - - return function_(...arguments_); - }, - { - browser, - accessPath, - optionsIndex, - }, - ); + head.appendChild(script); + }); + } finally { + head.removeChild(script); + } } -globalThis.__prettier = { - formatWithCursor: proxyFunction("formatWithCursor"), - getSupportInfo: proxyFunction("getSupportInfo", /* optionsIndex */ 0), - __debug: { - parse: proxyFunction("__debug.parse"), - }, -}; +async function loadEsmPrettier() { + const [prettier, ...builtinPlugins] = await Promise.all( + [esmFiles.prettier, ...esmFiles.plugins].map((file) => import(`/${file}`)), + ); + + return { prettier, builtinPlugins }; +} + +async function loadUmdPrettier() { + const { umdPluginFiles, esmPluginFiles } = Object.groupBy( + umdFiles.plugins, + (file) => `${file.endsWith(".mjs") ? "esm" : "umd"}PluginFiles`, + ); + + const [, esmPlugins] = await Promise.all([ + Promise.all( + [umdFiles.prettier, ...umdPluginFiles].map((file) => + loadScript(`/${file}`), + ), + ), + Promise.all(esmPluginFiles.map((file) => import(`/${file}`))), + ]); + + const { prettier } = globalThis; + const builtinPlugins = [ + ...Object.values(globalThis.prettierPlugins), + ...esmPlugins, + ]; + delete globalThis.prettier; + delete globalThis.prettierPlugins; + return { prettier, builtinPlugins }; +} + +/** +@param {"esm" | "umd"} prettierBundleFormat +*/ +function createPrettier(prettierBundleFormat) { + let cache; + function loadPrettier() { + if (!cache) { + cache = + prettierBundleFormat === "esm" ? loadEsmPrettier() : loadUmdPrettier(); + } + + return cache; + } + + const functions = new Map(); + + function proxyFunction(accessPath, optionsIndex = 1) { + let function_; + return responseInBrowser( + async (...arguments_) => { + const { prettier, builtinPlugins } = await loadPrettier(); + + if (!functions.has(accessPath)) { + function_ = prettier; + for (const property of accessPath.split(".")) { + function_ = function_[property]; + } + functions.set(accessPath, function_); + } + + const options = arguments_[optionsIndex] ?? {}; + + arguments_[optionsIndex] = { + ...options, + plugins: [...builtinPlugins, ...(options.plugins || [])], + }; + + return ensurePromise(function_(...arguments_)); + }, + { + browser, + accessPath, + optionsIndex, + }, + ); + } + + return { + formatWithCursor: proxyFunction("formatWithCursor"), + getSupportInfo: proxyFunction("getSupportInfo", /* optionsIndex */ 0), + __debug: { + parse: proxyFunction("__debug.parse"), + }, + }; +} + +globalThis.__prettier_esm = createPrettier("esm"); +globalThis.__prettier_umd = createPrettier("umd"); diff --git a/tests/config/browser/server.js b/tests/config/browser/server.js index 0abaeec29..27e4fcd55 100644 --- a/tests/config/browser/server.js +++ b/tests/config/browser/server.js @@ -9,7 +9,7 @@ async function getContent(url) { assert.ok(url.startsWith("/")); url = url.slice(1); - if (url === "esm.html" || url === "main.js" || url === "utilities.js") { + if (url === "index.html" || url === "main.js" || url === "utilities.js") { return await fs.readFile(new URL(url, import.meta.url), "utf8"); } @@ -29,7 +29,7 @@ async function startServer({ silent = false, port: preferredPort } = {}) { } if (url === "/") { - url = "/esm.html"; + url = "/index.html"; } // Only allow `.mjs`, `.js` and `.html` diff --git a/tests/config/browser/utilities.js b/tests/config/browser/utilities.js index 9cdddb83c..bad4a70b2 100644 --- a/tests/config/browser/utilities.js +++ b/tests/config/browser/utilities.js @@ -157,4 +157,12 @@ function serializeValue(value) { return value; } -export { requestFromNode, responseInBrowser }; +const ensurePromise = (value) => { + if (!(value instanceof Promise)) { + throw new TypeError("Expected value to be a 'Promise'."); + } + + return value; +}; + +export { ensurePromise, requestFromNode, responseInBrowser };