mirror of
https://github.com/saymrwulf/onnxruntime.git
synced 2026-05-15 20:50:42 +00:00
452 lines
15 KiB
TypeScript
452 lines
15 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';
|
|
|
|
/**
|
|
* @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'
|
|
|
|
const SOURCE_ROOT_FOLDER = path.join(__dirname, '../..'); // <ORT_ROOT>/js/
|
|
const DEFAULT_DEFINE = {
|
|
'BUILD_DEFS.DISABLE_WEBGL': 'false',
|
|
'BUILD_DEFS.DISABLE_WEBGPU': 'false',
|
|
'BUILD_DEFS.DISABLE_WASM': 'false',
|
|
'BUILD_DEFS.DISABLE_WASM_PROXY': 'false',
|
|
'BUILD_DEFS.DISABLE_WASM_THREAD': 'false',
|
|
'BUILD_DEFS.DISABLE_TRAINING': 'true',
|
|
};
|
|
|
|
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 {
|
|
isProduction?: boolean;
|
|
isNode?: boolean;
|
|
format: 'iife'|'cjs'|'esm';
|
|
outputBundleName: string;
|
|
define?: Record<string, string>;
|
|
}
|
|
|
|
async function buildBundle(options: esbuild.BuildOptions) {
|
|
const result = await esbuild.build({
|
|
logLevel: DEBUG ? (DEBUG === 'verbose' || DEBUG === 'save' ? 'verbose' : 'debug') : 'info',
|
|
metafile: !!DEBUG,
|
|
absWorkingDir: SOURCE_ROOT_FOLDER,
|
|
bundle: true,
|
|
banner: {js: COPYRIGHT_HEADER},
|
|
...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'}));
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
async function minifyCode(sourceCode: string): Promise<string> {
|
|
const result = await esbuild.transform(sourceCode, {
|
|
minify: true,
|
|
legalComments: 'none',
|
|
});
|
|
return result.code;
|
|
}
|
|
|
|
async function buildOrt({
|
|
isProduction = false,
|
|
isNode = false,
|
|
format,
|
|
outputBundleName,
|
|
define = DEFAULT_DEFINE,
|
|
}: OrtBuildOptions) {
|
|
// #region Plugin: resolve ignore imports
|
|
|
|
/**
|
|
* This plugin is used to ignore a few nodejs imports that are not used in the browser. Those imported functions are
|
|
* not really used in the browser because they are usually put behind a feature check. However, esbuild does not know
|
|
* that. It will complain about those imports are not available in the browser.
|
|
*
|
|
* This plugin will ignore those imports and replace them with empty exports.
|
|
*/
|
|
const excludeNodejsImports = {
|
|
name: 'exclude-nodejs-imports',
|
|
setup(build: esbuild.PluginBuild) {
|
|
build.onResolve({filter: /(^node:|^worker_threads$|^fs$|^path$|^perf_hooks$|^os$)/}, args => ({
|
|
namespace: 'nodejs-ignore',
|
|
path: args.path,
|
|
sideEffects: false,
|
|
}));
|
|
build.onLoad({filter: /.*/, namespace: 'nodejs-ignore'}, args => {
|
|
switch (args.path) {
|
|
case 'node:fs/promises':
|
|
case 'node:fs':
|
|
case 'fs':
|
|
return {
|
|
contents: 'export const readFile = undefined;' +
|
|
'export const readFileSync = undefined;' +
|
|
'export const createReadStream = undefined;'
|
|
};
|
|
case 'node:os':
|
|
case 'os':
|
|
return {contents: 'export const cpus = undefined;'};
|
|
case 'node:path':
|
|
case 'path':
|
|
return {contents: 'export const join = undefined;'};
|
|
default:
|
|
return {contents: ''};
|
|
}
|
|
});
|
|
},
|
|
};
|
|
// #endregion
|
|
|
|
// #region Plugin: web assembly multi-thread worker loader
|
|
|
|
/**
|
|
* This plugin is used to load web assembly multi-thread worker code as string.
|
|
*
|
|
* This allows to create the worker from a Blob, so we don't need to create a separate file for the worker.
|
|
*/
|
|
const wasmThreadedHandler = {
|
|
name: 'wasm-threaded-handler',
|
|
setup(build: esbuild.PluginBuild) {
|
|
build.onLoad({filter: /[\\/]ort-wasm-threaded\.worker\.js$/}, async args => {
|
|
let contents = await fs.readFile(args.path, {encoding: 'utf-8'});
|
|
if (isProduction) {
|
|
contents = await minifyCode(contents);
|
|
}
|
|
return {loader: 'text', contents};
|
|
});
|
|
},
|
|
};
|
|
// #endregion
|
|
|
|
// #region Plugin: generated emscripten .js loader
|
|
|
|
/**
|
|
* This plugin is used to patch the generated emscripten .js file for multi-thread build.
|
|
*
|
|
* Since we use inline worker for multi-thread, we make an optimization to use function.toString() to get the
|
|
* implementation of the exported `ortWasmThreaded` function to reduce the size of the bundle. However, the generated
|
|
* function uses a variable `_scriptDir` which is defined inside an IIFE closure. When we use function.toString(), the
|
|
* worker code will throw "_scriptDir is not defined" error.
|
|
*
|
|
* To fix this error, we need to patch the generated code to replace access to `_scriptDir` with `typeof _scriptDir
|
|
* !== "undefined" && _scriptDir`.
|
|
*/
|
|
const emscriptenThreadedJsHandler = {
|
|
name: 'emscripten-threaded-js-handler',
|
|
setup(build: esbuild.PluginBuild) {
|
|
build.onLoad({filter: /ort-wasm.*-threaded.*\.js$/}, async args => {
|
|
let contents = await fs.readFile(args.path, {encoding: 'utf-8'});
|
|
// For debug build, Emscripten generates the following code:
|
|
//
|
|
// if (_scriptDir) {
|
|
// scriptDirectory = _scriptDir;
|
|
// }
|
|
//
|
|
// We replace it with:
|
|
//
|
|
// if (typeof _scriptDir !== "undefined" && _scriptDir) {
|
|
// scriptDirectory = _scriptDir;
|
|
// }
|
|
contents = contents.replace('if (_scriptDir) {', 'if (typeof _scriptDir !== "undefined" && _scriptDir) {');
|
|
|
|
// For release build, Emscripten generates the following code:
|
|
//
|
|
// ...,_scriptDir&&(H=_scriptDir),...
|
|
// We replace it with:
|
|
// ...,(typeof _scriptDir !== "undefined" && _scriptDir)&&(H=_scriptDir),...
|
|
contents =
|
|
contents.replace(/_scriptDir(&&\(.+=_scriptDir\))/, '(typeof _scriptDir !== "undefined" && _scriptDir)$1');
|
|
|
|
return {contents};
|
|
});
|
|
}
|
|
};
|
|
// #endregion
|
|
|
|
// #region Plugin: proxy worker loader
|
|
|
|
/**
|
|
* This plugin is used to load proxy worker code as string.
|
|
*/
|
|
const proxyWorkerHandler = {
|
|
name: 'proxy-worker-handler',
|
|
setup(build: esbuild.PluginBuild) {
|
|
build.onResolve(
|
|
{filter: /proxy-worker\/main$/},
|
|
async args => ({path: args.path, namespace: 'proxy-worker', pluginData: args.resolveDir}));
|
|
|
|
build.onLoad({filter: /.*/, namespace: 'proxy-worker'}, async args => {
|
|
const result = await buildBundle({
|
|
entryPoints: [path.resolve(args.pluginData, args.path)],
|
|
outfile: `web/dist/${outputBundleName}.proxy.js`,
|
|
platform: 'browser',
|
|
plugins: [excludeNodejsImports, wasmThreadedHandler, emscriptenThreadedJsHandler],
|
|
define: {
|
|
...build.initialOptions.define,
|
|
'BUILD_DEFS.DISABLE_WASM_PROXY': 'true',
|
|
},
|
|
sourcemap: isProduction ? false : 'inline',
|
|
minify: isProduction,
|
|
write: false,
|
|
});
|
|
|
|
return {loader: 'text', contents: result.outputFiles![0].text};
|
|
});
|
|
},
|
|
};
|
|
// #endregion
|
|
|
|
await buildBundle({
|
|
entryPoints: ['web/lib/index.ts'],
|
|
outfile: `web/dist/${outputBundleName}.js`,
|
|
platform: isNode ? 'node' : 'browser',
|
|
format,
|
|
globalName: 'ort',
|
|
plugins: isNode ? undefined :
|
|
[excludeNodejsImports, proxyWorkerHandler, wasmThreadedHandler, emscriptenThreadedJsHandler],
|
|
external: isNode ? ['onnxruntime-common'] : undefined,
|
|
define,
|
|
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,
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
// tasks for each esbuild bundle
|
|
const buildTasks: Array<Promise<void>> = [];
|
|
/**
|
|
* add one build task
|
|
*/
|
|
const addBuildTask = async (task: Promise<void>) => {
|
|
if (DEBUG) {
|
|
// in DEBUG mode, build sequentially
|
|
await task;
|
|
} else {
|
|
buildTasks.push(task);
|
|
}
|
|
};
|
|
/**
|
|
* add all 6 build tasks for web bundles. Includes:
|
|
* - IIFE, debug: [name].js
|
|
* - IIFE, production: [name].min.js
|
|
* - CJS, debug: cjs/[name].js
|
|
* - CJS, production: cjs/[name].min.js
|
|
* - ESM, debug: esm/[name].js
|
|
* - ESM, production: esm/[name].min.js
|
|
*/
|
|
const addAllWebBuildTasks = async (options: Omit<OrtBuildOptions, 'format'>) => {
|
|
// [name].js
|
|
await addBuildTask(buildOrt({
|
|
...options,
|
|
format: 'iife',
|
|
}));
|
|
// [name].min.js
|
|
await addBuildTask(buildOrt({
|
|
...options,
|
|
outputBundleName: options.outputBundleName + '.min',
|
|
format: 'iife',
|
|
isProduction: true,
|
|
}));
|
|
// cjs/[name].js
|
|
await addBuildTask(buildOrt({
|
|
...options,
|
|
outputBundleName: 'cjs/' + options.outputBundleName,
|
|
format: 'cjs',
|
|
}));
|
|
// cjs/[name].min.js
|
|
await addBuildTask(buildOrt({
|
|
...options,
|
|
outputBundleName: 'cjs/' + options.outputBundleName + '.min',
|
|
format: 'cjs',
|
|
isProduction: true,
|
|
}));
|
|
// esm/[name].js
|
|
await addBuildTask(buildOrt({
|
|
...options,
|
|
outputBundleName: 'esm/' + options.outputBundleName,
|
|
format: 'esm',
|
|
}));
|
|
// esm/[name].min.js
|
|
await addBuildTask(buildOrt({
|
|
...options,
|
|
outputBundleName: 'esm/' + options.outputBundleName + '.min',
|
|
format: 'esm',
|
|
isProduction: true,
|
|
}));
|
|
};
|
|
|
|
if (BUNDLE_MODE === 'node' || BUNDLE_MODE === 'prod') {
|
|
// ort.node.min.js
|
|
await addBuildTask(buildOrt({
|
|
isProduction: true,
|
|
isNode: true,
|
|
format: 'cjs',
|
|
outputBundleName: 'ort.node.min',
|
|
define: {
|
|
...DEFAULT_DEFINE,
|
|
'BUILD_DEFS.DISABLE_WEBGPU': 'true',
|
|
'BUILD_DEFS.DISABLE_WEBGL': 'true',
|
|
'BUILD_DEFS.DISABLE_WASM_PROXY': 'true',
|
|
'BUILD_DEFS.DISABLE_WASM_THREAD': 'true',
|
|
},
|
|
}));
|
|
}
|
|
|
|
if (BUNDLE_MODE === 'dev') {
|
|
// ort.all.js
|
|
await addBuildTask(buildOrt({outputBundleName: 'ort.all', format: 'iife', define: {...DEFAULT_DEFINE}}));
|
|
}
|
|
|
|
if (BUNDLE_MODE === 'perf') {
|
|
// ort.all.min.js
|
|
await addBuildTask(buildOrt({
|
|
isProduction: true,
|
|
outputBundleName: 'ort.all.min',
|
|
format: 'iife',
|
|
}));
|
|
}
|
|
|
|
if (BUNDLE_MODE === 'prod') {
|
|
// ort.all[.min].js
|
|
await addAllWebBuildTasks({outputBundleName: 'ort.all'});
|
|
|
|
// ort[.min].js
|
|
await addAllWebBuildTasks({
|
|
outputBundleName: 'ort',
|
|
define: {...DEFAULT_DEFINE, 'BUILD_DEFS.DISABLE_WEBGPU': 'true'},
|
|
});
|
|
// ort.webgpu[.min].js
|
|
await addAllWebBuildTasks({
|
|
outputBundleName: 'ort.webgpu',
|
|
define: {...DEFAULT_DEFINE, 'BUILD_DEFS.DISABLE_WEBGL': 'true'},
|
|
});
|
|
// ort.wasm[.min].js
|
|
await addAllWebBuildTasks({
|
|
outputBundleName: 'ort.wasm',
|
|
define: {...DEFAULT_DEFINE, 'BUILD_DEFS.DISABLE_WEBGPU': 'true', 'BUILD_DEFS.DISABLE_WEBGL': 'true'},
|
|
});
|
|
// ort.webgl[.min].js
|
|
await addAllWebBuildTasks({
|
|
outputBundleName: 'ort.webgl',
|
|
define: {
|
|
...DEFAULT_DEFINE,
|
|
'BUILD_DEFS.DISABLE_WEBGPU': 'true',
|
|
'BUILD_DEFS.DISABLE_WASM': 'true',
|
|
},
|
|
});
|
|
// ort.wasm-core[.min].js
|
|
await addAllWebBuildTasks({
|
|
outputBundleName: 'ort.wasm-core',
|
|
define: {
|
|
...DEFAULT_DEFINE,
|
|
'BUILD_DEFS.DISABLE_WEBGPU': 'true',
|
|
'BUILD_DEFS.DISABLE_WEBGL': 'true',
|
|
'BUILD_DEFS.DISABLE_WASM_PROXY': 'true',
|
|
'BUILD_DEFS.DISABLE_WASM_THREAD': 'true',
|
|
},
|
|
});
|
|
// ort.training.wasm[.min].js
|
|
await addAllWebBuildTasks({
|
|
outputBundleName: 'ort.training.wasm',
|
|
define: {
|
|
...DEFAULT_DEFINE,
|
|
'BUILD_DEFS.DISABLE_TRAINING': 'false',
|
|
'BUILD_DEFS.DISABLE_WEBGPU': 'true',
|
|
'BUILD_DEFS.DISABLE_WEBGL': 'true',
|
|
},
|
|
});
|
|
}
|
|
|
|
if (BUNDLE_MODE === 'dev' || BUNDLE_MODE === 'perf') {
|
|
await addBuildTask(buildTest());
|
|
}
|
|
|
|
await Promise.all(buildTasks);
|
|
|
|
if (BUNDLE_MODE === 'prod') {
|
|
// generate package.json files under each of the dist folders for commonJS and ESModule
|
|
// this trick allows typescript to import this package as different module type
|
|
// see also: https://evertpot.com/universal-commonjs-esm-typescript-packages/
|
|
await fs.writeFile(path.resolve(__dirname, '../dist/cjs', 'package.json'), '{"type": "commonjs"}');
|
|
await fs.writeFile(path.resolve(__dirname, '../dist/esm', 'package.json'), '{"type": "module"}');
|
|
}
|
|
}
|
|
|
|
void main();
|