Skip to content
Merged
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
8 changes: 6 additions & 2 deletions js/web/lib/wasm/proxy-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -116,7 +120,7 @@ export const initializeWebAssemblyAndOrtRuntime = async (): Promise<void> => {
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
Expand Down
50 changes: 48 additions & 2 deletions js/web/lib/wasm/wasm-utils-import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
Expand Down
19 changes: 14 additions & 5 deletions js/web/script/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,17 @@ async function minifyWasmModuleJsForBrowser(filepath: string): Promise<string> {
// ```
// 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)];
Expand All @@ -142,7 +146,12 @@ async function minifyWasmModuleJsForBrowser(filepath: string): Promise<string> {
// 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:
Expand Down
11 changes: 10 additions & 1 deletion js/web/test/e2e/exports/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

'use strict';

const { runDevTest, runProdTest } = require('./test');
const { runDevTest, runProdTest, verifyAssets } = require('./test');
const { installOrtPackages } = require('./utils');

/**
Expand All @@ -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 = await import('globby');

return {
test: 'File "dist/assets/**/ort.*.mjs" should not exist',
success: globby.globbySync('dist/assets/**/ort.*.mjs', { cwd }).length === 0,
};
});
}
};
22 changes: 22 additions & 0 deletions js/web/test/e2e/exports/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Loading