diff --git a/js/README.md b/js/README.md index 5ef449b3a0..488b025cc6 100644 --- a/js/README.md +++ b/js/README.md @@ -127,6 +127,7 @@ Node.js v12+ (recommended v14+) 2. ~~Follow [instructions](https://www.onnxruntime.ai/docs/how-to/build.html#apis-and-language-bindings) for building ONNX Runtime WebAssembly. (TODO: document is not ready. we are working on it.)~~ in `/`, run either of the following commands to build WebAssembly: + ```sh # In windows, use 'build' to replace './build.sh' @@ -136,16 +137,19 @@ Node.js v12+ (recommended v14+) # The following command build release. ./build.sh --config Release --build_wasm --skip_tests --disable_wasm_exception_catching --disable_rtti ``` + To build with multi-thread support, append flag ` --enable_wasm_threads` to the command. 3. Copy following files from build output folder to `/js/web/dist/`: + - ort-wasm.wasm - - ort-wasm-threaded.wasm (if appliable) - - ort-wasm-threaded.worker.js (if appliable) + - ort-wasm-threaded.wasm (build with flag '--enable_wasm_threads') 4. Copy following files from build output folder to `/js/web/lib/wasm/binding/`: + - ort-wasm.js - - ort-wasm-threaded.js (if appliable) + - ort-wasm-threaded.js (build with flag '--enable_wasm_threads') + - ort-wasm-threaded.worker.js (build with flag '--enable_wasm_threads') 5. Use following command in folder `/js/web` to build: ``` diff --git a/js/web/.gitignore b/js/web/.gitignore index 4ec6ca918a..e6da0a8e2d 100644 --- a/js/web/.gitignore +++ b/js/web/.gitignore @@ -16,4 +16,5 @@ script/**/*.js.map lib/wasm/binding/**/*.wasm !lib/wasm/binding/**/*.d.ts +test/testdata-config.json test/data/node/ diff --git a/js/web/karma.conf.js b/js/web/karma.conf.js index 5b8991093b..d7900a3124 100644 --- a/js/web/karma.conf.js +++ b/js/web/karma.conf.js @@ -45,10 +45,8 @@ module.exports = function (config) { frameworks: ['mocha'], files: [ { pattern: commonFile }, - { pattern: 'test/testdata-config.js' }, { pattern: mainFile }, { pattern: 'test/testdata-file-cache-*.json', included: false }, - //{ pattern: 'test/onnx-worker.js', included: false }, { pattern: 'test/data/**/*', included: false, nocache: true }, { pattern: 'dist/ort-wasm.wasm', included: false }, { pattern: 'dist/ort-wasm-threaded.wasm', included: false }, diff --git a/js/web/lib/backend-wasm.ts b/js/web/lib/backend-wasm.ts index 9192a2ebd6..ee5d10fca8 100644 --- a/js/web/lib/backend-wasm.ts +++ b/js/web/lib/backend-wasm.ts @@ -1,7 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import {readFile} from 'fs'; import {Backend, env, InferenceSession, SessionHandler} from 'onnxruntime-common'; +import {cpus} from 'os'; +import {promisify} from 'util'; import {OnnxruntimeWebAssemblySessionHandler} from './wasm/session-handler'; import {initializeWebAssembly} from './wasm/wasm-factory'; @@ -18,7 +21,8 @@ export const initializeFlags = (): void => { } if (typeof env.wasm.numThreads !== 'number' || !Number.isInteger(env.wasm.numThreads) || env.wasm.numThreads < 0) { - env.wasm.numThreads = Math.ceil((navigator.hardwareConcurrency || 1) / 2); + const numCpuLogicalCores = typeof navigator === 'undefined' ? cpus().length : navigator.hardwareConcurrency; + env.wasm.numThreads = Math.ceil((numCpuLogicalCores || 1) / 2); } env.wasm.numThreads = Math.min(4, env.wasm.numThreads); @@ -42,9 +46,15 @@ class OnnxruntimeWebAssemblyBackend implements Backend { Promise { let buffer: Uint8Array; if (typeof pathOrBuffer === 'string') { - const response = await fetch(pathOrBuffer); - const arrayBuffer = await response.arrayBuffer(); - buffer = new Uint8Array(arrayBuffer); + if (typeof fetch === 'undefined') { + // node + buffer = await promisify(readFile)(pathOrBuffer); + } else { + // browser + const response = await fetch(pathOrBuffer); + const arrayBuffer = await response.arrayBuffer(); + buffer = new Uint8Array(arrayBuffer); + } } else { buffer = pathOrBuffer; } diff --git a/js/web/lib/wasm/binding/ort-wasm-threaded.d.ts b/js/web/lib/wasm/binding/ort-wasm-threaded.d.ts index ad8e5f6636..b5898908b2 100644 --- a/js/web/lib/wasm/binding/ort-wasm-threaded.d.ts +++ b/js/web/lib/wasm/binding/ort-wasm-threaded.d.ts @@ -3,5 +3,9 @@ import {OrtWasmModule} from './ort-wasm'; -declare const moduleFactory: EmscriptenModuleFactory; +export interface OrtWasmThreadedModule extends OrtWasmModule { + PThread?: {terminateAllThreads(): void}; +} + +declare const moduleFactory: EmscriptenModuleFactory; export default moduleFactory; diff --git a/js/web/lib/wasm/wasm-factory.ts b/js/web/lib/wasm/wasm-factory.ts index c7bef35eaf..b51a85d003 100644 --- a/js/web/lib/wasm/wasm-factory.ts +++ b/js/web/lib/wasm/wasm-factory.ts @@ -2,7 +2,10 @@ // Licensed under the MIT License. import {env} from 'onnxruntime-common'; +import * as path from 'path'; + import {OrtWasmModule} from './binding/ort-wasm'; +import {OrtWasmThreadedModule} from './binding/ort-wasm-threaded'; import ortWasmFactoryThreaded from './binding/ort-wasm-threaded.js'; import ortWasmFactory from './binding/ort-wasm.js'; @@ -13,11 +16,14 @@ let aborted = false; const isMultiThreadSupported = (): boolean => { try { - // Test for transferability of SABs (needed for Firefox) + // Test for transferability of SABs (for browsers. needed for Firefox) // https://groups.google.com/forum/#!msg/mozilla.dev.platform/IHkBZlHETpA/dwsMNchWEQAJ - new MessageChannel().port1.postMessage(new SharedArrayBuffer(1)); - // This typed array is a WebAssembly program containing threaded - // instructions. + if (typeof MessageChannel !== 'undefined') { + new MessageChannel().port1.postMessage(new SharedArrayBuffer(1)); + } + + // Test for WebAssembly threads capability (for both browsers and Node.js) + // This typed array is a WebAssembly program containing threaded instructions. return WebAssembly.validate(new Uint8Array([ 0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2, 1, 0, 5, 4, 1, 3, 1, 1, 10, 11, 1, 9, 0, 65, 0, 254, 16, 2, 0, 26, 11 @@ -65,9 +71,25 @@ export const initializeWebAssembly = async(): Promise => { const config: Partial = {}; if (useThreads) { - config.mainScriptUrlOrBlob = new Blob( - [`var ortWasmThreaded=(function(){var _scriptDir;return ${ortWasmFactoryThreaded.toString()}})();`], - {type: 'text/javascript'}); + if (typeof Blob === 'undefined') { + config.mainScriptUrlOrBlob = path.join(__dirname, 'ort-wasm-threaded.js'); + } else { + const scriptSourceCode = + `var ortWasmThreaded=(function(){var _scriptDir;return ${ortWasmFactoryThreaded.toString()}})();`; + config.mainScriptUrlOrBlob = new Blob([scriptSourceCode], {type: 'text/javascript'}); + config.locateFile = (fileName: string, scriptDirectory: string) => { + if (fileName.endsWith('.worker.js')) { + return URL.createObjectURL(new Blob( + [ + // This require() function is handled by webpack to load file content of the corresponding .worker.js + // eslint-disable-next-line @typescript-eslint/no-require-imports + require('./binding/ort-wasm-threaded.worker.js') + ], + {type: 'text/javascript'})); + } + return scriptDirectory + fileName; + }; + } } factory(config).then( @@ -100,3 +122,15 @@ export const getInstance = (): OrtWasmModule => { throw new Error('WebAssembly is not initialized yet.'); }; + +export const dispose = (): void => { + if (initialized && !initializing && !aborted) { + initializing = true; + + (wasm as OrtWasmThreadedModule).PThread?.terminateAllThreads(); + + initializing = false; + initialized = false; + aborted = true; + } +}; diff --git a/js/web/package.json b/js/web/package.json index 6cd261c924..053d627305 100644 --- a/js/web/package.json +++ b/js/web/package.json @@ -17,6 +17,7 @@ "prepare": "tsc", "build": "node ./script/build", "test": "node ./script/prepare-test-data && node ./script/test-runner-cli", + "test:e2e": "node ./test/e2e/run", "prepack": "node ./script/prepack" }, "dependencies": { diff --git a/js/web/script/build.ts b/js/web/script/build.ts index a85ce1a681..9c64b0876d 100644 --- a/js/web/script/build.ts +++ b/js/web/script/build.ts @@ -13,8 +13,9 @@ const args = minimist(process.argv); // --bundle-mode=prod (default) // --bundle-mode=dev // --bundle-mode=perf +// --bundle-mode=node const MODE = args['bundle-mode'] || 'prod'; -if (['prod', 'dev', 'perf'].indexOf(MODE) === -1) { +if (['prod', 'dev', 'perf', 'node'].indexOf(MODE) === -1) { throw new Error(`unknown build mode: ${MODE}`); } @@ -24,12 +25,16 @@ const WASM = typeof args.wasm === 'undefined' ? true : !!args.wasm; // Path variables const WASM_BINDING_FOLDER = path.join(__dirname, '..', 'lib', 'wasm', 'binding'); -const WASM_JS_PATH = path.join(WASM_BINDING_FOLDER, 'ort-wasm.js'); -const WASM_THREADED_JS_PATH = path.join(WASM_BINDING_FOLDER, 'ort-wasm-threaded.js'); +const WASM_BINDING_JS_PATH = path.join(WASM_BINDING_FOLDER, 'ort-wasm.js'); +const WASM_BINDING_THREADED_JS_PATH = path.join(WASM_BINDING_FOLDER, 'ort-wasm-threaded.js'); +const WASM_BINDING_THREADED_WORKER_JS_PATH = path.join(WASM_BINDING_FOLDER, 'ort-wasm-threaded.worker.js'); +const WASM_BINDING_THREADED_MIN_JS_PATH = path.join(WASM_BINDING_FOLDER, 'ort-wasm-threaded.min.js'); +const WASM_BINDING_THREADED_MIN_WORKER_JS_PATH = path.join(WASM_BINDING_FOLDER, 'ort-wasm-threaded.min.worker.js'); const WASM_DIST_FOLDER = path.join(__dirname, '..', 'dist'); const WASM_WASM_PATH = path.join(WASM_DIST_FOLDER, 'ort-wasm.wasm'); const WASM_THREADED_WASM_PATH = path.join(WASM_DIST_FOLDER, 'ort-wasm-threaded.wasm'); const WASM_THREADED_WORKER_JS_PATH = path.join(WASM_DIST_FOLDER, 'ort-wasm-threaded.worker.js'); +const WASM_THREADED_JS_PATH = path.join(WASM_DIST_FOLDER, 'ort-wasm-threaded.js'); function validateFile(path: string): void { npmlog.info('Build', `Ensure file: ${path}`); @@ -41,28 +46,87 @@ function validateFile(path: string): void { } } +npmlog.info('Build.Bundle', 'Retrieving npm bin folder...'); +const npmBin = execSync('npm bin', {encoding: 'utf8'}).trimRight(); +npmlog.info('Build.Bundle', `Retrieving npm bin folder... DONE, folder: ${npmBin}`); + if (WASM) { npmlog.info('Build', 'Validating WebAssembly artifacts...'); try { - validateFile(WASM_JS_PATH); - validateFile(WASM_THREADED_JS_PATH); + validateFile(WASM_BINDING_JS_PATH); + validateFile(WASM_BINDING_THREADED_JS_PATH); + validateFile(WASM_BINDING_THREADED_WORKER_JS_PATH); validateFile(WASM_WASM_PATH); validateFile(WASM_THREADED_WASM_PATH); - validateFile(WASM_THREADED_WORKER_JS_PATH); } catch (e) { npmlog.error('Build', `WebAssembly files are not ready. build WASM first. ERR: ${e}`); throw e; } npmlog.info('Build', 'Validating WebAssembly artifacts... DONE'); + + const VERSION = require(path.join(__dirname, '../package.json')).version; + const COPYRIGHT_BANNER = `/*! + * ONNX Runtime Web v${VERSION} + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +`; + + const terserCommand = path.join(npmBin, 'terser'); + npmlog.info('Build', 'Minimizing file "ort-wasm-threaded.js"...'); + try { + const terser = spawnSync( + terserCommand, + [ + WASM_BINDING_THREADED_JS_PATH, '--compress', 'passes=2', '--format', 'comments=false', '--mangle', + 'reserved=[_scriptDir]', '--module' + ], + {shell: true, encoding: 'utf-8'}); + if (terser.status !== 0) { + console.error(terser.error); + process.exit(terser.status === null ? undefined : terser.status); + } + + fs.writeFileSync(WASM_BINDING_THREADED_MIN_JS_PATH, terser.stdout); + fs.writeFileSync(WASM_THREADED_JS_PATH, COPYRIGHT_BANNER + terser.stdout); + + validateFile(WASM_BINDING_THREADED_MIN_JS_PATH); + validateFile(WASM_THREADED_JS_PATH); + } catch (e) { + npmlog.error('Build', `Failed to run terser on ort-wasm-threaded.js. ERR: ${e}`); + throw e; + } + npmlog.info('Build', 'Minimizing file "ort-wasm-threaded.js"... DONE'); + + npmlog.info('Build', 'Minimizing file "ort-wasm-threaded.worker.js"...'); + try { + const terser = spawnSync( + terserCommand, + [ + WASM_BINDING_THREADED_WORKER_JS_PATH, '--compress', 'passes=2', '--format', 'comments=false', '--mangle', + 'reserved=[_scriptDir]', '--toplevel' + ], + {shell: true, encoding: 'utf-8'}); + if (terser.status !== 0) { + console.error(terser.error); + process.exit(terser.status === null ? undefined : terser.status); + } + + fs.writeFileSync(WASM_BINDING_THREADED_MIN_WORKER_JS_PATH, terser.stdout); + fs.writeFileSync(WASM_THREADED_WORKER_JS_PATH, COPYRIGHT_BANNER + terser.stdout); + + validateFile(WASM_BINDING_THREADED_MIN_WORKER_JS_PATH); + validateFile(WASM_THREADED_WORKER_JS_PATH); + } catch (e) { + npmlog.error('Build', `Failed to run terser on ort-wasm-threaded.worker.js. ERR: ${e}`); + throw e; + } + npmlog.info('Build', 'Minimizing file "ort-wasm-threaded.worker.js"... DONE'); } npmlog.info('Build', 'Building bundle...'); { - npmlog.info('Build.Bundle', '(1/2) Retrieving npm bin folder...'); - const npmBin = execSync('npm bin', {encoding: 'utf8'}).trimRight(); - npmlog.info('Build.Bundle', `(1/2) Retrieving npm bin folder... DONE, folder: ${npmBin}`); - - npmlog.info('Build.Bundle', '(2/2) Running webpack to generate bundles...'); + npmlog.info('Build.Bundle', 'Running webpack to generate bundles...'); const webpackCommand = path.join(npmBin, 'webpack'); const webpackArgs = ['--env', `--bundle-mode=${MODE}`]; npmlog.info('Build.Bundle', `CMD: ${webpackCommand} ${webpackArgs.join(' ')}`); @@ -71,6 +135,6 @@ npmlog.info('Build', 'Building bundle...'); console.error(webpack.error); process.exit(webpack.status === null ? undefined : webpack.status); } - npmlog.info('Build.Bundle', '(2/2) Running webpack to generate bundles... DONE'); + npmlog.info('Build.Bundle', 'Running webpack to generate bundles... DONE'); } npmlog.info('Build', 'Building bundle... DONE'); diff --git a/js/web/script/test-runner-cli-args.ts b/js/web/script/test-runner-cli-args.ts index b2519e8617..b83b58a30f 100644 --- a/js/web/script/test-runner-cli-args.ts +++ b/js/web/script/test-runner-cli-args.ts @@ -119,11 +119,12 @@ export interface TestRunnerCliArgs { * * For running tests, the default mode is 'dev'. If flag '--perf' is set, the mode will be set to 'perf'. * - * Mode | Output File | Main | Source Map | Webpack Config - * ------ | ------------------ | -------------------- | ------------------ | -------------- - * prod | /dist/ort.min.js | /lib/index.ts | source-map | production - * dev | /test/ort.dev.js | /test/test-main.ts | inline-source-map | development - * perf | /test/ort.perf.js | /test/test-main.ts | (none) | production + * Mode | Output File | Main | Source Map | Webpack Config + * ------ | --------------------- | -------------------- | ------------------ | -------------- + * prod | /dist/ort.min.js | /lib/index.ts | source-map | production + * node | /dist/ort-web.node.js | /lib/index.ts | source-map | production + * dev | /test/ort.dev.js | /test/test-main.ts | inline-source-map | development + * perf | /test/ort.perf.js | /test/test-main.ts | (none) | production */ bundleMode: TestRunnerCliArgs.BundleMode; @@ -298,24 +299,23 @@ export function parseTestRunnerCliArgs(cmdlineArgs: string[]): TestRunnerCliArgs const mode = args._.length === 0 ? 'suite0' : args._[0]; - // Option: -b=<...>, --backend=<...> - const backendArgs = args.backend || args.b; - const backend = (typeof backendArgs !== 'string') ? ['webgl', 'wasm'] : backendArgs.split(','); - for (const b of backend) { - if (b !== 'webgl' && b !== 'wasm') { - throw new Error(`not supported backend ${b}`); - } - } - // Option: -e=<...>, --env=<...> const envArg = args.env || args.e; const env = (typeof envArg !== 'string') ? 'chrome' : envArg; if (['chrome', 'edge', 'firefox', 'electron', 'safari', 'node', 'bs'].indexOf(env) === -1) { throw new Error(`not supported env ${env}`); } - if (env === 'node') { - // TODO: support node - throw new Error('node is currently not supported.'); + + // Option: -b=<...>, --backend=<...> + const browserBackends = ['webgl', 'wasm']; + const nodejsBackends = ['cpu', 'wasm']; + const backendArgs = args.backend || args.b; + const backend = + (typeof backendArgs !== 'string') ? (env === 'node' ? nodejsBackends : browserBackends) : backendArgs.split(','); + for (const b of backend) { + if ((env !== 'node' && browserBackends.indexOf(b) === -1) || (env === 'node' && nodejsBackends.indexOf(b) === -1)) { + throw new Error(`backend ${b} is not supported in env ${env}`); + } } // Options: diff --git a/js/web/script/test-runner-cli.ts b/js/web/script/test-runner-cli.ts index d657e390ac..95bad0fe3b 100644 --- a/js/web/script/test-runner-cli.ts +++ b/js/web/script/test-runner-cli.ts @@ -5,7 +5,7 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import {execSync, spawnSync} from 'child_process'; -import * as fs from 'fs'; +import * as fs from 'fs-extra'; import * as globby from 'globby'; import {default as minimatch} from 'minimatch'; import npmlog from 'npmlog'; @@ -399,31 +399,45 @@ function run(config: Test.Config) { `(1/5) Writing file cache to file: testdata-file-cache-*.json ... ${ fileCacheUrls.length > 0 ? `DONE, ${fileCacheUrls.length} file(s) generated` : 'SKIPPED'}`); - // STEP 2. write the config to testdata-config.js - npmlog.info('TestRunnerCli.Run', '(2/5) Writing config to file: testdata-config.js ...'); + // STEP 2. write the config to testdata-config.json + npmlog.info('TestRunnerCli.Run', '(2/5) Writing config to file: testdata-config.json ...'); saveConfig(config); - npmlog.info('TestRunnerCli.Run', '(2/5) Writing config to file: testdata-config.js ... DONE'); + npmlog.info('TestRunnerCli.Run', '(2/5) Writing config to file: testdata-config.json ... DONE'); // STEP 3. get npm bin folder npmlog.info('TestRunnerCli.Run', '(3/5) Retrieving npm bin folder...'); const npmBin = execSync('npm bin', {encoding: 'utf8'}).trimRight(); npmlog.info('TestRunnerCli.Run', `(3/5) Retrieving npm bin folder... DONE, folder: ${npmBin}`); + // STEP 4. generate bundle + npmlog.info('TestRunnerCli.Run', '(4/5) Running build to generate bundle...'); + const buildCommand = `node ${path.join(__dirname, 'build')}`; + const buildArgs = [`--bundle-mode=${args.env === 'node' ? 'node' : args.bundleMode}`]; + if (args.backends.indexOf('wasm') === -1) { + buildArgs.push('--no-wasm'); + } + npmlog.info('TestRunnerCli.Run', `CMD: ${buildCommand} ${buildArgs.join(' ')}`); + const build = spawnSync(buildCommand, buildArgs, {shell: true, stdio: 'inherit'}); + if (build.status !== 0) { + console.error(build.error); + process.exit(build.status === null ? undefined : build.status); + } + npmlog.info('TestRunnerCli.Run', '(4/5) Running build to generate bundle... DONE'); + if (args.env === 'node') { - // STEP 4. use tsc to build ONNX Runtime Web - npmlog.info('TestRunnerCli.Run', '(4/5) Running tsc...'); + // STEP 5. run tsc and run mocha + npmlog.info('TestRunnerCli.Run', '(5/5) Running tsc...'); const tscCommand = path.join(npmBin, 'tsc'); const tsc = spawnSync(tscCommand, {shell: true, stdio: 'inherit'}); if (tsc.status !== 0) { console.error(tsc.error); process.exit(tsc.status === null ? undefined : tsc.status); } - npmlog.info('TestRunnerCli.Run', '(4/5) Running tsc... DONE'); + npmlog.info('TestRunnerCli.Run', '(5/5) Running tsc... DONE'); - // STEP 5. run mocha npmlog.info('TestRunnerCli.Run', '(5/5) Running mocha...'); const mochaCommand = path.join(npmBin, 'mocha'); - const mochaArgs = [path.join(TEST_ROOT, 'test-main'), '--timeout 60000']; + const mochaArgs = [path.join(TEST_ROOT, 'test-main'), `--timeout ${args.debug ? 9999999 : 60000}`]; npmlog.info('TestRunnerCli.Run', `CMD: ${mochaCommand} ${mochaArgs.join(' ')}`); const mocha = spawnSync(mochaCommand, mochaArgs, {shell: true, stdio: 'inherit'}); if (mocha.status !== 0) { @@ -433,21 +447,6 @@ function run(config: Test.Config) { npmlog.info('TestRunnerCli.Run', '(5/5) Running mocha... DONE'); } else { - // STEP 4. generate bundle - npmlog.info('TestRunnerCli.Run', '(4/5) Running build to generate bundle...'); - const buildCommand = `node ${path.join(__dirname, 'build')}`; - const buildArgs = [`--bundle-mode=${args.bundleMode}`]; - if (args.backends.indexOf('wasm') === -1) { - buildArgs.push('--no-wasm'); - } - npmlog.info('TestRunnerCli.Run', `CMD: ${buildCommand} ${buildArgs.join(' ')}`); - const build = spawnSync(buildCommand, buildArgs, {shell: true, stdio: 'inherit'}); - if (build.status !== 0) { - console.error(build.error); - process.exit(build.status === null ? undefined : build.status); - } - npmlog.info('TestRunnerCli.Run', '(4/5) Running build to generate bundle... DONE'); - // STEP 5. use Karma to run test npmlog.info('TestRunnerCli.Run', '(5/5) Running karma to start test runner...'); const karmaCommand = path.join(npmBin, 'karma'); @@ -549,39 +548,7 @@ function saveOneFileCache(index: number, fileCache: Test.FileCache) { } function saveConfig(config: Test.Config) { - let setOptions = ''; - if (config.options.debug !== undefined) { - setOptions += `ort.env.debug = ${config.options.debug};`; - } - if (config.options.webglFlags && config.options.webglFlags.contextId !== undefined) { - setOptions += `ort.env.webgl.contextId = ${JSON.stringify(config.options.webglFlags.contextId)};`; - } - if (config.options.webglFlags && config.options.webglFlags.matmulMaxBatchSize !== undefined) { - setOptions += `ort.env.webgl.matmulMaxBatchSize = ${config.options.webglFlags.matmulMaxBatchSize};`; - } - if (config.options.webglFlags && config.options.webglFlags.textureCacheMode !== undefined) { - setOptions += `ort.env.webgl.textureCacheMode = ${JSON.stringify(config.options.webglFlags.textureCacheMode)};`; - } - if (config.options.webglFlags && config.options.webglFlags.pack !== undefined) { - setOptions += `ort.env.webgl.pack = ${JSON.stringify(config.options.webglFlags.pack)};`; - } - if (config.options.wasmFlags && config.options.wasmFlags.numThreads !== undefined) { - setOptions += `ort.env.wasm.numThreads = ${JSON.stringify(config.options.wasmFlags.numThreads)};`; - } - if (config.options.wasmFlags && config.options.wasmFlags.loggingLevel !== undefined) { - setOptions += `ort.env.wasm.loggingLevel = ${JSON.stringify(config.options.wasmFlags.loggingLevel)};`; - } - if (config.options.wasmFlags && config.options.wasmFlags.initTimeout !== undefined) { - setOptions += `ort.env.wasm.initTimeout = ${JSON.stringify(config.options.wasmFlags.initTimeout)};`; - } - // TODO: support onnxruntime nodejs binding - // if (config.model.some(testGroup => testGroup.tests.some(test => test.backend === 'cpu'))) { - // setOptions += 'require(\'onnxruntime-node\');'; - // } - - fs.writeFileSync(path.join(TEST_ROOT, './testdata-config.js'), `${setOptions} - -ort.env.ORT_WEB_TEST_DATA=${JSON.stringify(config)};`); + fs.writeJSONSync(path.join(TEST_ROOT, './testdata-config.json'), config); } function getBrowserNameFromEnv(env: TestRunnerCliArgs['env'], debug?: boolean) { diff --git a/js/web/test/e2e/.gitignore b/js/web/test/e2e/.gitignore new file mode 100644 index 0000000000..c25f6c22a9 --- /dev/null +++ b/js/web/test/e2e/.gitignore @@ -0,0 +1 @@ +!**/*.js diff --git a/js/web/test/e2e/browser-test-wasm-no-threads.js b/js/web/test/e2e/browser-test-wasm-no-threads.js new file mode 100644 index 0000000000..6ed7193b40 --- /dev/null +++ b/js/web/test/e2e/browser-test-wasm-no-threads.js @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +it('Browser E2E testing - WebAssembly backend (no threads)', async function () { + ort.env.wasm.numThreads = 1; + await testFunction(ort, { executionProviders: ['wasm'] }); +}); diff --git a/js/web/test/e2e/browser-test-wasm.js b/js/web/test/e2e/browser-test-wasm.js new file mode 100644 index 0000000000..9d91aed76d --- /dev/null +++ b/js/web/test/e2e/browser-test-wasm.js @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +it('Browser E2E testing - WebAssembly backend', async function () { + await testFunction(ort, { executionProviders: ['wasm'] }); +}); diff --git a/js/web/test/e2e/browser-test-webgl.js b/js/web/test/e2e/browser-test-webgl.js new file mode 100644 index 0000000000..4da09438df --- /dev/null +++ b/js/web/test/e2e/browser-test-webgl.js @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +it('Browser E2E testing - WebGL backend', async function () { + await testFunction(ort, { executionProviders: ['webgl'] }); +}); diff --git a/js/web/test/e2e/common.js b/js/web/test/e2e/common.js new file mode 100644 index 0000000000..e7b9b22ef1 --- /dev/null +++ b/js/web/test/e2e/common.js @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +function assert(cond) { + if (!cond) throw new Error(); +} + +var testFunction = async function (ort, options) { + const session = await ort.InferenceSession.create('./model.onnx', options || {}); + + const dataA = Float32Array.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + const dataB = Float32Array.from([10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120]); + + const fetches = await session.run({ + a: new ort.Tensor('float32', dataA, [3, 4]), + b: new ort.Tensor('float32', dataB, [4, 3]) + }); + + const c = fetches.c; + + assert(c instanceof ort.Tensor); + assert(c.dims.length === 2 && c.dims[0] === 3 && c.dims[1] === 3); + assert(c.data[0] === 700); + assert(c.data[1] === 800); + assert(c.data[2] === 900); + assert(c.data[3] === 1580); + assert(c.data[4] === 1840); + assert(c.data[5] === 2100); + assert(c.data[6] === 2460); + assert(c.data[7] === 2880); + assert(c.data[8] === 3300); +}; + +if (typeof module === 'object') { + module.exports = testFunction; +} diff --git a/js/web/test/e2e/karma.conf.js b/js/web/test/e2e/karma.conf.js new file mode 100644 index 0000000000..a0552c0f09 --- /dev/null +++ b/js/web/test/e2e/karma.conf.js @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const args = require('minimist')(process.argv.slice(2)); +const SELF_HOST = !!args['self-host']; +const TEST_MAIN = args['test-main']; +if (typeof TEST_MAIN !== 'string') { + throw new Error('flag --test-main= is required'); +} +const USER_DATA = args['user-data']; +if (typeof USER_DATA !== 'string') { + throw new Error('flag --user-data= is required'); +} + +module.exports = function (config) { + const distPrefix = SELF_HOST ? './node_modules/onnxruntime-web/dist/' : 'http://localhost:8081/dist/'; + config.set({ + frameworks: ['mocha'], + files: [ + { pattern: distPrefix + 'ort.js' }, + { pattern: './common.js' }, + { pattern: TEST_MAIN }, + { pattern: './node_modules/onnxruntime-web/dist/**/*', included: false, nocache: true }, + { pattern: './model.onnx', included: false } + ], + proxies: { + '/model.onnx': '/base/model.onnx', + }, + client: { captureConsole: true, mocha: { expose: ['body'], timeout: 60000 } }, + reporters: ['mocha'], + captureTimeout: 120000, + reportSlowerThan: 100, + browserDisconnectTimeout: 600000, + browserNoActivityTimeout: 300000, + browserDisconnectTolerance: 0, + browserSocketTimeout: 60000, + hostname: 'localhost', + browsers: [], + customLaunchers: { + Chrome_default: { + base: 'Chrome', + chromeDataDir: USER_DATA + }, + Chrome_no_threads: { + base: 'Chrome', + chromeDataDir: USER_DATA, + // TODO: no-thread flags + } + } + }); +}; diff --git a/js/web/test/e2e/model.onnx b/js/web/test/e2e/model.onnx new file mode 100644 index 0000000000..088124bd48 --- /dev/null +++ b/js/web/test/e2e/model.onnx @@ -0,0 +1,16 @@ + backend-test:b + +a +bc"MatMultest_matmul_2dZ +a +  + +Z +b +  + +b +c +  + +B \ No newline at end of file diff --git a/js/web/test/e2e/node-test-main-no-threads.js b/js/web/test/e2e/node-test-main-no-threads.js new file mode 100644 index 0000000000..2eb7ee25a3 --- /dev/null +++ b/js/web/test/e2e/node-test-main-no-threads.js @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const ort = require('onnxruntime-web'); +const testFunction = require('./common'); + +it('Browser E2E testing - WebAssembly backend', async function () { + ort.env.wasm.numThreads = 1; + await testFunction(ort, { executionProviders: ['wasm'] }); +}); diff --git a/js/web/test/e2e/node-test-main.js b/js/web/test/e2e/node-test-main.js new file mode 100644 index 0000000000..bbac8e478b --- /dev/null +++ b/js/web/test/e2e/node-test-main.js @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const ort = require('onnxruntime-web'); +const testFunction = require('./common'); + +it('Browser E2E testing - WebAssembly backend', async function () { + await testFunction(ort, { executionProviders: ['wasm'] }); + + process.exit(); +}); diff --git a/js/web/test/e2e/package.json b/js/web/test/e2e/package.json new file mode 100644 index 0000000000..7e0c78e8ff --- /dev/null +++ b/js/web/test/e2e/package.json @@ -0,0 +1,13 @@ +{ + "devDependencies": { + "fs-extra": "^9.1.0", + "globby": "^11.0.3", + "karma": "^6.3.2", + "karma-chrome-launcher": "^3.1.0", + "karma-mocha": "^2.0.1", + "karma-mocha-reporter": "^2.2.5", + "light-server": "^2.9.1", + "minimist": "^1.2.5", + "mocha": "^8.3.2" + } +} diff --git a/js/web/test/e2e/run.js b/js/web/test/e2e/run.js new file mode 100644 index 0000000000..02cb3a3326 --- /dev/null +++ b/js/web/test/e2e/run.js @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const path = require('path'); +const fs = require('fs-extra'); +const globby = require('globby'); +const { spawn } = require('child_process'); +const startServer = require('./simple-http-server'); + +// copy whole folder to out-side of /js/ because we need to test in a folder that no `package.json` file +// exists in its parent folder. +// here we use /build/js/e2e/ for the test + +const TEST_E2E_SRC_FOLDER = __dirname; +const JS_ROOT_FOLDER = path.resolve(__dirname, '../../..'); +const TEST_E2E_RUN_FOLDER = path.resolve(JS_ROOT_FOLDER, '../build/js/e2e'); +const NPM_CACHE_FOLDER = path.resolve(TEST_E2E_RUN_FOLDER, '../npm_cache'); +const CHROME_USER_DATA_FOLDER = path.resolve(TEST_E2E_RUN_FOLDER, '../user_data'); +fs.emptyDirSync(TEST_E2E_RUN_FOLDER); +fs.emptyDirSync(NPM_CACHE_FOLDER); +fs.emptyDirSync(CHROME_USER_DATA_FOLDER); +fs.copySync(TEST_E2E_SRC_FOLDER, TEST_E2E_RUN_FOLDER); + +// find packed package + +const ORT_COMMON_FOLDER = path.resolve(JS_ROOT_FOLDER, 'common'); +const ORT_COMMON_PACKED_FILEPATH_CANDIDATES = globby.sync('onnxruntime-common-*.tgz', { cwd: ORT_COMMON_FOLDER }); +if (ORT_COMMON_PACKED_FILEPATH_CANDIDATES.length !== 1) { + throw new Error('cannot find exactly single package for onnxruntime-common.'); +} +const ORT_COMMON_PACKED_FILEPATH = path.resolve(ORT_COMMON_FOLDER, ORT_COMMON_PACKED_FILEPATH_CANDIDATES[0]); + +const ORT_WEB_FOLDER = path.resolve(JS_ROOT_FOLDER, 'web'); +const ORT_WEB_PACKED_FILEPATH_CANDIDATES = globby.sync('onnxruntime-web-*.tgz', { cwd: ORT_WEB_FOLDER }); +if (ORT_WEB_PACKED_FILEPATH_CANDIDATES.length !== 1) { + throw new Error('cannot find exactly single package for onnxruntime-web.'); +} +const ORT_WEB_PACKED_FILEPATH = path.resolve(ORT_WEB_FOLDER, ORT_WEB_PACKED_FILEPATH_CANDIDATES[0]); + +// we start here: + +async function main() { + // install dev dependencies + await runInShell(`npm install"`); + + // npm install with "--cache" to install packed packages with an empty cache folder + await runInShell(`npm install --cache "${NPM_CACHE_FOLDER}" "${ORT_COMMON_PACKED_FILEPATH}" "${ORT_WEB_PACKED_FILEPATH}"`); + + // test case run in Node.js + await testAllNodejsCases(); + + // test cases with self-host (ort hosted in same origin) + await testAllBrowserCases({ hostInKarma: true }); + + // test cases without self-host (ort hosted in same origin) + startServer(path.resolve(TEST_E2E_RUN_FOLDER, 'node_modules', 'onnxruntime-web')); + await testAllBrowserCases({ hostInKarma: false }); + + // no error occurs, exit with code 0 + process.exit(0); +} + +async function testAllNodejsCases() { + await runInShell('node ./node_modules/mocha/bin/mocha ./node-test-main-no-threads.js'); + await runInShell('node ./node_modules/mocha/bin/mocha ./node-test-main.js'); + await runInShell('node --experimental-wasm-threads --experimental-wasm-bulk-memory ./node_modules/mocha/bin/mocha ./node-test-main-no-threads.js'); + await runInShell('node --experimental-wasm-threads --experimental-wasm-bulk-memory ./node_modules/mocha/bin/mocha ./node-test-main.js'); +} + +async function testAllBrowserCases({ hostInKarma }) { + await runKarma({ hostInKarma, main: './browser-test-webgl.js', browser: 'Chrome_default' }); + await runKarma({ hostInKarma, main: './browser-test-wasm.js', browser: 'Chrome_default' }); + await runKarma({ hostInKarma, main: './browser-test-wasm-no-threads.js', browser: 'Chrome_default' }); +} + +async function runKarma({ hostInKarma, main, browser }) { + const selfHostFlag = hostInKarma ? '--self-host' : ''; + await runInShell( + `npx karma start --single-run --browsers ${browser} ${selfHostFlag} --test-main=${main} --user-data=${CHROME_USER_DATA_FOLDER}`); +} + +async function runInShell(cmd) { + console.log('==============================================================='); + console.log(' Running command in shell:'); + console.log(' > ' + cmd); + console.log('==============================================================='); + let complete = false; + const childProcess = spawn(cmd, { shell: true, stdio: 'inherit', cwd: TEST_E2E_RUN_FOLDER }); + childProcess.on('close', function (code) { + if (code !== 0) { + process.exit(code); + } else { + complete = true; + } + }); + while (!complete) { + await delay(100); + } +} + +async function delay(ms) { + return new Promise(function (resolve) { + setTimeout(function () { + resolve(); + }, ms); + }); +} + +main(); diff --git a/js/web/test/e2e/simple-http-server.js b/js/web/test/e2e/simple-http-server.js new file mode 100644 index 0000000000..f5e96e8aa0 --- /dev/null +++ b/js/web/test/e2e/simple-http-server.js @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// this is a simple HTTP server that enables CORS. +// following code is based on https://developer.mozilla.org/en-US/docs/Learn/Server-side/Node_server_without_framework + +var http = require('http'); +var fs = require('fs'); +var path = require('path'); + +module.exports = function (dir) { + http.createServer(function (request, response) { + console.log('request ', request.url); + + var filePath = '.' + request.url; + + var extname = String(path.extname(filePath)).toLowerCase(); + var mimeTypes = { + '.html': 'text/html', + '.js': 'text/javascript', + '.css': 'text/css', + '.json': 'application/json', + '.png': 'image/png', + '.jpg': 'image/jpg', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.wav': 'audio/wav', + '.mp4': 'video/mp4', + '.woff': 'application/font-woff', + '.ttf': 'application/font-ttf', + '.eot': 'application/vnd.ms-fontobject', + '.otf': 'application/font-otf', + '.wasm': 'application/wasm' + }; + + var contentType = mimeTypes[extname] || 'application/octet-stream'; + + fs.readFile(path.resolve(dir, filePath), function (error, content) { + if (error) { + if (error.code == 'ENOENT') { + response.writeHead(404); + response.end('404'); + } + else { + response.writeHead(500); + response.end('500'); + } + } + else { + response.setHeader('access-control-allow-origin', '*'); + response.writeHead(200, { 'Content-Type': contentType }); + response.end(content, 'utf-8'); + } + }); + + }).listen(8081); + console.log('Server running at http://127.0.0.1:8081/'); +}; diff --git a/js/web/test/test-main.ts b/js/web/test/test-main.ts index 86b2dce371..c68dc3f272 100644 --- a/js/web/test/test-main.ts +++ b/js/web/test/test-main.ts @@ -1,16 +1,48 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import '../lib/index'; // this need to be the first line +// Load onnxruntime-web and testdata-config. +// NOTE: this need to be called before import any other library. +const ort = require('..'); +const ORT_WEB_TEST_CONFIG = require('./testdata-config.json') as Test.Config; -import * as ort from 'onnxruntime-common'; import * as platform from 'platform'; import {Logger} from '../lib/onnxjs/instrument'; import {Test} from './test-types'; -const ORT_WEB_TEST_CONFIG = (ort.env as any).ORT_WEB_TEST_DATA as Test.Config; +if (ORT_WEB_TEST_CONFIG.model.some(testGroup => testGroup.tests.some(test => test.backend === 'cpu'))) { + // require onnxruntime-node + require('../../node'); +} + +// set flags +const options = ORT_WEB_TEST_CONFIG.options; +if (options.debug !== undefined) { + ort.env.debug = options.debug; +} +if (ort.env.webgl && options.webglFlags && options.webglFlags.contextId !== undefined) { + ort.env.webgl.contextId = options.webglFlags.contextId; +} +if (ort.env.webgl && options.webglFlags && options.webglFlags.matmulMaxBatchSize !== undefined) { + ort.env.webgl.matmulMaxBatchSize = options.webglFlags.matmulMaxBatchSize; +} +if (ort.env.webgl && options.webglFlags && options.webglFlags.textureCacheMode !== undefined) { + ort.env.webgl.textureCacheMode = options.webglFlags.textureCacheMode; +} +if (ort.env.webgl && options.webglFlags && options.webglFlags.pack !== undefined) { + ort.env.webgl.pack = options.webglFlags.pack; +} +if (ort.env.wasm && options.wasmFlags && options.wasmFlags.numThreads !== undefined) { + ort.env.wasm.numThreads = options.wasmFlags.numThreads; +} +if (ort.env.wasm && options.wasmFlags && options.wasmFlags.loggingLevel !== undefined) { + ort.env.wasm.loggingLevel = options.wasmFlags.loggingLevel; +} +if (ort.env.wasm && options.wasmFlags && options.wasmFlags.initTimeout !== undefined) { + ort.env.wasm.initTimeout = options.wasmFlags.initTimeout; +} // Set logging configuration for (const logConfig of ORT_WEB_TEST_CONFIG.log) { diff --git a/js/web/test/test-suite-whitelist.jsonc b/js/web/test/test-suite-whitelist.jsonc index 4d6efebc42..8539d6d150 100644 --- a/js/web/test/test-suite-whitelist.jsonc +++ b/js/web/test/test-suite-whitelist.jsonc @@ -1,4 +1,9 @@ { + "cpu": { + "onnx": [], + "node": [], + "ops": [] + }, "webgl": { "onnx": ["resnet50", "squeezenet", "tiny_yolov2", "emotion_ferplus"], "node": [ diff --git a/js/web/webpack.config.js b/js/web/webpack.config.js index 768b75ff4b..18160af8e3 100644 --- a/js/web/webpack.config.js +++ b/js/web/webpack.config.js @@ -7,38 +7,33 @@ const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); const TerserPlugin = require("terser-webpack-plugin"); const minimist = require('minimist'); -function addCopyrightBannerPlugin(mode) { - const VERSION = require(path.join(__dirname, 'package.json')).version; - const COPYRIGHT_BANNER = `/*! - * ONNX Runtime Web v${VERSION} - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */`; +const VERSION = require(path.join(__dirname, 'package.json')).version; +const COPYRIGHT_BANNER = `/*! +* ONNX Runtime Web v${VERSION} +* Copyright (c) Microsoft Corporation. All rights reserved. +* Licensed under the MIT License. +*/`; - if (mode === 'production') { - return new TerserPlugin({ - extractComments: false, - terserOptions: { - format: { - preamble: COPYRIGHT_BANNER, - comments: false, - }, - compress: { - passes: 2 - }, - mangle: { - reserved: ["_scriptDir"] - } +function defaultTerserPluginOptions() { + return { + extractComments: false, + terserOptions: { + format: { + comments: false, + }, + compress: { + passes: 2 + }, + mangle: { + reserved: ["_scriptDir"] } - }); - } else { - return new webpack.BannerPlugin({ banner: COPYRIGHT_BANNER, raw: true }); - } + } + }; } // common config for release bundle function buildConfig({ filename, format, target, mode, devtool }) { - return { + const config = { target: [format === 'commonjs' ? 'node' : 'web', target], entry: path.resolve(__dirname, 'lib/index.ts'), output: { @@ -49,10 +44,7 @@ function buildConfig({ filename, format, target, mode, devtool }) { } }, resolve: { extensions: ['.ts', '.js'] }, - plugins: [ - new webpack.WatchIgnorePlugin({ paths: [/\.js$/, /\.d\.ts$/] }), - addCopyrightBannerPlugin(mode), - ], + plugins: [new webpack.WatchIgnorePlugin({ paths: [/\.js$/, /\.d\.ts$/] })], module: { rules: [{ test: /\.ts$/, @@ -64,11 +56,28 @@ function buildConfig({ filename, format, target, mode, devtool }) { } } ] + }, { + test: /\.worker.js$/, + type: 'asset/source' }] }, mode, devtool }; + + if (mode === 'production') { + config.resolve.alias = { + './binding/ort-wasm-threaded.js': './binding/ort-wasm-threaded.min.js', + './binding/ort-wasm-threaded.worker.js': './binding/ort-wasm-threaded.min.worker.js' + }; + const options = defaultTerserPluginOptions(); + options.terserOptions.format.preamble = COPYRIGHT_BANNER; + config.plugins.push(new TerserPlugin(options)); + } else { + config.plugins.push(new webpack.BannerPlugin({ banner: COPYRIGHT_BANNER, raw: true })); + } + + return config; } // "ort{.min}.js" config @@ -108,10 +117,9 @@ function buildOrtWebConfig({ config.externals.path = 'path'; config.externals.fs = 'fs'; config.externals.util = 'util'; - } - // in browser, do not use those node builtin modules - if (format === 'umd') { - config.resolve.fallback = { path: false, fs: false, util: false }; + config.externals.worker_threads = 'worker_threads'; + config.externals.perf_hooks = 'perf_hooks'; + config.externals.os = 'os'; } return config; } @@ -123,7 +131,7 @@ function buildTestRunnerConfig({ mode = 'production', devtool = 'source-map' }) { - return { + const config = { target: ['web', target], entry: path.resolve(__dirname, 'test/test-main.ts'), output: { @@ -139,6 +147,7 @@ function buildTestRunnerConfig({ 'fs': 'fs', 'perf_hooks': 'perf_hooks', 'worker_threads': 'worker_threads', + '../../node': '../../node' }, resolve: { extensions: ['.ts', '.js'], @@ -148,7 +157,6 @@ function buildTestRunnerConfig({ plugins: [ new webpack.WatchIgnorePlugin({ paths: [/\.js$/, /\.d\.ts$/] }), new NodePolyfillPlugin(), - addCopyrightBannerPlugin(mode), ], module: { rules: [{ @@ -161,16 +169,25 @@ function buildTestRunnerConfig({ } } ] + }, { + test: /\.worker\.js$/, + type: 'asset/source' }] }, mode: mode, devtool: devtool, }; + + if (mode === 'production') { + config.plugins.push(new TerserPlugin(defaultTerserPluginOptions())); + } + + return config; } module.exports = () => { const args = minimist(process.argv); - const bundleMode = args['bundle-mode'] || 'prod'; // 'prod'|'dev'|'perf'|undefined; + const bundleMode = args['bundle-mode'] || 'prod'; // 'prod'|'dev'|'perf'|'node'|undefined; const builds = []; switch (bundleMode) { @@ -193,7 +210,10 @@ module.exports = () => { buildOrtWebConfig({ suffix: '.es6.min', target: 'es6' }), // ort-web.es6.js buildOrtWebConfig({ suffix: '.es6', mode: 'development', devtool: 'inline-source-map', target: 'es6' }), + ); + case 'node': + builds.push( // ort-web.node.js buildOrtWebConfig({ suffix: '.node', format: 'commonjs' }), ); diff --git a/tools/ci_build/github/azure-pipelines/win-wasm-ci-pipeline.yml b/tools/ci_build/github/azure-pipelines/win-wasm-ci-pipeline.yml index 83fca75cae..d5a090db2f 100644 --- a/tools/ci_build/github/azure-pipelines/win-wasm-ci-pipeline.yml +++ b/tools/ci_build/github/azure-pipelines/win-wasm-ci-pipeline.yml @@ -157,7 +157,6 @@ jobs: sourceFolder: $(Pipeline.Workspace)\artifacts contents: | **\*.wasm - **\*.worker.js targetFolder: $(Build.SourcesDirectory)\js\web\dist flattenFolders: true displayName: 'Binplace dist files' @@ -166,7 +165,6 @@ jobs: sourceFolder: $(Pipeline.Workspace)\artifacts contents: | **\*.js - !**\*.worker.js targetFolder: $(Build.SourcesDirectory)\js\web\lib\wasm\binding flattenFolders: true displayName: 'Binplace js files' @@ -212,6 +210,11 @@ jobs: workingDirectory: '$(Build.SourcesDirectory)\js\web' displayName: 'Generate NPM package (onnxruntime-web)' condition: and(succeeded(), eq(variables['BuildConfig'], 'Release')) + - script: | + npm run test:e2e + workingDirectory: '$(Build.SourcesDirectory)\js\web' + displayName: 'E2E package consuming test' + condition: and(succeeded(), eq(variables['BuildConfig'], 'Release')) - task: CopyFiles@2 inputs: sourceFolder: $(Build.SourcesDirectory)\js\common