diff --git a/.eslintignore b/.eslintignore index 76bf09833d9..fee462cf586 100644 --- a/.eslintignore +++ b/.eslintignore @@ -10,6 +10,8 @@ /test/integration/dynamic-references-raw/index.js /test/integration/dynamic-references-raw/local.js /test/integration/hmr-dynamic/index.js +/test/integration/wasm-async/index.js +/test/integration/wasm-dynamic/index.js # Generated by the build lib diff --git a/package.json b/package.json index 4b5ea37505a..f4a8ef90eca 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "babylon-walk": "^1.0.2", "browser-resolve": "^1.11.2", "chalk": "^2.1.0", + "child-process-promise": "^2.2.1", "chokidar": "^1.7.0", "commander": "^2.11.0", "cross-spawn": "^5.1.0", diff --git a/src/Bundle.js b/src/Bundle.js index 3dc9166c93c..7b54036fe26 100644 --- a/src/Bundle.js +++ b/src/Bundle.js @@ -15,7 +15,20 @@ class Bundle { this.entryAsset = null; this.assets = new Set(); this.childBundles = new Set(); - this.siblingBundles = new Map(); + this.siblingBundles = new Set; + this.siblingBundlesMap = new Map(); + } + + static createWithAsset(asset, parentBundle) { + let bundle = new Bundle( + asset.type, + Path.join(asset.options.outDir, asset.generateBundleName()), + parentBundle + ); + + bundle.entryAsset = asset; + bundle.addAsset(asset); + return bundle; } addAsset(asset) { @@ -33,26 +46,36 @@ class Bundle { return this; } - if (!this.siblingBundles.has(type)) { - let bundle = this.createChildBundle( + if (!this.siblingBundlesMap.has(type)) { + let bundle = new Bundle( type, Path.join( Path.dirname(this.name), Path.basename(this.name, Path.extname(this.name)) + '.' + type - ) + ), + this ); - this.siblingBundles.set(type, bundle); + + this.childBundles.add(bundle); + this.siblingBundles.add(bundle); + this.siblingBundlesMap.set(type, bundle); } - return this.siblingBundles.get(type); + return this.siblingBundlesMap.get(type); } - createChildBundle(type, name) { - let bundle = new Bundle(type, name, this); + createChildBundle(entryAsset) { + let bundle = Bundle.createWithAsset(entryAsset, this); this.childBundles.add(bundle); return bundle; } + createSiblingBundle(entryAsset) { + let bundle = this.createChildBundle(entryAsset); + this.siblingBundles.add(bundle); + return bundle; + } + get isEmpty() { return this.assets.size === 0; } diff --git a/src/Bundler.js b/src/Bundler.js index 864632b512b..c7d8d954f64 100644 --- a/src/Bundler.js +++ b/src/Bundler.js @@ -15,6 +15,7 @@ const localRequire = require('./utils/localRequire'); const config = require('./utils/config'); const emoji = require('./utils/emoji'); const loadEnv = require('./utils/env'); +const PromiseQueue = require('./utils/PromiseQueue'); /** * The Bundler is the main entry point. It resolves and loads assets, @@ -32,6 +33,17 @@ class Bundler extends EventEmitter { this.cache = this.options.cache ? new FSCache(this.options) : null; this.logger = new Logger(this.options); this.delegate = options.delegate || {}; + this.bundleLoaders = {}; + + this.addBundleLoader( + 'wasm', + require.resolve('./builtins/loaders/wasm-loader') + ); + this.addBundleLoader( + 'css', + require.resolve('./builtins/loaders/css-loader') + ); + this.addBundleLoader('js', require.resolve('./builtins/loaders/js-loader')); this.pending = false; this.loadedAssets = new Map(); @@ -41,7 +53,7 @@ class Bundler extends EventEmitter { this.hmr = null; this.bundleHashes = null; this.errored = false; - this.buildQueue = new Set(); + this.buildQueue = new PromiseQueue(this.processAsset.bind(this)); this.rebuildTimeout = null; } @@ -92,6 +104,18 @@ class Bundler extends EventEmitter { this.packagers.add(type, packager); } + addBundleLoader(type, path) { + if (typeof path !== 'string') { + throw new Error('Bundle loader should be a module path.'); + } + + if (this.farm) { + throw new Error('Bundle loaders must be added before bundling.'); + } + + this.bundleLoaders[type] = path; + } + async loadPlugins() { let pkg = await config.load(this.mainFile, ['package.json']); if (!pkg) { @@ -141,8 +165,26 @@ class Bundler extends EventEmitter { this.buildQueue.add(this.mainAsset); } - // Build the queued assets, and produce a bundle tree. - let bundle = await this.buildQueuedAssets(isInitialBundle); + // Build the queued assets. + let loadedAssets = await this.buildQueue.run(); + + // Emit an HMR update for any new assets (that don't have a parent bundle yet) + // plus the asset that actually changed. + if (this.hmr && !isInitialBundle) { + this.hmr.emitUpdate([...this.findOrphanAssets(), ...loadedAssets]); + } + + // Invalidate bundles + for (let asset of this.loadedAssets.values()) { + asset.invalidateBundle(); + } + + // Create a new bundle tree and package everything up. + let bundle = this.createBundleTree(this.mainAsset); + this.bundleHashes = await bundle.package(this, this.bundleHashes); + + // Unload any orphaned assets + this.unloadOrphanedAssets(); let buildTime = Date.now() - startTime; let time = @@ -151,6 +193,7 @@ class Bundler extends EventEmitter { : `${(buildTime / 1000).toFixed(2)}s`; this.logger.status(emoji.success, `Built in ${time}.`, 'green'); + this.emit('bundled', bundle); return bundle; } catch (err) { this.errored = true; @@ -182,8 +225,8 @@ class Bundler extends EventEmitter { await loadEnv(this.mainFile); this.options.extensions = Object.assign({}, this.parser.extensions); + this.options.bundleLoaders = this.bundleLoaders; this.options.env = process.env; - this.farm = WorkerFarm.getShared(this.options); if (this.options.watch) { // FS events on macOS are flakey in the tests, which write lots of files very quickly @@ -199,6 +242,8 @@ class Bundler extends EventEmitter { this.hmr = new HMRServer(); this.options.hmrPort = await this.hmr.start(this.options.hmrPort); } + + this.farm = WorkerFarm.getShared(this.options); } stop() { @@ -215,49 +260,11 @@ class Bundler extends EventEmitter { } } - async buildQueuedAssets(isInitialBundle = false) { - // Consume the rebuild queue until it is empty. - let loadedAssets = new Set(); - while (this.buildQueue.size > 0) { - let promises = []; - for (let asset of this.buildQueue) { - // Invalidate the asset, unless this is the initial bundle - if (!isInitialBundle) { - asset.invalidate(); - if (this.cache) { - this.cache.invalidate(asset.name); - } - } - - promises.push(this.loadAsset(asset)); - loadedAssets.add(asset); - } - - // Wait for all assets to load. If there are more added while - // these are processing, they'll be loaded in the next batch. - await Promise.all(promises); - } - - // Emit an HMR update for any new assets (that don't have a parent bundle yet) - // plus the asset that actually changed. - if (this.hmr && !isInitialBundle) { - this.hmr.emitUpdate([...this.findOrphanAssets(), ...loadedAssets]); - } - - // Invalidate bundles - for (let asset of this.loadedAssets.values()) { - asset.invalidateBundle(); - } - - // Create a new bundle tree and package everything up. - let bundle = this.createBundleTree(this.mainAsset); - this.bundleHashes = await bundle.package(this, this.bundleHashes); - - // Unload any orphaned assets - this.unloadOrphanedAssets(); - - this.emit('bundled', bundle); - return bundle; + async getAsset(name, parent) { + let asset = await this.resolveAsset(name, parent); + this.buildQueue.add(asset); + await this.buildQueue.run(); + return asset; } async resolveAsset(name, parent) { @@ -328,9 +335,19 @@ class Bundler extends EventEmitter { } } + async processAsset(asset, isRebuild) { + if (isRebuild) { + asset.invalidate(); + if (this.cache) { + this.cache.invalidate(asset.name); + } + } + + await this.loadAsset(asset); + } + async loadAsset(asset) { if (asset.processed) { - this.buildQueue.delete(asset); return; } @@ -386,8 +403,6 @@ class Bundler extends EventEmitter { asset.depAssets.set(dep, assetDep); } }); - - this.buildQueue.delete(asset); } createBundleTree(asset, dep, bundle, parentBundles = new Set()) { @@ -416,27 +431,20 @@ class Bundler extends EventEmitter { } } - // Create the root bundle if it doesn't exist if (!bundle) { - bundle = new Bundle( - asset.type, - Path.join(this.options.outDir, asset.generateBundleName()) - ); - bundle.entryAsset = asset; + // Create the root bundle if it doesn't exist + bundle = Bundle.createWithAsset(asset); + } else if (dep && dep.dynamic) { + // Create a new bundle for dynamic imports + bundle = bundle.createChildBundle(asset); + } else if (asset.type && !this.packagers.has(asset.type)) { + // No packager is available for this asset type. Create a new bundle with only this asset. + bundle.createSiblingBundle(asset); + } else { + // Add the asset to the common bundle of the asset's type + bundle.getSiblingBundle(asset.type).addAsset(asset); } - // Create a new bundle for dynamic imports - if (dep && dep.dynamic) { - bundle = bundle.createChildBundle( - asset.type, - Path.join(this.options.outDir, asset.generateBundleName()) - ); - bundle.entryAsset = asset; - } - - // Add the asset to the bundle of the asset's type - bundle.getSiblingBundle(asset.type).addAsset(asset); - // If the asset generated a representation for the parent bundle type, also add it there if (asset.generated[bundle.type] != null) { bundle.addAsset(asset); @@ -523,7 +531,7 @@ class Bundler extends EventEmitter { // Add the asset to the rebuild queue, and reset the timeout. for (let asset of assets) { - this.buildQueue.add(asset); + this.buildQueue.add(asset, true); } clearTimeout(this.rebuildTimeout); diff --git a/src/Server.js b/src/Server.js index 5e2155d3878..ccf595e908b 100644 --- a/src/Server.js +++ b/src/Server.js @@ -5,6 +5,10 @@ const getPort = require('get-port'); const serverErrors = require('./utils/customErrors').serverErrors; const generateCertificate = require('./utils/generateCertificate'); +serveStatic.mime.define({ + 'application/wasm': ['wasm'] +}); + function middleware(bundler) { const serve = serveStatic(bundler.options.outDir, {index: false}); diff --git a/src/assets/HTMLAsset.js b/src/assets/HTMLAsset.js index b22eb70edf1..57cda6cd340 100644 --- a/src/assets/HTMLAsset.js +++ b/src/assets/HTMLAsset.js @@ -47,7 +47,9 @@ class HTMLAsset extends Asset { continue; } if (elements && elements.includes(node.tag)) { - let assetPath = this.addURLDependency(decodeURIComponent(node.attrs[attr])); + let assetPath = this.addURLDependency( + decodeURIComponent(node.attrs[attr]) + ); if (!isURL(assetPath)) { assetPath = urlJoin(this.options.publicURL, assetPath); } diff --git a/src/assets/RawAsset.js b/src/assets/RawAsset.js index c67d240b479..310fa2d98fe 100644 --- a/src/assets/RawAsset.js +++ b/src/assets/RawAsset.js @@ -6,6 +6,12 @@ class RawAsset extends Asset { load() {} generate() { + // Don't return a URL to the JS bundle if there is a bundle loader defined for this asset type. + // This will cause the actual asset to be automatically preloaded prior to the JS bundle running. + if (this.options.bundleLoaders[this.type]) { + return {}; + } + const pathToAsset = urlJoin( this.options.publicURL, this.generateBundleName() diff --git a/src/builtins/bundle-loader.js b/src/builtins/bundle-loader.js index 16323dff036..bc754a3a988 100644 --- a/src/builtins/bundle-loader.js +++ b/src/builtins/bundle-loader.js @@ -1,16 +1,15 @@ var getBundleURL = require('./bundle-url').getBundleURL; -function loadBundles(bundles) { - var id = Array.isArray(bundles) ? bundles[bundles.length - 1] : bundles; +function loadBundlesLazy(bundles) { + var id = bundles[bundles.length - 1]; try { return Promise.resolve(require(id)); } catch (err) { if (err.code === 'MODULE_NOT_FOUND') { return new LazyPromise(function (resolve, reject) { - Promise.all(bundles.slice(0, -1).map(loadBundle)).then(function () { - return require(id); - }).then(resolve, reject); + loadBundles(bundles) + .then(resolve, reject); }); } @@ -18,15 +17,32 @@ function loadBundles(bundles) { } } -module.exports = exports = loadBundles; +function loadBundles(bundles) { + var id = bundles[bundles.length - 1]; -var bundles = {}; -var bundleLoaders = { - js: loadJSBundle, - css: loadCSSBundle -}; + return Promise.all(bundles.slice(0, -1).map(loadBundle)) + .then(function () { + return require(id); + }); +} +var bundleLoaders = {}; +function registerBundleLoader(type, loader) { + bundleLoaders[type] = loader; +} + +module.exports = exports = loadBundlesLazy; +exports.load = loadBundles; +exports.register = registerBundleLoader; + +var bundles = {}; function loadBundle(bundle) { + var id; + if (Array.isArray(bundle)) { + id = bundle[1]; + bundle = bundle[0]; + } + if (bundles[bundle]) { return bundles[bundle]; } @@ -34,50 +50,19 @@ function loadBundle(bundle) { var type = bundle.match(/\.(.+)$/)[1].toLowerCase(); var bundleLoader = bundleLoaders[type]; if (bundleLoader) { - return bundles[bundle] = bundleLoader(getBundleURL() + bundle); + return bundles[bundle] = bundleLoader(getBundleURL() + bundle) + .then(function (resolved) { + if (resolved) { + module.bundle.modules[id] = [function (require,module) { + module.exports = resolved; + }, {}]; + } + + return resolved; + }); } } -function loadJSBundle(bundle) { - return new Promise(function (resolve, reject) { - var script = document.createElement('script'); - script.async = true; - script.type = 'text/javascript'; - script.charset = 'utf-8'; - script.src = bundle; - script.onerror = function (e) { - script.onerror = script.onload = null; - reject(e); - }; - - script.onload = function () { - script.onerror = script.onload = null; - resolve(); - }; - - document.getElementsByTagName('head')[0].appendChild(script); - }); -} - -function loadCSSBundle(bundle) { - return new Promise(function (resolve, reject) { - var link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = bundle; - link.onerror = function (e) { - link.onerror = link.onload = null; - reject(e); - }; - - link.onload = function () { - link.onerror = link.onload = null; - resolve(); - }; - - document.getElementsByTagName('head')[0].appendChild(link); - }); -} - function LazyPromise(executor) { this.executor = executor; this.promise = null; diff --git a/src/builtins/hmr-runtime.js b/src/builtins/hmr-runtime.js index d81906e913a..af0cb1c1c4e 100644 --- a/src/builtins/hmr-runtime.js +++ b/src/builtins/hmr-runtime.js @@ -15,8 +15,8 @@ function Module() { module.bundle.Module = Module; if (!module.bundle.parent && typeof WebSocket !== 'undefined') { - var hostname = '{{HMR_HOSTNAME}}' || location.hostname; - var ws = new WebSocket('ws://' + hostname + ':{{HMR_PORT}}/'); + var hostname = process.env.HMR_HOSTNAME || location.hostname; + var ws = new WebSocket('ws://' + hostname + ':' + process.env.HMR_PORT + '/'); ws.onmessage = function(event) { var data = JSON.parse(event.data); diff --git a/src/builtins/loaders/css-loader.js b/src/builtins/loaders/css-loader.js new file mode 100644 index 00000000000..d8ce1e89f75 --- /dev/null +++ b/src/builtins/loaders/css-loader.js @@ -0,0 +1,18 @@ +module.exports = function loadCSSBundle(bundle) { + return new Promise(function (resolve, reject) { + var link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = bundle; + link.onerror = function (e) { + link.onerror = link.onload = null; + reject(e); + }; + + link.onload = function () { + link.onerror = link.onload = null; + resolve(); + }; + + document.getElementsByTagName('head')[0].appendChild(link); + }); +}; diff --git a/src/builtins/loaders/js-loader.js b/src/builtins/loaders/js-loader.js new file mode 100644 index 00000000000..3aa07468dd7 --- /dev/null +++ b/src/builtins/loaders/js-loader.js @@ -0,0 +1,20 @@ +module.exports = function loadJSBundle(bundle) { + return new Promise(function (resolve, reject) { + var script = document.createElement('script'); + script.async = true; + script.type = 'text/javascript'; + script.charset = 'utf-8'; + script.src = bundle; + script.onerror = function (e) { + script.onerror = script.onload = null; + reject(e); + }; + + script.onload = function () { + script.onerror = script.onload = null; + resolve(); + }; + + document.getElementsByTagName('head')[0].appendChild(script); + }); +}; diff --git a/src/builtins/loaders/wasm-loader.js b/src/builtins/loaders/wasm-loader.js new file mode 100644 index 00000000000..573d490f856 --- /dev/null +++ b/src/builtins/loaders/wasm-loader.js @@ -0,0 +1,16 @@ +module.exports = function loadWASMBundle(bundle) { + return fetch(bundle) + .then(function (res) { + if (WebAssembly.instantiateStreaming) { + return WebAssembly.instantiateStreaming(res); + } else { + return res.arrayBuffer() + .then(function (data) { + return WebAssembly.instantiate(data); + }); + } + }) + .then(function (wasmModule) { + return wasmModule.instance.exports; + }); +}; diff --git a/src/packagers/JSPackager.js b/src/packagers/JSPackager.js index 6e216195e32..a5fd26f6fbb 100644 --- a/src/packagers/JSPackager.js +++ b/src/packagers/JSPackager.js @@ -12,14 +12,12 @@ const prelude = { .replace(/;$/, '') }; -const hmr = fs - .readFileSync(path.join(__dirname, '../builtins/hmr-runtime.js'), 'utf8') - .trim(); - class JSPackager extends Packager { async start() { this.first = true; this.dedupe = new Map(); + this.bundleLoaders = new Set(); + this.externalModules = new Set(); let preludeCode = this.options.minify ? prelude.minified : prelude.source; await this.dest.write(preludeCode + '({'); @@ -39,23 +37,45 @@ class JSPackager extends Packager { for (let [dep, mod] of asset.depAssets) { // For dynamic dependencies, list the child bundles to load along with the module id if (dep.dynamic && this.bundle.childBundles.has(mod.parentBundle)) { - let bundles = [path.basename(mod.parentBundle.name)]; - for (let child of mod.parentBundle.siblingBundles.values()) { + let bundles = [this.getBundleSpecifier(mod.parentBundle)]; + for (let child of mod.parentBundle.siblingBundles) { if (!child.isEmpty) { - bundles.push(path.basename(child.name)); + bundles.push(this.getBundleSpecifier(child)); + this.bundleLoaders.add(child.type); } } bundles.push(mod.id); deps[dep.name] = bundles; + this.bundleLoaders.add(mod.type); } else { deps[dep.name] = this.dedupe.get(mod.generated.js) || mod.id; + + // If the dep isn't in this bundle, add it to the list of external modules to preload. + // Only do this if this is the root JS bundle, otherwise they will have already been + // loaded in parallel with this bundle as part of a dynamic import. + if ( + !this.bundle.assets.has(mod) && + (!this.bundle.parentBundle || this.bundle.parentBundle.type !== 'js') + ) { + this.externalModules.add(mod); + this.bundleLoaders.add(mod.type); + } } } await this.writeModule(asset.id, asset.generated.js, deps); } + getBundleSpecifier(bundle) { + let name = path.basename(bundle.name); + if (bundle.entryAsset) { + return [name, bundle.entryAsset.id]; + } + + return name; + } + async writeModule(id, code, deps = {}) { let wrapped = this.first ? '' : ','; wrapped += @@ -67,23 +87,89 @@ class JSPackager extends Packager { await this.dest.write(wrapped); } + async addAssetToBundle(asset) { + this.bundle.addAsset(asset); + if (!asset.parentBundle) { + asset.parentBundle = this.bundle; + } + + // Add all dependencies as well + for (let child of asset.depAssets.values()) { + await this.addAssetToBundle(child, this.bundle); + } + + await this.addAsset(asset); + } + + async writeBundleLoaders() { + if (this.bundleLoaders.size === 0) { + return false; + } + + let bundleLoader = this.bundler.loadedAssets.get( + require.resolve('../builtins/bundle-loader') + ); + if (this.externalModules.size > 0 && !bundleLoader) { + bundleLoader = await this.bundler.getAsset('_bundle_loader'); + await this.addAssetToBundle(bundleLoader); + } + + if (!bundleLoader) { + return; + } + + // Generate a module to register the bundle loaders that are needed + let loads = 'var b=require(' + bundleLoader.id + ');'; + for (let bundleType of this.bundleLoaders) { + let loader = this.options.bundleLoaders[bundleType]; + if (loader) { + let asset = await this.bundler.getAsset(loader); + await this.addAssetToBundle(asset); + loads += + 'b.register(' + + JSON.stringify(bundleType) + + ',require(' + + asset.id + + '));'; + } + } + + // Preload external modules before running entry point if needed + if (this.externalModules.size > 0) { + let preload = []; + for (let mod of this.externalModules) { + preload.push([mod.generateBundleName(), mod.id]); + } + + if (this.bundle.entryAsset) { + preload.push(this.bundle.entryAsset.id); + } + + loads += 'b.load(' + JSON.stringify(preload) + ');'; + } + + // Asset ids normally start at 1, so this should be safe. + await this.writeModule(0, loads, {}); + return true; + } + async end() { let entry = []; // Add the HMR runtime if needed. if (this.options.hmr) { - // Asset ids normally start at 1, so this should be safe. - await this.writeModule( - 0, - hmr - .replace('{{HMR_PORT}}', this.options.hmrPort) - .replace('{{HMR_HOSTNAME}}', this.options.hmrHostname) + let asset = await this.bundler.getAsset( + require.resolve('../builtins/hmr-runtime') ); + await this.addAssetToBundle(asset); + entry.push(asset.id); + } + + if (await this.writeBundleLoaders()) { entry.push(0); } - // Load the entry module - if (this.bundle.entryAsset) { + if (this.bundle.entryAsset && this.externalModules.size === 0) { entry.push(this.bundle.entryAsset.id); } diff --git a/src/packagers/index.js b/src/packagers/index.js index 4792f77579e..09aa00a0abe 100644 --- a/src/packagers/index.js +++ b/src/packagers/index.js @@ -20,6 +20,10 @@ class PackagerRegistry { this.packagers.set(type, packager); } + has(type) { + return this.packagers.has(type); + } + get(type) { return this.packagers.get(type) || RawPackager; } diff --git a/src/utils/PromiseQueue.js b/src/utils/PromiseQueue.js new file mode 100644 index 00000000000..8ba937c662f --- /dev/null +++ b/src/utils/PromiseQueue.js @@ -0,0 +1,76 @@ +class PromiseQueue { + constructor(callback) { + this.process = callback; + this.queue = []; + this.processing = new Set(); + this.processed = new Set(); + this.runPromise = null; + this.resolve = null; + this.reject = null; + } + + add(job, ...args) { + if (this.processing.has(job)) { + return; + } + + if (this.runPromise) { + this._runJob(job, args); + } else { + this.queue.push([job, args]); + } + + this.processing.add(job); + } + + run() { + if (this.runPromise) { + return this.runPromise; + } + + this.runPromise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + + this._next(); + return this.runPromise; + } + + async _runJob(job, args) { + try { + await this.process(job, ...args); + this.processing.delete(job); + this.processed.add(job); + this._next(); + } catch (err) { + this.queue.push([job, args]); + this.reject(err); + this._reset(); + } + } + + _next() { + if (!this.runPromise) { + return; + } + + if (this.queue.length > 0) { + while (this.queue.length > 0) { + this._runJob(...this.queue.shift()); + } + } else if (this.processing.size === 0) { + this.resolve(this.processed); + this._reset(); + } + } + + _reset() { + this.processed = new Set(); + this.runPromise = null; + this.resolve = null; + this.reject = null; + } +} + +module.exports = PromiseQueue; diff --git a/src/utils/fs.js b/src/utils/fs.js index f71707664b8..8b01c80680b 100644 --- a/src/utils/fs.js +++ b/src/utils/fs.js @@ -5,6 +5,7 @@ const mkdirp = require('mkdirp'); exports.readFile = promisify(fs.readFile); exports.writeFile = promisify(fs.writeFile); exports.stat = promisify(fs.stat); +exports.readdir = promisify(fs.readdir); exports.exists = function(filename) { return new Promise(resolve => { diff --git a/src/worker.js b/src/worker.js index c1ded1407f1..c6792e1e814 100644 --- a/src/worker.js +++ b/src/worker.js @@ -6,6 +6,8 @@ let parser; exports.init = function(options, callback) { parser = new Parser(options || {}); Object.assign(process.env, options.env || {}); + process.env.HMR_PORT = options.hmrPort; + process.env.HMR_HOSTNAME = options.hmrHostname; callback(); }; diff --git a/test/css.js b/test/css.js index 17e4b2c1277..945204401a7 100644 --- a/test/css.js +++ b/test/css.js @@ -31,7 +31,14 @@ describe('css', function() { assertBundleTree(b, { name: 'index.js', - assets: ['index.js', 'index.css', 'bundle-loader.js', 'bundle-url.js'], + assets: [ + 'index.js', + 'index.css', + 'bundle-loader.js', + 'bundle-url.js', + 'js-loader.js', + 'css-loader.js' + ], childBundles: [ { name: 'index.css', diff --git a/test/html.js b/test/html.js index 24c64f7b0ea..952554426e1 100644 --- a/test/html.js +++ b/test/html.js @@ -166,7 +166,12 @@ describe('html', function() { childBundles: [ { type: 'js', - assets: ['index.css', 'bundle-url.js', 'css-loader.js'], + assets: [ + 'index.css', + 'bundle-url.js', + 'css-loader.js', + 'hmr-runtime.js' + ], childBundles: [] } ] diff --git a/test/integration/wasm-async/.eslintrc b/test/integration/wasm-async/.eslintrc new file mode 100644 index 00000000000..38d01a2adca --- /dev/null +++ b/test/integration/wasm-async/.eslintrc @@ -0,0 +1,6 @@ +{ + "extends": "../.eslintrc.json", + "parserOptions": { + "sourceType": "module" + } +} diff --git a/test/integration/wasm-async/add.wasm b/test/integration/wasm-async/add.wasm new file mode 100644 index 00000000000..357f72da7a0 Binary files /dev/null and b/test/integration/wasm-async/add.wasm differ diff --git a/test/integration/wasm-async/index.js b/test/integration/wasm-async/index.js new file mode 100644 index 00000000000..bdc3b094397 --- /dev/null +++ b/test/integration/wasm-async/index.js @@ -0,0 +1,3 @@ +module.exports = import('./add.wasm').then(function ({add}) { + return add(2, 3); +}); diff --git a/test/integration/wasm-dynamic/.eslintrc b/test/integration/wasm-dynamic/.eslintrc new file mode 100644 index 00000000000..38d01a2adca --- /dev/null +++ b/test/integration/wasm-dynamic/.eslintrc @@ -0,0 +1,6 @@ +{ + "extends": "../.eslintrc.json", + "parserOptions": { + "sourceType": "module" + } +} diff --git a/test/integration/wasm-dynamic/add.wasm b/test/integration/wasm-dynamic/add.wasm new file mode 100644 index 00000000000..357f72da7a0 Binary files /dev/null and b/test/integration/wasm-dynamic/add.wasm differ diff --git a/test/integration/wasm-dynamic/dynamic.js b/test/integration/wasm-dynamic/dynamic.js new file mode 100644 index 00000000000..a7ac4d125a6 --- /dev/null +++ b/test/integration/wasm-dynamic/dynamic.js @@ -0,0 +1,3 @@ +var {add} = require('./add.wasm'); + +module.exports = add; diff --git a/test/integration/wasm-dynamic/index.js b/test/integration/wasm-dynamic/index.js new file mode 100644 index 00000000000..b6d3f9e972c --- /dev/null +++ b/test/integration/wasm-dynamic/index.js @@ -0,0 +1,3 @@ +module.exports = import('./dynamic').then(function (add) { + return add(2, 3); +}); diff --git a/test/integration/wasm-sync/.eslintrc b/test/integration/wasm-sync/.eslintrc new file mode 100644 index 00000000000..38d01a2adca --- /dev/null +++ b/test/integration/wasm-sync/.eslintrc @@ -0,0 +1,6 @@ +{ + "extends": "../.eslintrc.json", + "parserOptions": { + "sourceType": "module" + } +} diff --git a/test/integration/wasm-sync/add.wasm b/test/integration/wasm-sync/add.wasm new file mode 100644 index 00000000000..357f72da7a0 Binary files /dev/null and b/test/integration/wasm-sync/add.wasm differ diff --git a/test/integration/wasm-sync/index.js b/test/integration/wasm-sync/index.js new file mode 100644 index 00000000000..40ecfaba303 --- /dev/null +++ b/test/integration/wasm-sync/index.js @@ -0,0 +1,2 @@ +const {add} = require('./add.wasm'); +output(add(2, 3)); diff --git a/test/javascript.js b/test/javascript.js index 345ff0ebbbf..115ea2e7c85 100644 --- a/test/javascript.js +++ b/test/javascript.js @@ -44,7 +44,7 @@ describe('javascript', function() { assertBundleTree(b, { name: 'index.js', - assets: ['index.js', 'bundle-loader.js', 'bundle-url.js'], + assets: ['index.js', 'bundle-loader.js', 'bundle-url.js', 'js-loader.js'], childBundles: [ { assets: ['local.js'], @@ -84,7 +84,7 @@ describe('javascript', function() { assertBundleTree(b, { name: 'index.js', - assets: ['index.js', 'bundle-loader.js', 'bundle-url.js'], + assets: ['index.js', 'bundle-loader.js', 'bundle-url.js', 'js-loader.js'], childBundles: [ { assets: ['local.js', 'test.txt'], @@ -103,7 +103,7 @@ describe('javascript', function() { assertBundleTree(b, { name: 'index.js', - assets: ['index.js', 'bundle-loader.js', 'bundle-url.js'], + assets: ['index.js', 'bundle-loader.js', 'bundle-url.js', 'js-loader.js'], childBundles: [ { assets: ['local.js'], @@ -127,7 +127,8 @@ describe('javascript', function() { 'common.js', 'common-dep.js', 'bundle-loader.js', - 'bundle-url.js' + 'bundle-url.js', + 'js-loader.js' ], childBundles: [ { diff --git a/test/utils.js b/test/utils.js index 574dd175dc7..2c4454ad311 100644 --- a/test/utils.js +++ b/test/utils.js @@ -47,7 +47,7 @@ function bundle(file, opts) { return bundler(file, opts).bundle(); } -function run(bundle, globals) { +function run(bundle, globals, opts = {}) { // for testing dynamic imports const fakeDocument = { createElement(tag) { @@ -79,7 +79,17 @@ function run(bundle, globals) { document: fakeDocument, WebSocket, console, - location: {hostname: 'localhost'} + location: {hostname: 'localhost'}, + fetch(url) { + return Promise.resolve({ + arrayBuffer() { + return Promise.resolve( + new Uint8Array(fs.readFileSync(path.join(__dirname, 'dist', url))) + .buffer + ); + } + }); + } }, globals ); @@ -88,7 +98,12 @@ function run(bundle, globals) { vm.createContext(ctx); vm.runInContext(fs.readFileSync(bundle.name), ctx); - return ctx.require(bundle.entryAsset.id); + + if (opts.require !== false) { + return ctx.require(bundle.entryAsset.id); + } + + return ctx; } function assertBundleTree(bundle, tree) { @@ -132,9 +147,23 @@ function nextBundle(b) { }); } +function deferred() { + let resolve, reject; + let promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + + promise.resolve = resolve; + promise.reject = reject; + + return promise; +} + exports.sleep = sleep; exports.bundler = bundler; exports.bundle = bundle; exports.run = run; exports.assertBundleTree = assertBundleTree; exports.nextBundle = nextBundle; +exports.deferred = deferred; diff --git a/test/wasm.js b/test/wasm.js new file mode 100644 index 00000000000..aa85c100185 --- /dev/null +++ b/test/wasm.js @@ -0,0 +1,84 @@ +const assert = require('assert'); +const {bundle, run, assertBundleTree, deferred} = require('./utils'); + +describe('wasm', function() { + it('should preload a wasm file for a sync require', async function() { + let b = await bundle(__dirname + '/integration/wasm-sync/index.js'); + + assertBundleTree(b, { + name: 'index.js', + assets: [ + 'index.js', + 'bundle-loader.js', + 'bundle-url.js', + 'wasm-loader.js' + ], + childBundles: [ + { + type: 'wasm', + assets: ['add.wasm'], + childBundles: [] + } + ] + }); + + let promise = deferred(); + run(b, {output: promise.resolve}, {require: false}); + assert.equal(await promise, 5); + }); + + it('should load a wasm file asynchronously with dynamic import', async function() { + let b = await bundle(__dirname + '/integration/wasm-async/index.js'); + + assertBundleTree(b, { + name: 'index.js', + assets: [ + 'index.js', + 'bundle-loader.js', + 'bundle-url.js', + 'wasm-loader.js' + ], + childBundles: [ + { + type: 'wasm', + assets: ['add.wasm'], + childBundles: [] + } + ] + }); + + var res = run(b); + assert.equal(await res, 5); + }); + + it('should load a wasm file in parallel with a dynamic JS import', async function() { + let b = await bundle(__dirname + '/integration/wasm-dynamic/index.js'); + + assertBundleTree(b, { + name: 'index.js', + assets: [ + 'index.js', + 'bundle-loader.js', + 'bundle-url.js', + 'js-loader.js', + 'wasm-loader.js' + ], + childBundles: [ + { + type: 'js', + assets: ['dynamic.js'], + childBundles: [ + { + type: 'wasm', + assets: ['add.wasm'], + childBundles: [] + } + ] + } + ] + }); + + var res = run(b); + assert.equal(await res, 5); + }); +}); diff --git a/test/watcher.js b/test/watcher.js index 8f7b1898af0..9e377a900ac 100644 --- a/test/watcher.js +++ b/test/watcher.js @@ -49,7 +49,8 @@ describe('watcher', function() { 'common.js', 'common-dep.js', 'bundle-loader.js', - 'bundle-url.js' + 'bundle-url.js', + 'js-loader.js' ], childBundles: [ { @@ -73,7 +74,7 @@ describe('watcher', function() { bundle = await nextBundle(b); assertBundleTree(bundle, { name: 'index.js', - assets: ['index.js', 'bundle-loader.js', 'bundle-url.js'], + assets: ['index.js', 'bundle-loader.js', 'bundle-url.js', 'js-loader.js'], childBundles: [ { assets: ['a.js', 'common.js', 'common-dep.js'], @@ -129,7 +130,8 @@ describe('watcher', function() { 'common.js', 'common-dep.js', 'bundle-loader.js', - 'bundle-url.js' + 'bundle-url.js', + 'js-loader.js' ], childBundles: [ { @@ -154,7 +156,13 @@ describe('watcher', function() { bundle = await nextBundle(b); assertBundleTree(bundle, { name: 'index.js', - assets: ['index.js', 'common.js', 'bundle-loader.js', 'bundle-url.js'], + assets: [ + 'index.js', + 'common.js', + 'bundle-loader.js', + 'bundle-url.js', + 'js-loader.js' + ], childBundles: [ { assets: ['a.js'],