mirror of
https://github.com/saymrwulf/onnxruntime.git
synced 2026-05-17 21:10:43 +00:00
### Description This PR make numbers of optimizations to onnxruntime-web's module export and deployment. See each section below for more details. #### Preview > [onnxruntime-web@1.19.0-esmtest.20240513-a16cd2bd21](https://www.npmjs.com/package/onnxruntime-web/v/1.19.0-esmtest.20240513-a16cd2bd21) > ~~onnxruntime-web@1.19.0-esmtest.20240430-c7edbcc63d~~ > ~~onnxruntime-web@1.18.0-esmtest.20240428-624c681c83~~ > ~~onnxruntime-web@1.18.0-esmtest.20240411-1abb64e894~~ <details> <summary><h4>Breaking changes</h4></summary> There is no code change required, but there are a few differences regarding **code import**, **flags**, **bundler config** and **deployment steps**. #### Importing: Import table is changed. See following for details. <details> <summary><h5>Current import table:</h5></summary> | Target Name | Path for "import" or "require" | WebGL | JSEP | wasm | Proxy | Training | |------|-----|-----|-----|-----|-----|-----| | `ort` (default) | `onnxruntime-web` | ✔️ | ❌ | ✔️ | ✔️ | ❌ | | `ort.all` | `onnxruntime-web/experimental` | ✔️ | ✔️ | ✔️ | ✔️ | ❌ | | `ort.node` | `onnxruntime-web` | ❌ | ❌ | ✔️ | ❌ | ❌ | | `ort.training` | `onnxruntime-web/training` | ❌ | ❌ | ✔️ | ✔️<sup>\[1]</sup> | ✔️ | | `ort.wasm` | `onnxruntime-web/wasm` | ❌ | ❌ | ✔️ | ✔️ | ❌ | | `ort.wasm-core` | `onnxruntime-web/wasm-core` | ❌ | ❌ | ✔️ | ❌ | ❌ | | `ort.webgl` | `onnxruntime-web/webgl` | ✔️ | ❌ | ❌ | ✔️<sup>\[2]</sup> | ❌ | | `ort.webgpu` | `onnxruntime-web/webgpu` | ❌ | ✔️ | ✔️ | ✔️ | ❌ | * [1] didn't test. may not actually work. * [2] not working. this is a mistake in build config. </details> <details> <summary><h5>Proposed update:</h5></summary> | Target Name | Path for "import" or "require" | WebGL | JSEP | wasm | Proxy | Training | |------|-----|-----|-----|-----|-----|-----| | `ort` (default) | `onnxruntime-web` | ✔️ | ❌ | ✔️ | ✔️ | ❌ | | `ort.all` | ~~`onnxruntime-web/experimental`~~<br/>`onnxruntime-web/all` | ✔️ | ✔️ | ✔️ | ✔️ | ❌ | | `ort.node` | `onnxruntime-web` | ❌ | ❌ | ✔️ | ❌ | ❌ | | `ort.training` | `onnxruntime-web/training` | ❌ | ❌ | ✔️ | ✔️ | ✔️ | | `ort.wasm` | `onnxruntime-web/wasm` | ❌ | ❌ | ✔️ | ✔️ | ❌ | | ~~`ort.wasm-core`~~ | ~~`onnxruntime-web/wasm-core`~~ | ~~❌~~ | ~~❌~~ | ~~✔️~~ | ~~❌~~ | ~~❌~~ | | `ort.webgl` | `onnxruntime-web/webgl` | ✔️ | ❌ | ❌ | ~~✔️~~ ❌ | ❌ | | `ort.webgpu` | `onnxruntime-web/webgpu` | ❌ | ✔️ | ✔️ | ✔️ | ❌ | </details> #### Flags: The following flags are deprecated: - `env.wasm.simd` (boolean): will be ignored. SIMD is always enabled in build. The following flags changed their type: - `env.wasm.wasmPaths`: When using this flag as a string ( for the URL prefix ), nothing is changed. When using this flag as an object ( for per-file path override ), the type changed: ```diff - export interface Old_WasmFilePaths{ - 'ort-wasm.wasm'?: string; - 'ort-wasm-threaded.wasm'?: string; - 'ort-wasm-simd.wasm'?: string; - 'ort-training-wasm-simd.wasm'?: string; - 'ort-wasm-simd-threaded.wasm'?: string; - }; + export interface New_WasmFilePaths { + /** + * Specify the override path for the main .wasm file. + * + * This path should be an absolute path. + * + * If not modified, the filename of the .wasm file is: + * - `ort-wasm-simd-threaded.wasm` for default build + * - `ort-wasm-simd-threaded.jsep.wasm` for JSEP build (with WebGPU and WebNN) + * - `ort-training-wasm-simd-threaded.wasm` for training build + */ + wasm?: URL|string; + /** + * Specify the override path for the main .mjs file. + * + * This path should be an absolute path. + * + * If not modified, the filename of the .mjs file is: + * - `ort-wasm-simd-threaded.mjs` for default build + * - `ort-wasm-simd-threaded.jsep.mjs` for JSEP build (with WebGPU and WebNN) + * - `ort-training-wasm-simd-threaded.mjs` for training build + */ + mjs?: URL|string; + } ``` #### Bundler compatibility: Config changes are need for bundlers. See usage example in /js/web/test/e2e/ for Webpack, parcel and rollup. #### Deployment: - if consuming from a CDN, there is no breaking change. - if consuming from a local server, need to copy all `ort-*.wasm` and `ort-*.mjs` files (totally 6 files) in the dist folder. (previously only need to copy `ort-*.wasm` files.) </details> <details> <summary><h4>Problems</h4></summary> There are a few problems with the current module export and deployment: - Script URL cannot be correctly inferred when imported as ESM. - Workers are forcefully encoded using Blob URL, which makes onnxruntime-web not working in CSP environment and Node.js, when using proxy or multi-threading feature. - Generated JS code (by Emscripten) is encoded using `function.toString()`, which is unstable and error-prone. - When running with a different Emscripten build, always need the build step. Making it difficult to swap artifacts in deveopment/debug. </details> <details> <summary><h4>Goals</h4></summary> - Full ESM support - Support variances of ways to import. Including: - import from HTML's `<script>` tag (IIFE format, exporting to global variable `ort`) ```html <script src="https://example.com/cdn-path-to-onnxruntime-web/dist/ort.min.js"></script> ``` - import from source code inside `<script type="module">` tag (ESM) ```html <script type="module"> import * as ort from "https://example.com/cdn-path-to-onnxruntime-web/dist/ort.min.mjs"; // using 'ort' </script> ``` - import in a CommonJS project (CJS format, resolve from package.json "exports" field) ```js // myProject/main.js const ort = require('onnxruntime-web'); ``` - import in an ESM project (ESM format, resolve from package.json "exports" field) ```js // myProject/main.js (or main.mjs) import * as ort from 'onnxruntime-web'; ``` - Support popular bundlers when importing onnxruntime-web into a CJS/ESM project. - webpack (esm requires extra post-process step) - rollup - parcel (esm requires extra post-process step) - More bundlers **TBD** - Multi-threading support for Node.js NOTE: keeping single JavaScript file (the all-in-one bundle) is no longer a goal. This is because technically there is a conflict with the other requirements. </details> <details> <summary><h4>Important Design Decisions</h4></summary> - Drop support of single JavaScript output. - The current onnxruntime-web distribution uses a single JavaScript file to include all code. While there are a few benefits, it also creates problems as mentioned above. Since ESM is being used more and more widely, and browsers are making more restricted security checks and requirement, the old Blob based solution is going to be replaced. - To achieve the requirement, specifically, the CSP environment support, we have to offer a non Blob based solution. Therefore, we have to distribute multiple files and drop the single file solution. - Do not run parser/postprocess on Emscripten generated JavaScript. - Emscripten is evolving quickly so we should only depends on what's in its documentation instead of a certain implementation details. (for example, currently we patch on its code to deal with a special variable `_scriptDir`) - Keep the generated files as-is also helps to: - reduce the size of ort.min.js - make it easier to replace build artifacts when in development/debug - Drop support for non-SIMD and non-MultiThread. This helps to reduce the number of artifacts in distribution. - (fixed-sized) SIMD is supported in any mainstream JS environment. - Multi-thread as WebAssembly feature is supported in any mainstream JS environment. In some environment the feature is guarded with cross origin policy, but it can still work if not trying to create any worker. - Use ESM output for Emscripten generated JavaScript. - There are 2 ways to dynamically import classic (umd) modules and neither of them are recommended: - dynamically creating a <script> tag. This changes the HTML structure and have quite a lot of compatibility issue - use `fetch()` and `eval()`. However `eval` is strongly suggested to be avoid because there is a great perf hit. - importing ESM is super easy - just use the `import()` call. Considering ESM is widely supported in modern browsers and Node.js this is the better option. - Add Blob based solution as a fallback for cross-origin workers. - There are still wide use case of importing onnxruntime-web from CDN. In this usage, make it able create worker by using `fetch()`+`Blob` to create a same-origin Blob URL. </details> <details> <summary><h4>Distribution File Manifest</h4></summary> The distribution folder contains the following files: - WebAssembly artifacts. These files are the result of compiling the ONNX Runtime C++ code to WebAssembly by Emscripten. | File Name | Build Flags | |------|-----| | ort-wasm-simd-threaded.mjs <br/> ort-wasm-simd-threaded.wasm | `--enable_wasm_simd` <br/> `--enable_wasm_threads` | | ort-training-wasm-simd-threaded.mjs <br/> ort-training-wasm-simd-threaded.wasm | `--enable_training_apis` <br/> `--enable_wasm_simd` <br/> `--enable_wasm_threads` | | ort-wasm-simd-threaded.jsep.mjs <br/> ort-wasm-simd-threaded.jsep.wasm | `--enable_wasm_simd` <br/> `--enable_wasm_threads` <br/> `--use_jsep` <br/> `--use_webnn` | - onnxruntime-web JavaScript artifacts. These files are generated by ESBuild as the entry point for onnxruntime-web. There are multiple build targets for different use cases: | Target Name | Path for "import" or "require" | Description | |------|-----|-----| | `ort` | `onnxruntime-web` | The default target. | | `ort.all` | `onnxruntime-web/all` | The target including webgl. | | `ort.node` | `onnxruntime-web` | The default target for Node.js. | | `ort.training` | `onnxruntime-web/training` | The target including training APIs | | `ort.wasm` | `onnxruntime-web/wasm` | The target including only WebAssembly (CPU) EP | | `ort.webgl` | `onnxruntime-web/webgl` | The target including only WebGL EP | For each target, there are multiple files generated: | File Name | Description | |------|-----| | [target].js | The entry point for the target. IIFE and CommonJS format. | | [target].mjs | The entry point for the target. ESM format. | | [target].min.js <br/> [target].min.js.map | The entry point for the target. Minimized with sourcemap. IIFE and CommonJS format. | | [target].min.mjs <br/> [target].min.mjs.map | The entry point for the target. Minimized with sourcemap. ESM format. | | [target].proxy.mjs | (if appliable) The proxy ESM module for the target. | | [target].proxy.min.mjs <br/> [target].proxy.min.mjs.map | (if appliable) The proxy ESM module for the target. Minimized with sourcemap. | </details> <details> <summary><h4>Dynamic Import Explained</h4></summary> - Local Served | No Proxy: ``` [Bundle or ort.min.js] | + import()--> [ort-wasm-simd-threaded.mjs] | + WebAssembly.instantiateStreaming()--> [ort-wasm-simd-threaded.wasm] | + new Worker()--> [ort-wasm-simd-threaded.mjs (worker)] | + WebAssembly.instantiateStreaming()--> [ort-wasm-simd-threaded.wasm] ``` - Local Served | Proxy: ``` [Bundle or ort.min.js] | + import()--> [ort.proxy.min.mjs] | + new Worker()--> [ort.proxy.min.mjs (worker)] | + import()--> [ort-wasm-simd-threaded.mjs] | + WebAssembly.instantiateStreaming()--> [ort-wasm-simd-threaded.wasm] | + new Worker()--> [ort-wasm-simd-threaded.mjs (worker)] | + WebAssembly.instantiateStreaming()--> [ort-wasm-simd-threaded.wasm] ``` - Cross Origin | No Proxy: ``` [Bundle or ort.min.js] | + fetch('ort-wasm-simd-threaded.mjs') | + URL.createObjectURL(res.blob()) | + import()--> [blob:... (ort-wasm-simd-threaded)] | + WebAssembly.instantiateStreaming()--> [ort-wasm-simd-threaded.wasm] | + new Worker()--> [blob:... (ort-wasm-simd-threaded) (worker)] | + WebAssembly.instantiateStreaming()--> [ort-wasm-simd-threaded.wasm] ``` - Cross Origin | Proxy ``` [Bundle or ort.min.js] | + fetch('ort.proxy.min.mjs') | + URL.createObjectURL(res.blob()) | + import()--> [blob:... (ort.proxy)] | + new Worker()--> [blob:... (ort.proxy) (worker)] | + fetch('ort-wasm-simd-threaded.mjs') | + URL.createObjectURL(res.blob()) | + import()--> [blob:... (ort-wasm-simd-threaded)] | + WebAssembly.instantiateStreaming()--> [ort-wasm-simd-threaded.wasm] | + new Worker()--> [blob:... (ort-wasm-simd-threaded) (worker)] | + WebAssembly.instantiateStreaming()--> [ort-wasm-simd-threaded.wasm] ``` </details>
513 lines
17 KiB
TypeScript
513 lines
17 KiB
TypeScript
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// Licensed under the MIT License.
|
|
|
|
import * as esbuild from 'esbuild';
|
|
import minimist from 'minimist';
|
|
import * as fs from 'node:fs/promises';
|
|
import * as path from 'node:path';
|
|
import {SourceMapConsumer, SourceMapGenerator} from 'source-map';
|
|
|
|
console.time('BUILD');
|
|
|
|
/**
|
|
* @summary Build script for ort-web using esbuild.
|
|
*/
|
|
|
|
const args = minimist(process.argv.slice(2));
|
|
/**
|
|
* --bundle-mode=prod (default)
|
|
* Build multiple ort-web bundles for production.
|
|
*
|
|
* --bundle-mode=dev
|
|
* Build a single ort-web bundle for development, and a test bundle.
|
|
*
|
|
* --bundle-mode=perf
|
|
* Build a single ort-web bundle for performance test, and a test bundle.
|
|
*
|
|
* --bundle-mode=node
|
|
* Build a single ort-web bundle for nodejs.
|
|
*/
|
|
const BUNDLE_MODE: 'prod'|'dev'|'perf'|'node' = args['bundle-mode'] || 'prod';
|
|
|
|
/**
|
|
* --debug
|
|
* Enable debug mode. In this mode, esbuild metafile feature will be enabled. Simple bundle analysis will be printed.
|
|
*
|
|
* --debug=verbose
|
|
* Enable debug mode. In this mode, esbuild metafile feature will be enabled. Detailed bundle analysis will be
|
|
* printed.
|
|
*
|
|
* --debug=save
|
|
* Enable debug mode. In this mode, esbuild metafile feature will be enabled. Full bundle analysis will be saved to a
|
|
* file as JSON.
|
|
*/
|
|
const DEBUG = args.debug; // boolean|'verbose'|'save'
|
|
|
|
/**
|
|
* Root folder of the source code: `<ORT_ROOT>/js/`
|
|
*/
|
|
const SOURCE_ROOT_FOLDER = path.join(__dirname, '../..');
|
|
|
|
/**
|
|
* Default define values for the build.
|
|
*/
|
|
const DEFAULT_DEFINE = {
|
|
'BUILD_DEFS.DISABLE_WEBGL': 'false',
|
|
'BUILD_DEFS.DISABLE_JSEP': 'false',
|
|
'BUILD_DEFS.DISABLE_WASM': 'false',
|
|
'BUILD_DEFS.DISABLE_WASM_PROXY': 'false',
|
|
'BUILD_DEFS.DISABLE_TRAINING': 'true',
|
|
|
|
'BUILD_DEFS.IS_ESM': 'false',
|
|
'BUILD_DEFS.ESM_IMPORT_META_URL': 'undefined',
|
|
} as const;
|
|
|
|
const COPYRIGHT_HEADER = `/*!
|
|
* ONNX Runtime Web v${require('../package.json').version}
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License.
|
|
*/`;
|
|
|
|
interface OrtBuildOptions {
|
|
readonly isProduction?: boolean;
|
|
readonly isNode?: boolean;
|
|
readonly format: 'iife'|'cjs'|'esm';
|
|
readonly outputName: string;
|
|
readonly define?: Record<string, string>;
|
|
}
|
|
|
|
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);
|
|
const storedBuildOptions = esbuildAlreadyBuilt.get(options.outfile!);
|
|
if (storedBuildOptions) {
|
|
if (serializedOptions !== storedBuildOptions) {
|
|
throw new Error(`Inconsistent build options for "${options.outfile!}".`);
|
|
}
|
|
console.log(`Already built "${options.outfile!}", skipping...`);
|
|
return;
|
|
} else {
|
|
esbuildAlreadyBuilt.set(options.outfile!, serializedOptions);
|
|
console.log(`Building "${options.outfile!}"...`);
|
|
}
|
|
|
|
// Patch banner:
|
|
//
|
|
// - Add copy right header.
|
|
// - For Node + ESM, add a single line fix to make it work.
|
|
// (see: https://github.com/evanw/esbuild/pull/2067#issuecomment-1981642558)
|
|
const NODE_ESM_FIX_MIN = 'import{createRequire}from"module";const require=createRequire(import.meta.url);';
|
|
const banner = {
|
|
js: options.platform === 'node' && options.format === 'esm' ? COPYRIGHT_HEADER + '\n' + NODE_ESM_FIX_MIN :
|
|
COPYRIGHT_HEADER
|
|
};
|
|
|
|
// Patch footer:
|
|
//
|
|
// For IIFE format, add a custom footer to make it compatible with CommonJS module system.
|
|
// For other formats, no footer is needed.
|
|
//
|
|
// ESBuild does not support UMD format (which is a combination of IIFE and CommonJS). We don't want to generate 2
|
|
// build targets (IIFE and CommonJS) because it will increase the package size. Instead, we generate IIFE and append
|
|
// this footer to make it compatible with CommonJS module system.
|
|
//
|
|
// see also: https://github.com/evanw/esbuild/issues/507
|
|
//
|
|
const COMMONJS_FOOTER_MIN = 'typeof exports=="object"&&typeof module=="object"&&(module.exports=ort);';
|
|
const footer = options.format === 'iife' ? {js: COMMONJS_FOOTER_MIN} : undefined;
|
|
|
|
// set BUILD_DEFS for ESM.
|
|
if (options.format === 'esm') {
|
|
options.define = {
|
|
...options.define,
|
|
'BUILD_DEFS.ESM_IMPORT_META_URL': 'import.meta.url',
|
|
'BUILD_DEFS.IS_ESM': 'true',
|
|
};
|
|
}
|
|
|
|
const result = await esbuild.build({
|
|
logLevel: DEBUG ? (DEBUG === 'verbose' || DEBUG === 'save' ? 'verbose' : 'debug') : 'info',
|
|
metafile: !!DEBUG,
|
|
absWorkingDir: SOURCE_ROOT_FOLDER,
|
|
bundle: true,
|
|
banner,
|
|
footer,
|
|
...options
|
|
});
|
|
if (DEBUG) {
|
|
if (DEBUG === 'save') {
|
|
await fs.writeFile(
|
|
`${path.basename(options.outfile!)}.esbuild.metafile.json`, JSON.stringify(result.metafile!, null, 2));
|
|
} else {
|
|
console.log(await esbuild.analyzeMetafile(result.metafile!, {verbose: DEBUG === 'verbose'}));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build one ort-web target.
|
|
*
|
|
* The distribution code is split into multiple files:
|
|
* - [output-name][.min].[m]js
|
|
* - ort[-training]-wasm-simd-threaded[.jsep].mjs
|
|
*/
|
|
async function buildOrt({
|
|
isProduction = false,
|
|
isNode = false,
|
|
format,
|
|
outputName,
|
|
define = DEFAULT_DEFINE,
|
|
}: OrtBuildOptions) {
|
|
const platform = isNode ? 'node' : 'browser';
|
|
const external =
|
|
isNode ? ['onnxruntime-common'] : ['node:fs/promises', 'node:fs', 'node:os', 'module', 'worker_threads'];
|
|
const defineOverride: Record<string, string> = {};
|
|
if (!isNode) {
|
|
defineOverride.process = 'undefined';
|
|
defineOverride['globalThis.process'] = 'undefined';
|
|
}
|
|
|
|
await buildBundle({
|
|
entryPoints: ['web/lib/index.ts'],
|
|
outfile: `web/dist/${outputName}${isProduction ? '.min' : ''}.${format === 'esm' ? 'mjs' : 'js'}`,
|
|
platform,
|
|
format,
|
|
globalName: 'ort',
|
|
external,
|
|
define: {...define, ...defineOverride},
|
|
sourcemap: isProduction ? 'linked' : 'inline',
|
|
minify: isProduction,
|
|
});
|
|
}
|
|
|
|
async function buildTest() {
|
|
const isProduction = BUNDLE_MODE === 'perf';
|
|
|
|
await buildBundle({
|
|
absWorkingDir: path.join(SOURCE_ROOT_FOLDER, 'web/test'),
|
|
|
|
entryPoints: ['test-main.ts'],
|
|
outfile: isProduction ? 'ort.test.min.js' : 'ort.test.js',
|
|
platform: 'browser',
|
|
format: 'iife',
|
|
define: DEFAULT_DEFINE,
|
|
sourcemap: isProduction ? false : 'inline',
|
|
sourceRoot: path.join(SOURCE_ROOT_FOLDER, 'web/test'),
|
|
external: ['../../node'],
|
|
plugins: [
|
|
// polyfill nodejs modules
|
|
require('esbuild-plugin-polyfill-node').polyfillNode({globals: false}),
|
|
// make "ort" external
|
|
{
|
|
name: 'make-ort-external',
|
|
setup(build: esbuild.PluginBuild) {
|
|
build.onResolve(
|
|
{filter: /^onnxruntime-common$/},
|
|
_args => ({path: 'onnxruntime-common', namespace: 'make-ort-external'}));
|
|
build.onLoad(
|
|
{filter: /.*/, namespace: 'make-ort-external'},
|
|
_args => ({contents: 'module.exports = globalThis.ort;'}));
|
|
}
|
|
}
|
|
],
|
|
minify: isProduction,
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Perform the post-process step after ESBuild finishes the build.
|
|
*
|
|
* This is a custom post process step to insert magic comments to a specific import call:
|
|
* ```
|
|
* ... await import(...
|
|
* ```
|
|
* to:
|
|
* ```
|
|
* ... await import(/* webpackIgnore: true *\/...
|
|
* ```
|
|
*
|
|
* Why we need this?
|
|
*
|
|
* If a project uses Webpack to bundle the code, Webpack will try to resolve the dynamic import calls. However, we don't
|
|
* want Webpack to resolve the dynamic import calls inside the ort-web bundle because:
|
|
*
|
|
* - We want to keep the ort-*.mjs and ort-*.wasm as-is. This makes it able to replace the ort-*.mjs and ort-*.wasm with
|
|
* a custom build if needed.
|
|
* - The Emscripten generated code uses `require()` to load Node.js modules. Those code is guarded by a feature check to
|
|
* make sure only run in Node.js. Webpack does not recognize the feature check and will try to resolve the `require()`
|
|
* in browser environment. This will cause the Webpack build to fail.
|
|
* - There are multiple entry points that use dynamic import to load the ort-*.mjs and ort-*.wasm. If the content of the
|
|
* dynamic import is resolved by Webpack, it will be duplicated in the final bundle. This will increase the bundle size.
|
|
*
|
|
* What about other bundlers?
|
|
*
|
|
* TBD
|
|
*
|
|
*/
|
|
async function postProcess() {
|
|
const IMPORT_MAGIC_COMMENT = '/*webpackIgnore:true*/';
|
|
const IMPORT_ORIGINAL = 'await import(';
|
|
const IMPORT_NEW = `await import(${IMPORT_MAGIC_COMMENT}`;
|
|
|
|
const files = await fs.readdir(path.join(SOURCE_ROOT_FOLDER, 'web/dist'));
|
|
for (const file of files) {
|
|
// only process on "ort.*.min.js" and "ort.*.min.mjs" files.
|
|
if ((file.endsWith('.min.js') || file.endsWith('.min.mjs')) && file.startsWith('ort.')) {
|
|
const jsFilePath = path.join(SOURCE_ROOT_FOLDER, 'web/dist', file);
|
|
const sourcemapFilePath = jsFilePath + '.map';
|
|
|
|
const originalJsFileSize = (await fs.stat(jsFilePath)).size;
|
|
|
|
if (!files.includes(file + '.map')) {
|
|
continue;
|
|
}
|
|
|
|
const jsFileLines = (await fs.readFile(jsFilePath, 'utf-8')).split('\n');
|
|
|
|
let line = -1, column = -1, found = false;
|
|
for (let i = 0; i < jsFileLines.length; i++) {
|
|
const importColumnIndex = jsFileLines[i].indexOf(IMPORT_ORIGINAL);
|
|
if (importColumnIndex !== -1) {
|
|
if (found || importColumnIndex !== jsFileLines[i].lastIndexOf(IMPORT_ORIGINAL)) {
|
|
throw new Error(`Multiple dynamic import calls found in "${jsFilePath}". Should not happen.`);
|
|
}
|
|
line = i + 1;
|
|
column = importColumnIndex + IMPORT_ORIGINAL.length;
|
|
jsFileLines[i] = jsFileLines[i].replace(IMPORT_ORIGINAL, IMPORT_NEW);
|
|
found = true;
|
|
}
|
|
}
|
|
if (!found) {
|
|
if (file.includes('webgl')) {
|
|
// skip webgl
|
|
continue;
|
|
}
|
|
throw new Error(`Dynamic import call not found in "${jsFilePath}". Should not happen.`);
|
|
}
|
|
|
|
const originalSourcemapString = await fs.readFile(sourcemapFilePath, 'utf-8');
|
|
await SourceMapConsumer.with(originalSourcemapString, null, async (consumer) => {
|
|
// create new source map and set source content
|
|
const updatedSourceMap = new SourceMapGenerator();
|
|
for (const source of consumer.sources) {
|
|
const content = consumer.sourceContentFor(source);
|
|
if (!content) {
|
|
throw new Error(`Source content not found for source "${source}".`);
|
|
}
|
|
updatedSourceMap.setSourceContent(source, content);
|
|
}
|
|
|
|
consumer.eachMapping((mapping) => {
|
|
if (mapping.generatedLine === line && mapping.generatedColumn >= column) {
|
|
mapping.generatedColumn += IMPORT_MAGIC_COMMENT.length;
|
|
}
|
|
|
|
updatedSourceMap.addMapping({
|
|
generated: {line: mapping.generatedLine, column: mapping.generatedColumn},
|
|
source: mapping.source,
|
|
original: {line: mapping.originalLine, column: mapping.originalColumn},
|
|
name: mapping.name,
|
|
});
|
|
});
|
|
|
|
const updatedSourcemapString = updatedSourceMap.toString();
|
|
|
|
// perform simple validation
|
|
const originalSourcemap = JSON.parse(originalSourcemapString);
|
|
const updatedSourcemap = JSON.parse(updatedSourcemapString);
|
|
|
|
if (originalSourcemap.sources.length !== updatedSourcemap.sources.length ||
|
|
originalSourcemap.sourcesContent.length !== updatedSourcemap.sourcesContent.length ||
|
|
new Set(originalSourcemap.names).size !== new Set(updatedSourcemap.names).size) {
|
|
throw new Error('Failed to update source map: source map length mismatch.');
|
|
}
|
|
const originalMappingsCount = originalSourcemap.mappings.split(/[;,]/);
|
|
const updatedMappingsCount = updatedSourcemap.mappings.split(/[;,]/);
|
|
if (originalMappingsCount.length !== updatedMappingsCount.length) {
|
|
throw new Error('Failed to update source map: mappings count mismatch.');
|
|
}
|
|
|
|
await fs.writeFile(sourcemapFilePath, updatedSourcemapString);
|
|
});
|
|
|
|
await fs.writeFile(jsFilePath, jsFileLines.join('\n'));
|
|
const newJsFileSize = (await fs.stat(jsFilePath)).size;
|
|
if (newJsFileSize - originalJsFileSize !== IMPORT_MAGIC_COMMENT.length) {
|
|
throw new Error(`Failed to insert magic comment to file "${file}". Original size: ${
|
|
originalJsFileSize}, New size: ${newJsFileSize}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function validate() {
|
|
const files = await fs.readdir(path.join(SOURCE_ROOT_FOLDER, 'web/dist'));
|
|
for (const file of files) {
|
|
// validate on all "ort.*.min.js" and "ort.*.min.mjs" files.
|
|
if ((file.endsWith('.js') || file.endsWith('.mjs')) && file.startsWith('ort.')) {
|
|
const isMinified = file.endsWith('.min.js') || file.endsWith('.min.mjs');
|
|
const content = await fs.readFile(path.join(SOURCE_ROOT_FOLDER, 'web/dist', file), 'utf-8');
|
|
|
|
if (isMinified) {
|
|
// all files should not contain BUILD_DEFS definition. BUILD_DEFS should be defined in the build script only.
|
|
//
|
|
// If the final bundle contains BUILD_DEFS definition, it means the build script is not working correctly. In
|
|
// this case, we should fix the build script (this file).
|
|
//
|
|
if (content.includes('BUILD_DEFS')) {
|
|
throw new Error(`Validation failed: "${file}" contains BUILD_DEFS definition.`);
|
|
}
|
|
}
|
|
|
|
// all files should contain the magic comment to ignore dynamic import calls.
|
|
//
|
|
if (!file.includes('webgl') && !file.startsWith('ort.esm.')) {
|
|
const contentToSearch = isMinified ? '/*webpackIgnore:true*/' : '/* webpackIgnore: true */';
|
|
if (!content.includes(contentToSearch)) {
|
|
throw new Error(`Validation failed: "${file}" does not contain magic comment.`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
console.timeLog('BUILD', 'Start building ort-web bundles...');
|
|
|
|
/**
|
|
* add all 6 build tasks for web bundles. Includes:
|
|
* - IIFE/CJS, debug: [name].js
|
|
* - IIFE/CJS, production: [name].min.js
|
|
* - ESM, debug: [name].mjs
|
|
* - ESM, production: [name].min.mjs
|
|
*/
|
|
const addAllWebBuildTasks = async (options: Omit<OrtBuildOptions, 'format'>) => {
|
|
// [name].js
|
|
await buildOrt({
|
|
...options,
|
|
format: 'iife',
|
|
});
|
|
// [name].min.js
|
|
await buildOrt({
|
|
...options,
|
|
outputName: options.outputName,
|
|
format: 'iife',
|
|
isProduction: true,
|
|
});
|
|
// [name].mjs
|
|
await buildOrt({
|
|
...options,
|
|
outputName: options.outputName,
|
|
format: 'esm',
|
|
});
|
|
// [name].min.mjs
|
|
await buildOrt({
|
|
...options,
|
|
outputName: options.outputName,
|
|
format: 'esm',
|
|
isProduction: true,
|
|
});
|
|
};
|
|
|
|
if (BUNDLE_MODE === 'node' || BUNDLE_MODE === 'prod') {
|
|
// ort.node.min.js
|
|
await buildOrt({
|
|
isProduction: true,
|
|
isNode: true,
|
|
format: 'cjs',
|
|
outputName: 'ort.node',
|
|
define: {
|
|
...DEFAULT_DEFINE,
|
|
'BUILD_DEFS.DISABLE_JSEP': 'true',
|
|
'BUILD_DEFS.DISABLE_WEBGL': 'true',
|
|
'BUILD_DEFS.DISABLE_WASM_PROXY': 'true',
|
|
},
|
|
});
|
|
// ort.node.min.mjs
|
|
await buildOrt({
|
|
isProduction: true,
|
|
isNode: true,
|
|
format: 'esm',
|
|
outputName: 'ort.node',
|
|
define: {
|
|
...DEFAULT_DEFINE,
|
|
'BUILD_DEFS.DISABLE_JSEP': 'true',
|
|
'BUILD_DEFS.DISABLE_WEBGL': 'true',
|
|
'BUILD_DEFS.DISABLE_WASM_PROXY': 'true',
|
|
},
|
|
});
|
|
}
|
|
|
|
if (BUNDLE_MODE === 'dev') {
|
|
// ort.all.js
|
|
await buildOrt({outputName: 'ort.all', format: 'iife', define: {...DEFAULT_DEFINE}});
|
|
}
|
|
|
|
if (BUNDLE_MODE === 'perf') {
|
|
// ort.all.min.js
|
|
await buildOrt({
|
|
isProduction: true,
|
|
outputName: 'ort.all',
|
|
format: 'iife',
|
|
});
|
|
}
|
|
|
|
if (BUNDLE_MODE === 'prod') {
|
|
// ort.all[.min].[m]js
|
|
await addAllWebBuildTasks({outputName: 'ort.all'});
|
|
|
|
// ort[.min].[m]js
|
|
await addAllWebBuildTasks({
|
|
outputName: 'ort',
|
|
define: {...DEFAULT_DEFINE, 'BUILD_DEFS.DISABLE_JSEP': 'true'},
|
|
});
|
|
// ort.webgpu[.min].[m]js
|
|
await addAllWebBuildTasks({
|
|
outputName: 'ort.webgpu',
|
|
define: {...DEFAULT_DEFINE, 'BUILD_DEFS.DISABLE_WEBGL': 'true'},
|
|
});
|
|
// ort.wasm[.min].[m]js
|
|
await addAllWebBuildTasks({
|
|
outputName: 'ort.wasm',
|
|
define: {...DEFAULT_DEFINE, 'BUILD_DEFS.DISABLE_JSEP': 'true', 'BUILD_DEFS.DISABLE_WEBGL': 'true'},
|
|
});
|
|
// ort.webgl[.min].[m]js
|
|
await addAllWebBuildTasks({
|
|
outputName: 'ort.webgl',
|
|
define: {
|
|
...DEFAULT_DEFINE,
|
|
'BUILD_DEFS.DISABLE_JSEP': 'true',
|
|
'BUILD_DEFS.DISABLE_WASM': 'true',
|
|
'BUILD_DEFS.DISABLE_WASM_PROXY': 'true',
|
|
},
|
|
});
|
|
// ort.training.wasm[.min].[m]js
|
|
await addAllWebBuildTasks({
|
|
outputName: 'ort.training.wasm',
|
|
define: {
|
|
...DEFAULT_DEFINE,
|
|
'BUILD_DEFS.DISABLE_TRAINING': 'false',
|
|
'BUILD_DEFS.DISABLE_JSEP': 'true',
|
|
'BUILD_DEFS.DISABLE_WEBGL': 'true',
|
|
},
|
|
});
|
|
}
|
|
|
|
if (BUNDLE_MODE === 'dev' || BUNDLE_MODE === 'perf') {
|
|
await buildTest();
|
|
}
|
|
|
|
if (BUNDLE_MODE === 'prod') {
|
|
console.timeLog('BUILD', 'Start post-processing...');
|
|
await postProcess();
|
|
|
|
console.timeLog('BUILD', 'Start validating...');
|
|
await validate();
|
|
}
|
|
|
|
console.timeEnd('BUILD');
|
|
}
|
|
|
|
void main();
|