From 35ab96138ddfea3998a921f97d31c390199eca0c Mon Sep 17 00:00:00 2001 From: Yulong Wang <7679871+fs-eire@users.noreply.github.com> Date: Wed, 5 Mar 2025 09:48:36 -0800 Subject: [PATCH 1/2] [js/web] improve workaround for bundlers --- js/web/lib/wasm/proxy-wrapper.ts | 8 +++-- js/web/lib/wasm/wasm-utils-import.ts | 50 ++++++++++++++++++++++++++-- js/web/script/build.ts | 19 ++++++++--- js/web/test/e2e/exports/main.js | 11 +++++- js/web/test/e2e/exports/test.js | 22 ++++++++++++ 5 files changed, 100 insertions(+), 10 deletions(-) diff --git a/js/web/lib/wasm/proxy-wrapper.ts b/js/web/lib/wasm/proxy-wrapper.ts index 5d97bb83e3475..30b1f5101e5f2 100644 --- a/js/web/lib/wasm/proxy-wrapper.ts +++ b/js/web/lib/wasm/proxy-wrapper.ts @@ -12,7 +12,11 @@ import { } from './proxy-messages'; import * as core from './wasm-core-impl'; import { initializeWebAssembly } from './wasm-factory'; -import { importProxyWorker, inferWasmPathPrefixFromScriptSrc } from './wasm-utils-import'; +import { + importProxyWorker, + inferWasmPathPrefixFromScriptSrc, + isEsmImportMetaUrlHardcodedAsFileUri, +} from './wasm-utils-import'; const isProxy = (): boolean => !!env.wasm.proxy && typeof document !== 'undefined'; let proxyWorker: Worker | undefined; @@ -116,7 +120,7 @@ export const initializeWebAssemblyAndOrtRuntime = async (): Promise => { BUILD_DEFS.IS_ESM && BUILD_DEFS.ENABLE_BUNDLE_WASM_JS && !message.in!.wasm.wasmPaths && - (objectUrl || BUILD_DEFS.ESM_IMPORT_META_URL?.startsWith('file:')) + (objectUrl || isEsmImportMetaUrlHardcodedAsFileUri) ) { // for a build bundled the wasm JS, if either of the following conditions is met: // - the proxy worker is loaded from a blob URL diff --git a/js/web/lib/wasm/wasm-utils-import.ts b/js/web/lib/wasm/wasm-utils-import.ts index 871b575d71edc..a8e27f6f334bc 100644 --- a/js/web/lib/wasm/wasm-utils-import.ts +++ b/js/web/lib/wasm/wasm-utils-import.ts @@ -11,6 +11,39 @@ import { isNode } from './wasm-utils-env'; */ const origin = isNode || typeof location === 'undefined' ? undefined : location.origin; +/** + * Some bundlers (eg. Webpack) will rewrite `import.meta.url` to a file URL at compile time. + * + * This function checks if `import.meta.url` starts with `file:`, but using the `>` and `<` operators instead of + * `startsWith` function so that code minimizers can remove the dead code correctly. + * + * For example, if we use terser to minify the following code: + * ```js + * if ("file://hard-coded-filename".startsWith("file:")) { + * console.log(1) + * } else { + * console.log(2) + * } + * + * if ("file://hard-coded-filename" > "file:" && "file://hard-coded-filename" < "file;") { + * console.log(3) + * } else { + * console.log(4) + * } + * ``` + * + * The minified code will be: + * ```js + * "file://hard-coded-filename".startsWith("file:")?console.log(1):console.log(2),console.log(3); + * ``` + * + * (use Terser 5.39.0 with default options, https://try.terser.org/) + * + * @returns true if the import.meta.url is hardcoded as a file URI. + */ +export const isEsmImportMetaUrlHardcodedAsFileUri = + BUILD_DEFS.IS_ESM && BUILD_DEFS.ESM_IMPORT_META_URL! > 'file:' && BUILD_DEFS.ESM_IMPORT_META_URL! < 'file;'; + const getScriptSrc = (): string | undefined => { // if Nodejs, return undefined if (isNode) { @@ -26,9 +59,22 @@ const getScriptSrc = (): string | undefined => { // new URL('actual-bundle-name.js', import.meta.url).href // ``` // So that bundler can preprocess the URL correctly. - if (BUILD_DEFS.ESM_IMPORT_META_URL?.startsWith('file:')) { + if (isEsmImportMetaUrlHardcodedAsFileUri) { // if the rewritten URL is a relative path, we need to use the origin to resolve the URL. - return new URL(new URL(BUILD_DEFS.BUNDLE_FILENAME, BUILD_DEFS.ESM_IMPORT_META_URL).href, origin).href; + + // The following is a workaround for Vite. + // + // Vite uses a bundler(rollup/rolldown) that does not rewrite `import.meta.url` to a file URL. So in theory, this + // code path should not be executed in Vite. However, the bundler does not know it and it still try to load the + // following pattern: + // - `return new URL('filename', import.meta.url).href` + // + // By replacing the pattern above with the following code, we can skip the resource loading behavior: + // - `const URL2 = URL; return new URL2('filename', import.meta.url).href;` + // + // And it still works in Webpack. + const URL2 = URL; + return new URL(new URL2(BUILD_DEFS.BUNDLE_FILENAME, BUILD_DEFS.ESM_IMPORT_META_URL).href, origin).href; } return BUILD_DEFS.ESM_IMPORT_META_URL; diff --git a/js/web/script/build.ts b/js/web/script/build.ts index 6006de62b41b6..7966262631bbf 100644 --- a/js/web/script/build.ts +++ b/js/web/script/build.ts @@ -123,13 +123,17 @@ async function minifyWasmModuleJsForBrowser(filepath: string): Promise { // ``` // with: // ``` - // new Worker(import.meta.url.startsWith('file:') - // ? new URL(BUILD_DEFS.BUNDLE_FILENAME, import.meta.url) - // : new URL(import.meta.url), ... + // new Worker((() => { + // const URL2 = URL; + // return import.meta.url > 'file:' && import.meta.url < 'file;' + // ? new URL2(BUILD_DEFS.BUNDLE_FILENAME, import.meta.url) + // : new URL(import.meta.url); + // })(), ... // ``` // // NOTE: this is a workaround for some bundlers that does not support runtime import.meta.url. - // TODO: in emscripten 3.1.61+, need to update this code. + // + // Check more details in the comment of `isEsmImportMetaUrlHardcodedAsFileUri()` and `getScriptSrc()` in file `lib/wasm/wasm-utils-import.ts`. // First, check if there is exactly one occurrence of "new Worker(new URL(import.meta.url)". const matches = [...contents.matchAll(/new Worker\(new URL\(import\.meta\.url\),/g)]; @@ -142,7 +146,12 @@ async function minifyWasmModuleJsForBrowser(filepath: string): Promise { // Replace the only occurrence. contents = contents.replace( /new Worker\(new URL\(import\.meta\.url\),/, - `new Worker(import.meta.url.startsWith('file:')?new URL(BUILD_DEFS.BUNDLE_FILENAME, import.meta.url):new URL(import.meta.url),`, + `new Worker((() => { + const URL2 = URL; + return (import.meta.url > 'file:' && import.meta.url < 'file;') + ? new URL2(BUILD_DEFS.BUNDLE_FILENAME, import.meta.url) + : new URL(import.meta.url); + })(),`, ); // Use terser to minify the code with special configurations: diff --git a/js/web/test/e2e/exports/main.js b/js/web/test/e2e/exports/main.js index 8ed22a6784e7c..4a750a9dd0a65 100644 --- a/js/web/test/e2e/exports/main.js +++ b/js/web/test/e2e/exports/main.js @@ -3,7 +3,7 @@ 'use strict'; -const { runDevTest, runProdTest } = require('./test'); +const { runDevTest, runProdTest, verifyAssets } = require('./test'); const { installOrtPackages } = require('./utils'); /** @@ -29,5 +29,14 @@ module.exports = async function main(PRESERVE, PACKAGES_TO_INSTALL) { await runDevTest('vite-default', '\x1b[32m➜\x1b[39m \x1b[1mLocal\x1b[22m:', 5173); await runProdTest('vite-default', '\x1b[32m➜\x1b[39m \x1b[1mLocal\x1b[22m:', 4173); + + await verifyAssets('vite-default', async (cwd) => { + const globby = require('globby'); + + return { + test: 'File "dist/assets/**/ort.*.mjs" should not exist', + success: globby.globbySync('dist/assets/**/ort.*.mjs', { cwd }).length === 0, + }; + }); } }; diff --git a/js/web/test/e2e/exports/test.js b/js/web/test/e2e/exports/test.js index 9c5ed745ab0b5..e2bcffea97519 100644 --- a/js/web/test/e2e/exports/test.js +++ b/js/web/test/e2e/exports/test.js @@ -121,7 +121,29 @@ async function runProdTest(testCaseName, ready, port) { await runTest(testCaseName, ['prod'], ready, 'npm run start', port); } +async function verifyAssets(testCaseName, testers) { + testers = Array.isArray(testers) ? testers : [testers]; + const wd = path.join(__dirname, 'testcases', testCaseName); + + console.log(`[${testCaseName}] Verifying assets...`); + + const testResults = []; + + try { + for (const tester of testers) { + testResults.push(await tester(wd)); + } + + if (testResults.some((r) => !r.success)) { + throw new Error(`[${testCaseName}] asset verification failed.`); + } + } finally { + console.log(`[${testCaseName}] asset verification result:`, testResults); + } +} + module.exports = { runDevTest, runProdTest, + verifyAssets, }; From e5a063ec567272da740f0f7d48041678a4a52e0d Mon Sep 17 00:00:00 2001 From: Yulong Wang <7679871+fs-eire@users.noreply.github.com> Date: Wed, 5 Mar 2025 14:13:44 -0800 Subject: [PATCH 2/2] fix E2E test --- js/web/test/e2e/exports/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/web/test/e2e/exports/main.js b/js/web/test/e2e/exports/main.js index 4a750a9dd0a65..d8c7bbf69039f 100644 --- a/js/web/test/e2e/exports/main.js +++ b/js/web/test/e2e/exports/main.js @@ -31,7 +31,7 @@ module.exports = async function main(PRESERVE, PACKAGES_TO_INSTALL) { await runProdTest('vite-default', '\x1b[32m➜\x1b[39m \x1b[1mLocal\x1b[22m:', 4173); await verifyAssets('vite-default', async (cwd) => { - const globby = require('globby'); + const globby = await import('globby'); return { test: 'File "dist/assets/**/ort.*.mjs" should not exist',