Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[js/web] make ort-web export compatible with webpack #22196

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 30 additions & 5 deletions js/web/lib/build-def.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,6 @@ interface BuildDefinitions {
* defines whether to disable proxy feature in WebAssembly backend in the build.
*/
readonly DISABLE_WASM_PROXY: boolean;
/**
* defines whether to disable training APIs in WebAssembly backend.
*/
readonly DISABLE_TRAINING: boolean;
/**
* defines whether to disable dynamic importing WASM module in the build.
*/
Expand All @@ -48,9 +44,38 @@ interface BuildDefinitions {
/**
* placeholder for the import.meta.url in ESM. in CJS, this is undefined.
*/
readonly ESM_IMPORT_META_URL: string|undefined;
readonly ESM_IMPORT_META_URL: string | undefined;

// #endregion

/**
* placeholder for the bundle filename.
*
* This is used for bundler compatibility fix when using Webpack with `import.meta.url` inside ESM module.
*
* The default behavior of some bundlers (eg. Webpack) is to rewrite `import.meta.url` to the file local path at
* compile time. This behavior will break the following code:
* ```js
* new Worker(new URL(import.meta.url), { type: 'module' });
* ```
*
* This is because the `import.meta.url` will be rewritten to a local path, so the line above will be equivalent to:
* ```js
* new Worker(new URL('file:///path/to/your/file.js'), { type: 'module' });
* ```
*
* This will cause the browser fails to load the worker script.
*
* To fix this, we need to align with how the bundlers deal with `import.meta.url`:
* ```js
* new Worker(new URL('path-to-bundle.mjs', import.meta.url), { type: 'module' });
* ```
*
* This will make the browser load the worker script correctly.
*
* Since we have multiple bundle outputs, we need to define this placeholder in the build definitions.
*/
readonly BUNDLE_FILENAME: string;
}

declare const BUILD_DEFS: BuildDefinitions;
11 changes: 11 additions & 0 deletions js/web/lib/wasm/proxy-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,17 @@ export const initializeWebAssemblyAndOrtRuntime = async (): Promise<void> => {
proxyWorker.onmessage = onProxyWorkerMessage;
initWasmCallbacks = [resolve, reject];
const message: OrtWasmMessage = { type: 'init-wasm', in: env };
if (BUILD_DEFS.IS_ESM && !message.in!.wasm.wasmPaths && BUILD_DEFS.ESM_IMPORT_META_URL?.startsWith('file:')) {
// if `import.meta.url` is a file URL, it means it is overwriten by the bundler. in this case, unless the
// overrided wasm path is set, we need to use the bundler preferred URL format:
// new URL('filename', import.meta.url)
// so that the bundler can handle the file using corresponding loaders.
message.in!.wasm.wasmPaths = {
wasm: !BUILD_DEFS.DISABLE_JSEP
? new URL('ort-wasm-simd-threaded.jsep.wasm', BUILD_DEFS.ESM_IMPORT_META_URL).href
: new URL('ort-wasm-simd-threaded.wasm', BUILD_DEFS.ESM_IMPORT_META_URL).href,
};
}
proxyWorker.postMessage(message);
temporaryObjectUrl = objectUrl;
} catch (e) {
Expand Down
21 changes: 15 additions & 6 deletions js/web/lib/wasm/wasm-utils-import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,24 @@ export const scriptSrc =
// if Nodejs, return undefined
isNode
? undefined
: // if It's ESM, use import.meta.url
(BUILD_DEFS.ESM_IMPORT_META_URL ??
// use `document.currentScript.src` if available
(typeof document !== 'undefined'
: BUILD_DEFS.IS_ESM // if It's ESM, use import.meta.url
? // For ESM, if the import.meta.url is a file URL, this usually means the bundler rewrites `import.meta.url` to
// the file path at compile time. In this case, this file path cannot be used to determine the runtime URL.
//
// We need to use the URL constructor like this:
// ```js
// new URL('actual-bundle-name.js', import.meta.url).href
// ```
// So that bundler can preprocess the URL correctly.
BUILD_DEFS.ESM_IMPORT_META_URL?.startsWith('file:')
? new URL(BUILD_DEFS.BUNDLE_FILENAME, BUILD_DEFS.ESM_IMPORT_META_URL).href
: BUILD_DEFS.ESM_IMPORT_META_URL
: typeof document !== 'undefined'
? (document.currentScript as HTMLScriptElement)?.src
: // use `self.location.href` if available
typeof self !== 'undefined'
? self.location?.href
: undefined));
: undefined;

/**
* The origin of the current location.
Expand Down Expand Up @@ -117,7 +126,7 @@ export const importProxyWorker = async (): Promise<[undefined | string, Worker]>
}

// If the script source is from the same origin, we can use the embedded proxy module directly.
if (isSameOrigin(scriptSrc)) {
if (BUILD_DEFS.DISABLE_DYNAMIC_IMPORT || isSameOrigin(scriptSrc)) {
return [undefined, createProxyWorker!()];
}

Expand Down
2 changes: 1 addition & 1 deletion js/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
},
"./wasm": {
"node": null,
"import": "./dist/ort.wasm.min.mjs",
"import": "./dist/ort.wasm.bundle.min.mjs",
"require": "./dist/ort.wasm.min.js",
"types": "./types.d.ts"
},
Expand Down
50 changes: 47 additions & 3 deletions js/web/script/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,35 @@ async function minifyWasmModuleJsForBrowser(filepath: string): Promise<string> {
const TIME_TAG = `BUILD:terserMinify:${filepath}`;
console.time(TIME_TAG);

const contents = await fs.readFile(filepath, { encoding: 'utf-8' });
let contents = await fs.readFile(filepath, { encoding: 'utf-8' });

// Replace the following line to create worker:
// ```
// new Worker(new URL(import.meta.url), ...
// ```
// with:
// ```
// new Worker(import.meta.url.startsWith('file:')
// ? new URL(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.

// 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)];
if (matches.length !== 1) {
throw new Error(
`Unexpected number of matches for "new Worker(new URL(import.meta.url)" in "${filepath}": ${matches.length}.`,
);
}

// 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),`,
);

// Find the first and the only occurrence of minified function implementation of "_emscripten_thread_set_strongref":
// ```js
Expand Down Expand Up @@ -265,8 +293,11 @@ async function buildOrt({
const external = isNode
? ['onnxruntime-common']
: ['node:fs/promises', 'node:fs', 'node:os', 'module', 'worker_threads'];
const bundleFilename = `${outputName}${isProduction ? '.min' : ''}.${format === 'esm' ? 'mjs' : 'js'}`;
const plugins: esbuild.Plugin[] = [];
const defineOverride: Record<string, string> = {};
const defineOverride: Record<string, string> = {
'BUILD_DEFS.BUNDLE_FILENAME': JSON.stringify(bundleFilename),
};
if (!isNode) {
defineOverride.process = 'undefined';
defineOverride['globalThis.process'] = 'undefined';
Expand All @@ -285,7 +316,7 @@ async function buildOrt({

await buildBundle({
entryPoints: ['web/lib/index.ts'],
outfile: `web/dist/${outputName}${isProduction ? '.min' : ''}.${format === 'esm' ? 'mjs' : 'js'}`,
outfile: `web/dist/${bundleFilename}`,
platform,
format,
globalName: 'ort',
Expand Down Expand Up @@ -619,6 +650,19 @@ async function main() {
outputName: 'ort.wasm',
define: { ...DEFAULT_DEFINE, 'BUILD_DEFS.DISABLE_JSEP': 'true', 'BUILD_DEFS.DISABLE_WEBGL': 'true' },
});
// ort.wasm.bundle.min.mjs
await buildOrt({
isProduction: true,
outputName: 'ort.wasm.bundle',
format: 'esm',
define: {
...DEFAULT_DEFINE,
'BUILD_DEFS.DISABLE_JSEP': 'true',
'BUILD_DEFS.DISABLE_WEBGL': 'true',
'BUILD_DEFS.DISABLE_DYNAMIC_IMPORT': 'true',
},
});

// ort.webgl[.min].[m]js
await addAllWebBuildTasks({
outputName: 'ort.webgl',
Expand Down
Loading