diff --git a/fixtures/js/shared_example.js b/fixtures/js/shared_example.js new file mode 100644 index 00000000..68a54f8a --- /dev/null +++ b/fixtures/js/shared_example.js @@ -0,0 +1,5 @@ +// used in a createdSharedEntry() test +require('./no_require'); +require('./requires_arrow_function'); +require('./../css/h1_style.css'); +require('./print_to_app'); diff --git a/index.js b/index.js index 7113f290..122fec35 100644 --- a/index.js +++ b/index.js @@ -392,14 +392,14 @@ class Encore { } /** - * Add a "commons" file that holds JS shared by multiple chunks. + * Add a "commons" file that holds JS shared by multiple chunks/files. * * @param {string} name The chunk name (e.g. vendor to create a vendor.js) - * @param {string|Array} files Array of files to put in the vendor entry + * @param {string} file A file whose code & imports should be put into the shared file. * @returns {Encore} */ - createSharedEntry(name, files) { - webpackConfig.createSharedEntry(name, files); + createSharedEntry(name, file) { + webpackConfig.createSharedEntry(name, file); return this; } diff --git a/lib/WebpackConfig.js b/lib/WebpackConfig.js index 512522a6..16738f1e 100644 --- a/lib/WebpackConfig.js +++ b/lib/WebpackConfig.js @@ -43,6 +43,7 @@ class WebpackConfig { this.publicPath = null; this.manifestKeyPrefix = null; this.sharedCommonsEntryName = null; + this.sharedCommonsEntryFile = null; this.providedVariables = {}; this.configuredFilenames = {}; this.aliases = {}; @@ -317,15 +318,20 @@ class WebpackConfig { this.splitChunksConfigurationCallback = callback; } - createSharedEntry(name, files) { + createSharedEntry(name, file) { // don't allow to call this twice if (this.sharedCommonsEntryName) { throw new Error('createSharedEntry() cannot be called multiple times: you can only create *one* shared entry.'); } + if (Array.isArray(file)) { + throw new Error('Argument 2 to createSharedEntry() must be a single string file: not an array of files. Try creating one file that requires/imports all the modules that should be included.'); + } + this.sharedCommonsEntryName = name; + this.sharedCommonsEntryFile = file; - this.addEntry(name, files); + this.addEntry(name, file); } enablePostCssLoader(postCssLoaderOptionsCallback = () => {}) { diff --git a/lib/config-generator.js b/lib/config-generator.js index 2a0b2aa4..4f56b17a 100644 --- a/lib/config-generator.js +++ b/lib/config-generator.js @@ -37,8 +37,12 @@ const vuePluginUtil = require('./plugins/vue'); const friendlyErrorPluginUtil = require('./plugins/friendly-errors'); const assetOutputDisplay = require('./plugins/asset-output-display'); const notifierPluginUtil = require('./plugins/notifier'); +const sharedEntryConcatPuginUtil = require('./plugins/shared-entry-concat'); const PluginPriorities = require('./plugins/plugin-priorities'); const applyOptionsCallback = require('./utils/apply-options-callback'); +const tmp = require('tmp'); +const fs = require('fs'); +const path = require('path'); class ConfigGenerator { /** @@ -114,6 +118,24 @@ class ConfigGenerator { entry[entryName] = entryChunks; } + if (this.webpackConfig.sharedCommonsEntryName) { + /* + * This is a hack: we need to create a new "entry" + * file that simply requires the same file that + * the "shared entry" requires. + * + * See shared-entry-concat-plugin.js for more details. + */ + const tmpFileObject = tmp.fileSync(); + fs.writeFileSync( + tmpFileObject.name, + // quotes in the filename would cause problems + `require('${path.resolve(this.webpackConfig.getContext(), this.webpackConfig.sharedCommonsEntryFile)}')` + ); + + entry._tmp_shared = tmpFileObject.name; + } + return entry; } @@ -301,6 +323,8 @@ class ConfigGenerator { assetOutputDisplay(plugins, this.webpackConfig, friendlyErrorPlugin); } + sharedEntryConcatPuginUtil(plugins, this.webpackConfig); + this.webpackConfig.plugins.forEach(function(plugin) { plugins.push(plugin); }); diff --git a/lib/plugins/plugin-priorities.js b/lib/plugins/plugin-priorities.js index 6c62d72b..8455aa7e 100644 --- a/lib/plugins/plugin-priorities.js +++ b/lib/plugins/plugin-priorities.js @@ -14,6 +14,7 @@ module.exports = { DeleteUnusedEntriesJSPlugin: 0, EntryFilesManifestPlugin: 0, WebpackManifestPlugin: 0, + SharedEntryContactPlugin: 0, LoaderOptionsPlugin: 0, ProvidePlugin: 0, CleanWebpackPlugin: 0, diff --git a/lib/plugins/shared-entry-concat.js b/lib/plugins/shared-entry-concat.js new file mode 100644 index 00000000..7ca3113a --- /dev/null +++ b/lib/plugins/shared-entry-concat.js @@ -0,0 +1,33 @@ +/* + * This file is part of the Symfony Webpack Encore package. + * + * (c) Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +'use strict'; + +const SharedEntryConcatPlugin = require('../webpack/shared-entry-concat-plugin'); +const PluginPriorities = require('./plugin-priorities'); +const path = require('path'); + +/** + * @param {Array} plugins + * @param {WebpackConfig} webpackConfig + * @return {void} + */ +module.exports = function(plugins, webpackConfig) { + if (!webpackConfig.sharedCommonsEntryName) { + return; + } + + plugins.push({ + plugin: new SharedEntryConcatPlugin( + webpackConfig.sharedCommonsEntryName, + webpackConfig.outputPath + ), + priority: PluginPriorities.SharedEntryContactPlugin + }); +}; diff --git a/lib/webpack/shared-entry-concat-plugin.js b/lib/webpack/shared-entry-concat-plugin.js new file mode 100644 index 00000000..ed0014e6 --- /dev/null +++ b/lib/webpack/shared-entry-concat-plugin.js @@ -0,0 +1,68 @@ +/* + * This file is part of the Symfony Webpack Encore package. + * + * (c) Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +function SharedEntryConcatPlugin(sharedEntryName, buildDir) { + this.sharedEntryName = sharedEntryName; + this.buildDir = buildDir; +} + +SharedEntryConcatPlugin.prototype.apply = function(compiler) { + const done = (stats) => { + if (stats.hasErrors()) { + return; + } + + /* + * This is a hack. See ConfigGenerator.buildEntryConfig() + * for other details. + * + * Basically, the "_tmp_shared" entry is created automatically + * as a "fake" entry. Internally, it simply requires the same + * file that is the source file of the shared entry. + * + * In this plugin, we literally read the final, compiled _tmp_shared.js + * entry, and put its contents at the bottom of the final, compiled, + * shared commons file. Then, we delete _tmp_shared.js. This + * is because the shared entry is actually "removed" as an entry + * file in SplitChunksPlugin, which means that if it contains + * any code that should be executed, that code is not normally + * executed. This fixes that. + */ + + const sharedEntryOutputFile = path.join(this.buildDir, this.sharedEntryName + '.js'); + const tmpEntryBootstrapFile = path.join(this.buildDir, '_tmp_shared.js'); + + if (!fs.existsSync(sharedEntryOutputFile)) { + throw new Error(`Could not find shared entry output file: ${sharedEntryOutputFile}`); + } + + if (!fs.existsSync(tmpEntryBootstrapFile)) { + throw new Error(`Could not find temporary shared entry bootstrap file: ${tmpEntryBootstrapFile}`); + } + + fs.writeFileSync( + sharedEntryOutputFile, + fs.readFileSync(sharedEntryOutputFile) + fs.readFileSync(tmpEntryBootstrapFile) + ); + + fs.unlinkSync(tmpEntryBootstrapFile); + }; + + compiler.hooks.done.tap( + { name: 'SharedEntryConcatPlugin' }, + done + ); +}; + +module.exports = SharedEntryConcatPlugin; diff --git a/package.json b/package.json index 0f2cc76b..77058f50 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "sinon": "^2.3.4", "stylus": "^0.54.5", "stylus-loader": "^3.0.2", + "tmp": "^0.0.33", "ts-loader": "^4.3.0", "typescript": "^2.3.4", "url-loader": "^1.0.1", diff --git a/test/functional.js b/test/functional.js index 26457e70..7c65a91d 100644 --- a/test/functional.js +++ b/test/functional.js @@ -610,7 +610,7 @@ describe('Functional tests using webpack', function() { config.setPublicPath('/build'); config.addEntry('main', ['./js/no_require', './js/code_splitting', './js/arrow_function', './js/print_to_app']); config.addEntry('other', ['./js/no_require', './css/h1_style.css']); - config.createSharedEntry('shared', ['./js/no_require', './js/requires_arrow_function', './css/h1_style.css']); + config.createSharedEntry('shared', './js/shared_example'); testSetup.runWebpack(config, (webpackAssert) => { // check the file is extracted correctly @@ -650,10 +650,9 @@ describe('Functional tests using webpack', function() { [ 'build/runtime.js', 'build/shared.js', - 'build/main.js' ], (browser) => { - // assert that the javascript executed + // assert that the javascript brought into shared is executed browser.assert.text('#app', 'Welcome to Encore!'); done(); }