diff --git a/js/package-lock.json b/js/package-lock.json index 1f8a6a09039d3..548706ee286b7 100644 --- a/js/package-lock.json +++ b/js/package-lock.json @@ -27,6 +27,7 @@ "mocha": "^10.2.0", "npmlog": "^7.0.1", "prettier": "^3.0.3", + "terser": "^5.31.0", "typescript": "^5.2.2" } }, @@ -600,6 +601,64 @@ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@jspm/core": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@jspm/core/-/core-2.0.1.tgz", @@ -1288,6 +1347,12 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -1479,6 +1544,12 @@ "color-support": "bin.js" } }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "node_modules/comment-parser": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.0.tgz", @@ -4172,6 +4243,25 @@ "node": ">=8" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/spdx-correct": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", @@ -4341,6 +4431,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/terser": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.0.tgz", + "integrity": "sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -5009,6 +5117,55 @@ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, + "@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true + }, + "@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "@jspm/core": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@jspm/core/-/core-2.0.1.tgz", @@ -5482,6 +5639,12 @@ "ieee754": "^1.2.1" } }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, "builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -5613,6 +5776,12 @@ "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", "dev": true }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "comment-parser": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.0.tgz", @@ -7603,6 +7772,22 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "spdx-correct": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", @@ -7733,6 +7918,18 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, + "terser": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.0.tgz", + "integrity": "sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==", + "dev": true, + "requires": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + } + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/js/package.json b/js/package.json index 63b7df6ed9de3..308d6931a927c 100644 --- a/js/package.json +++ b/js/package.json @@ -21,6 +21,7 @@ "mocha": "^10.2.0", "npmlog": "^7.0.1", "prettier": "^3.0.3", + "terser": "^5.31.0", "typescript": "^5.2.2" }, "scripts": { diff --git a/js/web/lib/build-def.d.ts b/js/web/lib/build-def.d.ts index 4f30e71d690a3..188aaebc7d187 100644 --- a/js/web/lib/build-def.d.ts +++ b/js/web/lib/build-def.d.ts @@ -32,6 +32,10 @@ interface BuildDefinitions { * defines whether to disable training APIs in WebAssembly backend. */ readonly DISABLE_TRAINING: boolean; + /** + * defines whether to disable dynamic importing WASM module in the build. + */ + readonly DISABLE_DYNAMIC_IMPORT: boolean; // #endregion diff --git a/js/web/lib/wasm/wasm-utils-import.ts b/js/web/lib/wasm/wasm-utils-import.ts index c14941ee6afbe..f80bd7195d456 100644 --- a/js/web/lib/wasm/wasm-utils-import.ts +++ b/js/web/lib/wasm/wasm-utils-import.ts @@ -121,12 +121,28 @@ export const importProxyWorker = async(): Promise<[undefined | string, Worker]> return [url, createProxyWorker!(url)]; }; +/** + * The embedded WebAssembly module. + * + * This is only available in ESM and when embedding is not disabled. + */ +const embeddedWasmModule: EmscriptenModuleFactory|undefined = + BUILD_DEFS.IS_ESM && BUILD_DEFS.DISABLE_DYNAMIC_IMPORT ? + // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires + require( + !BUILD_DEFS.DISABLE_TRAINING ? '../../dist/ort-training-wasm-simd-threaded.mjs' : + !BUILD_DEFS.DISABLE_JSEP ? '../../dist/ort-wasm-simd-threaded.jsep.mjs' : + '../../dist/ort-wasm-simd-threaded.mjs') + .default : + undefined; + /** * Import the WebAssembly module. * * This function will perform the following steps: - * 1. If a preload is needed, it will preload the module and return the object URL. - * 2. Otherwise, it will perform a dynamic import of the module. + * 1. If BUILD_DEFS.DISABLE_DYNAMIC_IMPORT is true, use the embedded module. + * 2. If a preload is needed, it will preload the module and return the object URL. + * 3. Otherwise, it will perform a dynamic import of the module. * * @returns - A promise that resolves to a tuple of 2 elements: * - The object URL of the preloaded module, or undefined if no preload is needed. @@ -135,22 +151,26 @@ export const importProxyWorker = async(): Promise<[undefined | string, Worker]> export const importWasmModule = async( urlOverride: string|undefined, prefixOverride: string|undefined, isMultiThreaded: boolean): Promise<[undefined | string, EmscriptenModuleFactory]> => { - const wasmModuleFilename = !BUILD_DEFS.DISABLE_TRAINING ? 'ort-training-wasm-simd-threaded.mjs' : - !BUILD_DEFS.DISABLE_JSEP ? 'ort-wasm-simd-threaded.jsep.mjs' : - 'ort-wasm-simd-threaded.mjs'; - const wasmModuleUrl = urlOverride ?? normalizeUrl(wasmModuleFilename, prefixOverride); - // need to preload if all of the following conditions are met: - // 1. not in Node.js. - // - Node.js does not have the same origin policy for creating workers. - // 2. multi-threaded is enabled. - // - If multi-threaded is disabled, no worker will be created. So we don't need to preload the module. - // 3. the absolute URL is available. - // - If the absolute URL is failed to be created, the origin cannot be determined. In this case, we will not - // preload the module. - // 4. the worker URL is not from the same origin. - // - If the worker URL is from the same origin, we can create the worker directly. - const needPreload = !isNode && isMultiThreaded && wasmModuleUrl && !isSameOrigin(wasmModuleUrl, prefixOverride); - const url = - needPreload ? (await preload(wasmModuleUrl)) : (wasmModuleUrl ?? fallbackUrl(wasmModuleFilename, prefixOverride)); - return [needPreload ? url : undefined, await dynamicImportDefault>(url)]; + if (BUILD_DEFS.DISABLE_DYNAMIC_IMPORT) { + return [undefined, embeddedWasmModule!]; + } else { + const wasmModuleFilename = !BUILD_DEFS.DISABLE_TRAINING ? 'ort-training-wasm-simd-threaded.mjs' : + !BUILD_DEFS.DISABLE_JSEP ? 'ort-wasm-simd-threaded.jsep.mjs' : + 'ort-wasm-simd-threaded.mjs'; + const wasmModuleUrl = urlOverride ?? normalizeUrl(wasmModuleFilename, prefixOverride); + // need to preload if all of the following conditions are met: + // 1. not in Node.js. + // - Node.js does not have the same origin policy for creating workers. + // 2. multi-threaded is enabled. + // - If multi-threaded is disabled, no worker will be created. So we don't need to preload the module. + // 3. the absolute URL is available. + // - If the absolute URL is failed to be created, the origin cannot be determined. In this case, we will not + // preload the module. + // 4. the worker URL is not from the same origin. + // - If the worker URL is from the same origin, we can create the worker directly. + const needPreload = !isNode && isMultiThreaded && wasmModuleUrl && !isSameOrigin(wasmModuleUrl, prefixOverride); + const url = needPreload ? (await preload(wasmModuleUrl)) : + (wasmModuleUrl ?? fallbackUrl(wasmModuleFilename, prefixOverride)); + return [needPreload ? url : undefined, await dynamicImportDefault>(url)]; + } }; diff --git a/js/web/script/build.ts b/js/web/script/build.ts index 7ef9bb6b70347..eba5efa3f11e0 100644 --- a/js/web/script/build.ts +++ b/js/web/script/build.ts @@ -57,6 +57,7 @@ const DEFAULT_DEFINE = { 'BUILD_DEFS.DISABLE_WASM': 'false', 'BUILD_DEFS.DISABLE_WASM_PROXY': 'false', 'BUILD_DEFS.DISABLE_TRAINING': 'true', + 'BUILD_DEFS.DISABLE_DYNAMIC_IMPORT': 'false', 'BUILD_DEFS.IS_ESM': 'false', 'BUILD_DEFS.ESM_IMPORT_META_URL': 'undefined', @@ -76,7 +77,102 @@ interface OrtBuildOptions { readonly define?: Record; } -const esbuildAlreadyBuilt = new Map(); +const terserAlreadyBuilt = new Map(); + +/** + * This function is only used to minify the Emscripten generated JS code. The ESBuild minify option is not able to + * tree-shake some unused code as expected. Specifically, there are 2 issues: + * 1. the use of `await import("module")` + * 2. the use of `await import("worker_threads")`, with top-level "await". + * + * The 2 code snippets mentioned above are guarded by feature checks to make sure they are only run in Node.js. However, + * ESBuild fails to tree-shake them and will include them in the final bundle. It will generate code like this: + * + * ```js + * // original code (example, not exact generated code) + * var isNode = typeof process !== 'undefined' && process.versions?.node; + * if (isNode) { + * const {createRequire} = await import('module'); + * ... + * } + * + * // minimized code (with setting "define: {'process': 'undefined'}") + * var x=!0;if(x){const{createRequire:rt}=await import("module");...} + * ``` + * + * The remaining dynamic import call makes trouble for further building steps. To solve this issue, we use Terser to + * minify the Emscripten generated JS code. Terser does more aggressive optimizations and is able to tree-shake the + * unused code with special configurations. + * + * We assume the minimized code does not contain any dynamic import calls. + */ +async function minifyWasmModuleJsForBrowser(filepath: string): Promise { + const code = terserAlreadyBuilt.get(filepath); + if (code) { + return code; + } + + const doMinify = (async () => { + const TIME_TAG = `BUILD:terserMinify:${filepath}`; + console.time(TIME_TAG); + + const contents = await fs.readFile(filepath, {encoding: 'utf-8'}); + + // Find the first and the only occurrence of minified function implementation of "_emscripten_thread_set_strongref": + // ```js + // _emscripten_thread_set_strongref: (thread) => { + // if (ENVIRONMENT_IS_NODE) { + // PThread.pthreads[thread].ref(); + // } + // } + // ``` + // + // It is minified to: (example) + // ```js + // function Pb(a){D&&N[a>>>0].ref()} + // ``` + + // The following code will look for the function name and mark the function call as pure, so that Terser will + // minify the code correctly. + + const markedAsPure = []; + // First, try if we are working on the original (not minified) source file. This is when we are working with the + // debug build. + const isOriginal = contents.includes('PThread.pthreads[thread].ref()'); + if (isOriginal) { + markedAsPure.push('PThread.pthreads[thread].ref'); + } else { + // If it is not the original source file, we need to find the minified function call. + const matches = [...contents.matchAll(/\{[_a-zA-Z][_a-zA-Z0-9]*&&([_a-zA-Z][_a-zA-Z0-9]*\[.+?]\.ref)\(\)}/g)]; + if (matches.length !== 1) { + throw new Error(`Unexpected number of matches for minified "PThread.pthreads[thread].ref()" in "${filepath}": ${ + matches.length}.`); + } + // matches[0] is the first and the only match. + // matches[0][0] is the full matched string and matches[0][1] is the first capturing group. + markedAsPure.push(matches[0][1]); + } + + const terser = await import('terser'); + const result = await terser.minify(contents, { + module: true, + compress: { + passes: 2, + global_defs: {'process': undefined, 'globalThis.process': undefined}, + pure_funcs: markedAsPure, + }, + }); + + console.timeEnd(TIME_TAG); + + return result.code!; + })(); + + terserAlreadyBuilt.set(filepath, doMinify); + return doMinify; +} + +const esbuildAlreadyBuilt = new Map(); async function buildBundle(options: esbuild.BuildOptions) { // Skip if the same build options have been built before. const serializedOptions = JSON.stringify(options); @@ -162,18 +258,31 @@ async function buildOrt({ const platform = isNode ? 'node' : 'browser'; const external = isNode ? ['onnxruntime-common'] : ['node:fs/promises', 'node:fs', 'node:os', 'module', 'worker_threads']; + const plugins: esbuild.Plugin[] = []; const defineOverride: Record = {}; if (!isNode) { defineOverride.process = 'undefined'; defineOverride['globalThis.process'] = 'undefined'; } + if (define['BUILD_DEFS.DISABLE_DYNAMIC_IMPORT'] === 'true') { + plugins.push({ + name: 'emscripten-mjs-handler', + setup(build: esbuild.PluginBuild) { + build.onLoad( + {filter: /dist[\\/]ort-.*wasm.*\.mjs$/}, + async args => ({contents: await minifyWasmModuleJsForBrowser(args.path)})); + } + }); + } + await buildBundle({ entryPoints: ['web/lib/index.ts'], outfile: `web/dist/${outputName}${isProduction ? '.min' : ''}.${format === 'esm' ? 'mjs' : 'js'}`, platform, format, globalName: 'ort', + plugins, external, define: {...define, ...defineOverride}, sourcemap: isProduction ? 'linked' : 'inline', @@ -280,8 +389,8 @@ async function postProcess() { } } if (!found) { - if (file.includes('webgl')) { - // skip webgl + if (file.includes('.webgl.') || file.includes('.bundle.')) { + // skip webgl and bundle, they don't have dynamic import calls. continue; } throw new Error(`Dynamic import call not found in "${jsFilePath}". Should not happen.`); @@ -363,7 +472,7 @@ async function validate() { // all files should contain the magic comment to ignore dynamic import calls. // - if (!file.includes('webgl') && !file.startsWith('ort.esm.')) { + if (!file.includes('.webgl.') && !file.includes('.bundle.')) { const contentToSearch = isMinified ? '/*webpackIgnore:true*/' : '/* webpackIgnore: true */'; if (!content.includes(contentToSearch)) { throw new Error(`Validation failed: "${file}" does not contain magic comment.`); @@ -457,17 +566,40 @@ async function main() { if (BUNDLE_MODE === 'prod') { // ort.all[.min].[m]js await addAllWebBuildTasks({outputName: 'ort.all'}); + // ort.all.bundle.min.mjs + await buildOrt({ + isProduction: true, + outputName: 'ort.all.bundle', + format: 'esm', + define: {...DEFAULT_DEFINE, 'BUILD_DEFS.DISABLE_DYNAMIC_IMPORT': 'true'}, + }); // ort[.min].[m]js await addAllWebBuildTasks({ outputName: 'ort', define: {...DEFAULT_DEFINE, 'BUILD_DEFS.DISABLE_JSEP': 'true'}, }); + // ort.bundle.min.mjs + await buildOrt({ + isProduction: true, + outputName: 'ort.bundle', + format: 'esm', + define: {...DEFAULT_DEFINE, 'BUILD_DEFS.DISABLE_JSEP': 'true', 'BUILD_DEFS.DISABLE_DYNAMIC_IMPORT': 'true'}, + }); + // ort.webgpu[.min].[m]js await addAllWebBuildTasks({ outputName: 'ort.webgpu', define: {...DEFAULT_DEFINE, 'BUILD_DEFS.DISABLE_WEBGL': 'true'}, }); + // ort.webgpu.bundle.min.mjs + await buildOrt({ + isProduction: true, + outputName: 'ort.webgpu.bundle', + format: 'esm', + define: {...DEFAULT_DEFINE, 'BUILD_DEFS.DISABLE_WEBGL': 'true', 'BUILD_DEFS.DISABLE_DYNAMIC_IMPORT': 'true'}, + }); + // ort.wasm[.min].[m]js await addAllWebBuildTasks({ outputName: 'ort.wasm', diff --git a/js/web/test/e2e/run-data.js b/js/web/test/e2e/run-data.js index 58371bafd276d..507192f29be9c 100644 --- a/js/web/test/e2e/run-data.js +++ b/js/web/test/e2e/run-data.js @@ -30,6 +30,12 @@ const BROWSER_TEST_CASES = [ [true, true, './browser-test-wasm.js', 'ort.min.mjs', ['num_threads=2', 'proxy=1']], // wasm, 2 threads, proxy [true, true, './browser-test-wasm.js', 'ort.min.mjs', ['num_threads=1', 'proxy=1']], // wasm, 1 thread, proxy + // ort.bundle.min.mjs + [true, false, './browser-test-wasm.js', 'ort.bundle.min.mjs', ['num_threads=1']], // 1 thread + [true, false, './browser-test-wasm.js', 'ort.bundle.min.mjs', ['num_threads=2']], // 2 threads + [true, false, './browser-test-wasm.js', 'ort.bundle.min.mjs', ['num_threads=2', 'proxy=1']], // 2 threads, proxy + [true, false, './browser-test-wasm.js', 'ort.bundle.min.mjs', ['num_threads=1', 'proxy=1']], // 1 thread, proxy + // path override: // wasm, path override filenames for both mjs and wasm, same origin [true, false, './browser-test-wasm-path-override-filename.js', 'ort.min.js', ['port=9876', 'files=mjs,wasm']],