Skip to content

Commit aafa8d1

Browse files
authored
[js/web] improve workaround for bundlers (#23902)
### Description This PR improves the workaround for bundlers in onnxruntime-web. Specifically, the following changes have been made: - Use [this workaround](xenova@9c50aa2) as suggested by @xenova in huggingface/transformers.js#1161 (comment) - Use `url > "file:" && url < "file;"` instead of `url.startsWith("file:")` to allow minifiers to remove dead code correctly. This change allows to remove unnecessary dependencies of file parsed from `new URL("ort.bundle.min.js", import.meta.url)` in Vite, and optimize code like `if("file://filepath.js".startsWith("file:")) {do_sth1(); } else {do_sth2();}` into `do_sth1()` for webpack/terser usages. Resolves huggingface/transformers.js#1161
1 parent 5e636a6 commit aafa8d1

File tree

5 files changed

+100
-10
lines changed

5 files changed

+100
-10
lines changed

js/web/lib/wasm/proxy-wrapper.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import {
1212
} from './proxy-messages';
1313
import * as core from './wasm-core-impl';
1414
import { initializeWebAssembly } from './wasm-factory';
15-
import { importProxyWorker, inferWasmPathPrefixFromScriptSrc } from './wasm-utils-import';
15+
import {
16+
importProxyWorker,
17+
inferWasmPathPrefixFromScriptSrc,
18+
isEsmImportMetaUrlHardcodedAsFileUri,
19+
} from './wasm-utils-import';
1620

1721
const isProxy = (): boolean => !!env.wasm.proxy && typeof document !== 'undefined';
1822
let proxyWorker: Worker | undefined;
@@ -116,7 +120,7 @@ export const initializeWebAssemblyAndOrtRuntime = async (): Promise<void> => {
116120
BUILD_DEFS.IS_ESM &&
117121
BUILD_DEFS.ENABLE_BUNDLE_WASM_JS &&
118122
!message.in!.wasm.wasmPaths &&
119-
(objectUrl || BUILD_DEFS.ESM_IMPORT_META_URL?.startsWith('file:'))
123+
(objectUrl || isEsmImportMetaUrlHardcodedAsFileUri)
120124
) {
121125
// for a build bundled the wasm JS, if either of the following conditions is met:
122126
// - the proxy worker is loaded from a blob URL

js/web/lib/wasm/wasm-utils-import.ts

+48-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,39 @@ import { isNode } from './wasm-utils-env';
1111
*/
1212
const origin = isNode || typeof location === 'undefined' ? undefined : location.origin;
1313

14+
/**
15+
* Some bundlers (eg. Webpack) will rewrite `import.meta.url` to a file URL at compile time.
16+
*
17+
* This function checks if `import.meta.url` starts with `file:`, but using the `>` and `<` operators instead of
18+
* `startsWith` function so that code minimizers can remove the dead code correctly.
19+
*
20+
* For example, if we use terser to minify the following code:
21+
* ```js
22+
* if ("file://hard-coded-filename".startsWith("file:")) {
23+
* console.log(1)
24+
* } else {
25+
* console.log(2)
26+
* }
27+
*
28+
* if ("file://hard-coded-filename" > "file:" && "file://hard-coded-filename" < "file;") {
29+
* console.log(3)
30+
* } else {
31+
* console.log(4)
32+
* }
33+
* ```
34+
*
35+
* The minified code will be:
36+
* ```js
37+
* "file://hard-coded-filename".startsWith("file:")?console.log(1):console.log(2),console.log(3);
38+
* ```
39+
*
40+
* (use Terser 5.39.0 with default options, https://try.terser.org/)
41+
*
42+
* @returns true if the import.meta.url is hardcoded as a file URI.
43+
*/
44+
export const isEsmImportMetaUrlHardcodedAsFileUri =
45+
BUILD_DEFS.IS_ESM && BUILD_DEFS.ESM_IMPORT_META_URL! > 'file:' && BUILD_DEFS.ESM_IMPORT_META_URL! < 'file;';
46+
1447
const getScriptSrc = (): string | undefined => {
1548
// if Nodejs, return undefined
1649
if (isNode) {
@@ -26,9 +59,22 @@ const getScriptSrc = (): string | undefined => {
2659
// new URL('actual-bundle-name.js', import.meta.url).href
2760
// ```
2861
// So that bundler can preprocess the URL correctly.
29-
if (BUILD_DEFS.ESM_IMPORT_META_URL?.startsWith('file:')) {
62+
if (isEsmImportMetaUrlHardcodedAsFileUri) {
3063
// if the rewritten URL is a relative path, we need to use the origin to resolve the URL.
31-
return new URL(new URL(BUILD_DEFS.BUNDLE_FILENAME, BUILD_DEFS.ESM_IMPORT_META_URL).href, origin).href;
64+
65+
// The following is a workaround for Vite.
66+
//
67+
// Vite uses a bundler(rollup/rolldown) that does not rewrite `import.meta.url` to a file URL. So in theory, this
68+
// code path should not be executed in Vite. However, the bundler does not know it and it still try to load the
69+
// following pattern:
70+
// - `return new URL('filename', import.meta.url).href`
71+
//
72+
// By replacing the pattern above with the following code, we can skip the resource loading behavior:
73+
// - `const URL2 = URL; return new URL2('filename', import.meta.url).href;`
74+
//
75+
// And it still works in Webpack.
76+
const URL2 = URL;
77+
return new URL(new URL2(BUILD_DEFS.BUNDLE_FILENAME, BUILD_DEFS.ESM_IMPORT_META_URL).href, origin).href;
3278
}
3379

3480
return BUILD_DEFS.ESM_IMPORT_META_URL;

js/web/script/build.ts

+14-5
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,17 @@ async function minifyWasmModuleJsForBrowser(filepath: string): Promise<string> {
123123
// ```
124124
// with:
125125
// ```
126-
// new Worker(import.meta.url.startsWith('file:')
127-
// ? new URL(BUILD_DEFS.BUNDLE_FILENAME, import.meta.url)
128-
// : new URL(import.meta.url), ...
126+
// new Worker((() => {
127+
// const URL2 = URL;
128+
// return import.meta.url > 'file:' && import.meta.url < 'file;'
129+
// ? new URL2(BUILD_DEFS.BUNDLE_FILENAME, import.meta.url)
130+
// : new URL(import.meta.url);
131+
// })(), ...
129132
// ```
130133
//
131134
// NOTE: this is a workaround for some bundlers that does not support runtime import.meta.url.
132-
// TODO: in emscripten 3.1.61+, need to update this code.
135+
//
136+
// Check more details in the comment of `isEsmImportMetaUrlHardcodedAsFileUri()` and `getScriptSrc()` in file `lib/wasm/wasm-utils-import.ts`.
133137

134138
// First, check if there is exactly one occurrence of "new Worker(new URL(import.meta.url)".
135139
const matches = [...contents.matchAll(/new Worker\(new URL\(import\.meta\.url\),/g)];
@@ -142,7 +146,12 @@ async function minifyWasmModuleJsForBrowser(filepath: string): Promise<string> {
142146
// Replace the only occurrence.
143147
contents = contents.replace(
144148
/new Worker\(new URL\(import\.meta\.url\),/,
145-
`new Worker(import.meta.url.startsWith('file:')?new URL(BUILD_DEFS.BUNDLE_FILENAME, import.meta.url):new URL(import.meta.url),`,
149+
`new Worker((() => {
150+
const URL2 = URL;
151+
return (import.meta.url > 'file:' && import.meta.url < 'file;')
152+
? new URL2(BUILD_DEFS.BUNDLE_FILENAME, import.meta.url)
153+
: new URL(import.meta.url);
154+
})(),`,
146155
);
147156

148157
// Use terser to minify the code with special configurations:

js/web/test/e2e/exports/main.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
'use strict';
55

6-
const { runDevTest, runProdTest } = require('./test');
6+
const { runDevTest, runProdTest, verifyAssets } = require('./test');
77
const { installOrtPackages } = require('./utils');
88

99
/**
@@ -29,5 +29,14 @@ module.exports = async function main(PRESERVE, PACKAGES_TO_INSTALL) {
2929

3030
await runDevTest('vite-default', '\x1b[32m➜\x1b[39m \x1b[1mLocal\x1b[22m:', 5173);
3131
await runProdTest('vite-default', '\x1b[32m➜\x1b[39m \x1b[1mLocal\x1b[22m:', 4173);
32+
33+
await verifyAssets('vite-default', async (cwd) => {
34+
const globby = await import('globby');
35+
36+
return {
37+
test: 'File "dist/assets/**/ort.*.mjs" should not exist',
38+
success: globby.globbySync('dist/assets/**/ort.*.mjs', { cwd }).length === 0,
39+
};
40+
});
3241
}
3342
};

js/web/test/e2e/exports/test.js

+22
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,29 @@ async function runProdTest(testCaseName, ready, port) {
121121
await runTest(testCaseName, ['prod'], ready, 'npm run start', port);
122122
}
123123

124+
async function verifyAssets(testCaseName, testers) {
125+
testers = Array.isArray(testers) ? testers : [testers];
126+
const wd = path.join(__dirname, 'testcases', testCaseName);
127+
128+
console.log(`[${testCaseName}] Verifying assets...`);
129+
130+
const testResults = [];
131+
132+
try {
133+
for (const tester of testers) {
134+
testResults.push(await tester(wd));
135+
}
136+
137+
if (testResults.some((r) => !r.success)) {
138+
throw new Error(`[${testCaseName}] asset verification failed.`);
139+
}
140+
} finally {
141+
console.log(`[${testCaseName}] asset verification result:`, testResults);
142+
}
143+
}
144+
124145
module.exports = {
125146
runDevTest,
126147
runProdTest,
148+
verifyAssets,
127149
};

0 commit comments

Comments
 (0)