diff --git a/README.md b/README.md index a32a257..2fced52 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,43 @@ The previous configuration will expect and build the files: - `src/index.html` => `dist/index.html` - `src/index.js` => `dist/index.js` +#### Excluding a page from chunks + +If you want a page to be excluded from either the [vendor](#chunksvendor) or [common](#chunkscommon) chunk, then you can do so by providing an object with a `name` and `independent` flag (set to `true`) instead of just the name of the page. + +```js +// sagui.config.js +module.exports = { + pages: ['index', 'about', { name: 'demo', independent: true }] +} +``` + +### `chunks.vendor` + +If you want all your external dependencies (`node_modules`) in your [pages](#pages) to be bundled together in a "vendor" chunk, then set this flag to `true`. By default it is set to `false`. + +```js +// sagui.config.js +module.exports = { + chunks: { + vendor: true + } +} +``` + +### `chunks.common` + +If you do not want all the common dependencies of your [pages](#pages) to be bundled together in a "common" chunk, then set this flag to `false`. By default it is set to `true`. + +```js +// sagui.config.js +module.exports = { + chunks: { + common: false + } +} +``` + ### `libraries` Create **reusable libraries** that can be shared across applications. Sagui will take care of the build process so that external libraries are not bundled and that you have a CommonJS module as the output. diff --git a/package.json b/package.json index f6b3b2c..0efc2be 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sagui", - "version": "11.4.2", + "version": "11.5.0", "description": "Front-end tooling in a single dependency", "preferGlobal": false, "bin": { @@ -111,6 +111,7 @@ "karma-phantomjs-launcher": "^1.0.4", "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^2.0.3", + "lodash.merge": "^4.6.0", "lodash.uniq": "^4.5.0", "node-sass": "^4.5.2", "null-loader": "^0.1.1", @@ -125,7 +126,7 @@ "url-loader": "^0.5.8", "webpack": "^3.2.0", "webpack-dev-server": "^2.5.1", - "webpack-md5-hash": "0.0.5", + "webpack-md5-hash": "0.0.6", "webpack-merge": "^4.1.0", "yaml-loader": "^0.5.0" } diff --git a/spec/fixtures/project-content-with-invalid-import/src/index.spec.js b/spec/fixtures/project-content-with-invalid-import/src/index.spec.js new file mode 100644 index 0000000..0c07d55 --- /dev/null +++ b/spec/fixtures/project-content-with-invalid-import/src/index.spec.js @@ -0,0 +1,5 @@ +import('name-that-is-invalid') + +describe('my project', function() { + it('should work not work at all', function() {}) +}) diff --git a/spec/fixtures/project-with-independent-page/node_modules/.DS_Store b/spec/fixtures/project-with-independent-page/node_modules/.DS_Store new file mode 100644 index 0000000..5cd123b Binary files /dev/null and b/spec/fixtures/project-with-independent-page/node_modules/.DS_Store differ diff --git a/spec/fixtures/project-with-independent-page/node_modules/dependencyA/.DS_Store b/spec/fixtures/project-with-independent-page/node_modules/dependencyA/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/spec/fixtures/project-with-independent-page/node_modules/dependencyA/.DS_Store differ diff --git a/spec/fixtures/project-with-independent-page/node_modules/dependencyA/index.js b/spec/fixtures/project-with-independent-page/node_modules/dependencyA/index.js new file mode 100644 index 0000000..56232d1 --- /dev/null +++ b/spec/fixtures/project-with-independent-page/node_modules/dependencyA/index.js @@ -0,0 +1,3 @@ +module.exports = function () { + return console.log('dependencyA') +} diff --git a/spec/fixtures/project-with-independent-page/node_modules/dependencyA/package.json b/spec/fixtures/project-with-independent-page/node_modules/dependencyA/package.json new file mode 100644 index 0000000..c8ce753 --- /dev/null +++ b/spec/fixtures/project-with-independent-page/node_modules/dependencyA/package.json @@ -0,0 +1,14 @@ +{ + "name": "dependencyA", + "version": "1.0.0", + "description": "", + "main": "index.js", + "dependencies": {}, + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/spec/fixtures/project-with-independent-page/node_modules/dependencyB/index.js b/spec/fixtures/project-with-independent-page/node_modules/dependencyB/index.js new file mode 100644 index 0000000..a449133 --- /dev/null +++ b/spec/fixtures/project-with-independent-page/node_modules/dependencyB/index.js @@ -0,0 +1,3 @@ +module.exports = function () { + return console.log('dependencyB') +} diff --git a/spec/fixtures/project-with-independent-page/node_modules/dependencyB/package.json b/spec/fixtures/project-with-independent-page/node_modules/dependencyB/package.json new file mode 100644 index 0000000..f675a78 --- /dev/null +++ b/spec/fixtures/project-with-independent-page/node_modules/dependencyB/package.json @@ -0,0 +1,14 @@ +{ + "name": "dependencyB", + "version": "1.0.0", + "description": "", + "main": "index.js", + "dependencies": {}, + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/spec/fixtures/project-with-independent-page/sagui.config.js b/spec/fixtures/project-with-independent-page/sagui.config.js new file mode 100644 index 0000000..206d080 --- /dev/null +++ b/spec/fixtures/project-with-independent-page/sagui.config.js @@ -0,0 +1,7 @@ +module.exports = { + pages: ['index', 'about', { name: 'demo', independent: true }], + chunks: { + vendor: true, + common: true, + } +} diff --git a/spec/fixtures/project-with-independent-page/src/about.html b/spec/fixtures/project-with-independent-page/src/about.html new file mode 100644 index 0000000..08824ab --- /dev/null +++ b/spec/fixtures/project-with-independent-page/src/about.html @@ -0,0 +1,11 @@ + + + + Hello + + + + +
+ + diff --git a/spec/fixtures/project-with-independent-page/src/about.js b/spec/fixtures/project-with-independent-page/src/about.js new file mode 100644 index 0000000..cc26831 --- /dev/null +++ b/spec/fixtures/project-with-independent-page/src/about.js @@ -0,0 +1,5 @@ +import shared from './shared' +import dependencyA from 'dependencyA' + +shared() +dependencyA() diff --git a/spec/fixtures/project-with-independent-page/src/demo.html b/spec/fixtures/project-with-independent-page/src/demo.html new file mode 100644 index 0000000..08824ab --- /dev/null +++ b/spec/fixtures/project-with-independent-page/src/demo.html @@ -0,0 +1,11 @@ + + + + Hello + + + + +
+ + diff --git a/spec/fixtures/project-with-independent-page/src/demo.js b/spec/fixtures/project-with-independent-page/src/demo.js new file mode 100644 index 0000000..12daf59 --- /dev/null +++ b/spec/fixtures/project-with-independent-page/src/demo.js @@ -0,0 +1,7 @@ +import shared from './shared' +import dependencyA from 'dependencyA' +import dependencyB from 'dependencyB' + +shared() +dependencyA() +dependencyB() diff --git a/spec/fixtures/project-with-independent-page/src/index.html b/spec/fixtures/project-with-independent-page/src/index.html new file mode 100644 index 0000000..08824ab --- /dev/null +++ b/spec/fixtures/project-with-independent-page/src/index.html @@ -0,0 +1,11 @@ + + + + Hello + + + + +
+ + diff --git a/spec/fixtures/project-with-independent-page/src/index.js b/spec/fixtures/project-with-independent-page/src/index.js new file mode 100644 index 0000000..67de6da --- /dev/null +++ b/spec/fixtures/project-with-independent-page/src/index.js @@ -0,0 +1,5 @@ +import shared from './shared' +import dependencyB from 'dependencyB' + +shared() +dependencyB() diff --git a/spec/fixtures/project-with-independent-page/src/shared.js b/spec/fixtures/project-with-independent-page/src/shared.js new file mode 100644 index 0000000..2ba7300 --- /dev/null +++ b/spec/fixtures/project-with-independent-page/src/shared.js @@ -0,0 +1 @@ +export default () => console.log('shared module') diff --git a/spec/fixtures/project-with-two-pages-disabled-common/sagui.config.js b/spec/fixtures/project-with-two-pages-disabled-common/sagui.config.js new file mode 100644 index 0000000..cf5fe22 --- /dev/null +++ b/spec/fixtures/project-with-two-pages-disabled-common/sagui.config.js @@ -0,0 +1,6 @@ +module.exports = { + pages: ['index', 'about'], + chunks: { + common: false + } +} diff --git a/spec/fixtures/project-with-two-pages-disabled-common/src/about.html b/spec/fixtures/project-with-two-pages-disabled-common/src/about.html new file mode 100644 index 0000000..08824ab --- /dev/null +++ b/spec/fixtures/project-with-two-pages-disabled-common/src/about.html @@ -0,0 +1,11 @@ + + + + Hello + + + + +
+ + diff --git a/spec/fixtures/project-with-two-pages-disabled-common/src/about.js b/spec/fixtures/project-with-two-pages-disabled-common/src/about.js new file mode 100644 index 0000000..1781aab --- /dev/null +++ b/spec/fixtures/project-with-two-pages-disabled-common/src/about.js @@ -0,0 +1,3 @@ +import shared from './shared' + +shared() diff --git a/spec/fixtures/project-with-two-pages-disabled-common/src/index.html b/spec/fixtures/project-with-two-pages-disabled-common/src/index.html new file mode 100644 index 0000000..08824ab --- /dev/null +++ b/spec/fixtures/project-with-two-pages-disabled-common/src/index.html @@ -0,0 +1,11 @@ + + + + Hello + + + + +
+ + diff --git a/spec/fixtures/project-with-two-pages-disabled-common/src/index.js b/spec/fixtures/project-with-two-pages-disabled-common/src/index.js new file mode 100644 index 0000000..1781aab --- /dev/null +++ b/spec/fixtures/project-with-two-pages-disabled-common/src/index.js @@ -0,0 +1,3 @@ +import shared from './shared' + +shared() diff --git a/spec/fixtures/project-with-two-pages-disabled-common/src/shared.js b/spec/fixtures/project-with-two-pages-disabled-common/src/shared.js new file mode 100644 index 0000000..2ba7300 --- /dev/null +++ b/spec/fixtures/project-with-two-pages-disabled-common/src/shared.js @@ -0,0 +1 @@ +export default () => console.log('shared module') diff --git a/spec/fixtures/project-with-two-pages-enabled-vendor/node_modules/.DS_Store b/spec/fixtures/project-with-two-pages-enabled-vendor/node_modules/.DS_Store new file mode 100644 index 0000000..84549d0 Binary files /dev/null and b/spec/fixtures/project-with-two-pages-enabled-vendor/node_modules/.DS_Store differ diff --git a/spec/fixtures/project-with-two-pages-enabled-vendor/node_modules/dependencyA/.DS_Store b/spec/fixtures/project-with-two-pages-enabled-vendor/node_modules/dependencyA/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/spec/fixtures/project-with-two-pages-enabled-vendor/node_modules/dependencyA/.DS_Store differ diff --git a/spec/fixtures/project-with-two-pages-enabled-vendor/node_modules/dependencyA/index.js b/spec/fixtures/project-with-two-pages-enabled-vendor/node_modules/dependencyA/index.js new file mode 100644 index 0000000..56232d1 --- /dev/null +++ b/spec/fixtures/project-with-two-pages-enabled-vendor/node_modules/dependencyA/index.js @@ -0,0 +1,3 @@ +module.exports = function () { + return console.log('dependencyA') +} diff --git a/spec/fixtures/project-with-two-pages-enabled-vendor/node_modules/dependencyA/package.json b/spec/fixtures/project-with-two-pages-enabled-vendor/node_modules/dependencyA/package.json new file mode 100644 index 0000000..c8ce753 --- /dev/null +++ b/spec/fixtures/project-with-two-pages-enabled-vendor/node_modules/dependencyA/package.json @@ -0,0 +1,14 @@ +{ + "name": "dependencyA", + "version": "1.0.0", + "description": "", + "main": "index.js", + "dependencies": {}, + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/spec/fixtures/project-with-two-pages-enabled-vendor/node_modules/dependencyB/index.js b/spec/fixtures/project-with-two-pages-enabled-vendor/node_modules/dependencyB/index.js new file mode 100644 index 0000000..a449133 --- /dev/null +++ b/spec/fixtures/project-with-two-pages-enabled-vendor/node_modules/dependencyB/index.js @@ -0,0 +1,3 @@ +module.exports = function () { + return console.log('dependencyB') +} diff --git a/spec/fixtures/project-with-two-pages-enabled-vendor/node_modules/dependencyB/package.json b/spec/fixtures/project-with-two-pages-enabled-vendor/node_modules/dependencyB/package.json new file mode 100644 index 0000000..f675a78 --- /dev/null +++ b/spec/fixtures/project-with-two-pages-enabled-vendor/node_modules/dependencyB/package.json @@ -0,0 +1,14 @@ +{ + "name": "dependencyB", + "version": "1.0.0", + "description": "", + "main": "index.js", + "dependencies": {}, + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/spec/fixtures/project-with-two-pages-enabled-vendor/sagui.config.js b/spec/fixtures/project-with-two-pages-enabled-vendor/sagui.config.js new file mode 100644 index 0000000..911978b --- /dev/null +++ b/spec/fixtures/project-with-two-pages-enabled-vendor/sagui.config.js @@ -0,0 +1,6 @@ +module.exports = { + pages: ['index', 'about'], + chunks: { + vendor: true + } +} diff --git a/spec/fixtures/project-with-two-pages-enabled-vendor/src/about.html b/spec/fixtures/project-with-two-pages-enabled-vendor/src/about.html new file mode 100644 index 0000000..08824ab --- /dev/null +++ b/spec/fixtures/project-with-two-pages-enabled-vendor/src/about.html @@ -0,0 +1,11 @@ + + + + Hello + + + + +
+ + diff --git a/spec/fixtures/project-with-two-pages-enabled-vendor/src/about.js b/spec/fixtures/project-with-two-pages-enabled-vendor/src/about.js new file mode 100644 index 0000000..cc26831 --- /dev/null +++ b/spec/fixtures/project-with-two-pages-enabled-vendor/src/about.js @@ -0,0 +1,5 @@ +import shared from './shared' +import dependencyA from 'dependencyA' + +shared() +dependencyA() diff --git a/spec/fixtures/project-with-two-pages-enabled-vendor/src/index.html b/spec/fixtures/project-with-two-pages-enabled-vendor/src/index.html new file mode 100644 index 0000000..08824ab --- /dev/null +++ b/spec/fixtures/project-with-two-pages-enabled-vendor/src/index.html @@ -0,0 +1,11 @@ + + + + Hello + + + + +
+ + diff --git a/spec/fixtures/project-with-two-pages-enabled-vendor/src/index.js b/spec/fixtures/project-with-two-pages-enabled-vendor/src/index.js new file mode 100644 index 0000000..67de6da --- /dev/null +++ b/spec/fixtures/project-with-two-pages-enabled-vendor/src/index.js @@ -0,0 +1,5 @@ +import shared from './shared' +import dependencyB from 'dependencyB' + +shared() +dependencyB() diff --git a/spec/fixtures/project-with-two-pages-enabled-vendor/src/shared.js b/spec/fixtures/project-with-two-pages-enabled-vendor/src/shared.js new file mode 100644 index 0000000..2ba7300 --- /dev/null +++ b/spec/fixtures/project-with-two-pages-enabled-vendor/src/shared.js @@ -0,0 +1 @@ +export default () => console.log('shared module') diff --git a/spec/fixtures/project-with-two-pages/sagui.config.js b/spec/fixtures/project-with-two-pages/sagui.config.js new file mode 100644 index 0000000..74ab9d0 --- /dev/null +++ b/spec/fixtures/project-with-two-pages/sagui.config.js @@ -0,0 +1,3 @@ +module.exports = { + pages: ['index', 'about'] +} diff --git a/spec/fixtures/project-with-two-pages/src/about.html b/spec/fixtures/project-with-two-pages/src/about.html new file mode 100644 index 0000000..08824ab --- /dev/null +++ b/spec/fixtures/project-with-two-pages/src/about.html @@ -0,0 +1,11 @@ + + + + Hello + + + + +
+ + diff --git a/spec/fixtures/project-with-two-pages/src/about.js b/spec/fixtures/project-with-two-pages/src/about.js new file mode 100644 index 0000000..1781aab --- /dev/null +++ b/spec/fixtures/project-with-two-pages/src/about.js @@ -0,0 +1,3 @@ +import shared from './shared' + +shared() diff --git a/spec/fixtures/project-with-two-pages/src/index.html b/spec/fixtures/project-with-two-pages/src/index.html new file mode 100644 index 0000000..08824ab --- /dev/null +++ b/spec/fixtures/project-with-two-pages/src/index.html @@ -0,0 +1,11 @@ + + + + Hello + + + + +
+ + diff --git a/spec/fixtures/project-with-two-pages/src/index.js b/spec/fixtures/project-with-two-pages/src/index.js new file mode 100644 index 0000000..1781aab --- /dev/null +++ b/spec/fixtures/project-with-two-pages/src/index.js @@ -0,0 +1,3 @@ +import shared from './shared' + +shared() diff --git a/spec/fixtures/project-with-two-pages/src/shared.js b/spec/fixtures/project-with-two-pages/src/shared.js new file mode 100644 index 0000000..2ba7300 --- /dev/null +++ b/spec/fixtures/project-with-two-pages/src/shared.js @@ -0,0 +1 @@ +export default () => console.log('shared module') diff --git a/spec/integration/index.integration-spec.js b/spec/integration/index.integration-spec.js index b89b829..6012b32 100644 --- a/spec/integration/index.integration-spec.js +++ b/spec/integration/index.integration-spec.js @@ -41,6 +41,8 @@ describe('[integration] sagui', function () { const projectContentCustomPrettierOptionsInEslintrc = path.join(__dirname, '../fixtures/project-content-with-custom-prettier-options-in-eslintrc') const projectContentWithDynamicImports = path.join(__dirname, '../fixtures/project-content-with-dynamic-import') const projectContentWithCaseMismatchInModulePath = path.join(__dirname, '../fixtures/project-with-case-mismatch-in-module-paths') + const projectContentWithInvalidImports = path.join(__dirname, '../fixtures/project-content-with-invalid-import') + let projectPath, projectSrcPath beforeEach(function () { @@ -137,6 +139,106 @@ describe('[integration] sagui', function () { }) }) + describe('chunks', () => { + describe('project with two pages on build', () => { + const projectFixture = path.join(__dirname, '../fixtures/project-with-two-pages') + beforeEach(function () { + fs.copySync(projectFixture, projectPath, { overwrite: true }) + return sagui({ projectPath, action: actions.BUILD }) + }) + + it('should have created a common.js file', () => { + const files = fs.readdirSync(path.join(projectPath, 'dist')) + expect(files.filter((file) => file.match(/common.+\.js$/))).not.to.be.empty + }) + + it('should NOT have created a vendor.js file', () => { + const files = fs.readdirSync(path.join(projectPath, 'dist')) + expect(files.filter((file) => file.match(/vendor.+\.js$/))).to.be.empty + }) + }) + + describe('project with two pages and disabled common chunks on build', () => { + const projectFixture = path.join(__dirname, '../fixtures/project-with-two-pages-disabled-common') + beforeEach(function () { + fs.copySync(projectFixture, projectPath, { overwrite: true }) + return sagui({ projectPath, action: actions.BUILD }) + }) + + it('should not have created a common.js file', () => { + const files = fs.readdirSync(path.join(projectPath, 'dist')) + expect(files.filter((file) => file.match(/common.+\.js$/))).to.be.empty + }) + + it('should NOT have created a vendor.js file', () => { + const files = fs.readdirSync(path.join(projectPath, 'dist')) + expect(files.filter((file) => file.match(/vendor.+\.js$/))).to.be.empty + }) + }) + + describe('project with two pages and enabled vendor chunks on build', () => { + const projectFixture = path.join(__dirname, '../fixtures/project-with-two-pages-enabled-vendor') + beforeEach(function () { + fs.copySync(projectFixture, projectPath, { overwrite: true }) + return sagui({ projectPath, action: actions.BUILD }) + }) + + it('should have created a common.js file without the content of the node_modules dependencies', () => { + const files = fs.readdirSync(path.join(projectPath, 'dist')) + const commonFiles = files.filter((file) => file.match(/common.+\.js$/)) + const commonContent = fs.readFileSync(path.join(projectPath, 'dist', commonFiles[0])).toString() + expect(commonFiles).not.to.be.empty + expect(commonContent).not.to.have.string('dependencyA') + expect(commonContent).not.to.have.string('dependencyB') + }) + + it('should have created a vendor.js file with the content of the node_modules dependencies', () => { + const files = fs.readdirSync(path.join(projectPath, 'dist')) + const vendorFiles = files.filter((file) => file.match(/vendor.+\.js$/)) + const vendorContent = fs.readFileSync(path.join(projectPath, 'dist', vendorFiles[0])).toString() + expect(vendorFiles).not.to.be.empty + expect(vendorContent).to.have.string('dependencyA') + expect(vendorContent).to.have.string('dependencyB') + }) + }) + + describe('project with two pages but one that is independent with vendor and common chunks enabled', () => { + const projectFixture = path.join(__dirname, '../fixtures/project-with-independent-page') + beforeEach(function () { + fs.copySync(projectFixture, projectPath, { overwrite: true }) + return sagui({ projectPath, action: actions.BUILD }) + }) + + it('should have created a common.js file without the content of the node_modules dependencies, but with the shared content', () => { + const files = fs.readdirSync(path.join(projectPath, 'dist')) + const commonFiles = files.filter((file) => file.match(/common.+\.js$/)) + const commonContent = fs.readFileSync(path.join(projectPath, 'dist', commonFiles[0])).toString() + expect(commonFiles).not.to.be.empty + expect(commonContent).not.to.have.string('dependencyA') + expect(commonContent).not.to.have.string('dependencyB') + expect(commonContent).to.have.string('shared') + }) + + it('should have created a vendor.js file with the content of the node_modules dependencies', () => { + const files = fs.readdirSync(path.join(projectPath, 'dist')) + const vendorFiles = files.filter((file) => file.match(/vendor.+\.js$/)) + const vendorContent = fs.readFileSync(path.join(projectPath, 'dist', vendorFiles[0])).toString() + expect(vendorFiles).not.to.be.empty + expect(vendorContent).to.have.string('dependencyA') + expect(vendorContent).to.have.string('dependencyB') + }) + + it('should include all dependencies and shared code in the independent page', () => { + const files = fs.readdirSync(path.join(projectPath, 'dist')) + const aboutFiles = files.filter((file) => file.match(/demo.+\.js$/)) + const aboutContent = fs.readFileSync(path.join(projectPath, 'dist', aboutFiles[0])).toString() + expect(aboutContent).to.have.string('dependencyA') + expect(aboutContent).to.have.string('dependencyB') + expect(aboutContent).to.have.string('shared') + }) + }) + }) + describe('project with duplicated transient dependencies and colliding node_modules', () => { const projectWithNodeModules = path.join(__dirname, '../fixtures/project-with-node-modules') beforeEach(function () { @@ -382,5 +484,16 @@ npm-debug.log`) ) }) }) + + describe('when there are invalid imports', () => { + it('should not hang the test runner', async () => { + fs.copySync(projectContentWithInvalidImports, projectPath, { overwrite: true }) + await sagui({ projectPath, action: actions.TEST_UNIT }) + .then( + () => { throw new Error('It should have failed') }, + (error) => expect(error).to.eql(1) + ) + }) + }) }) }) diff --git a/src/configure-webpack/build-pages-config.js b/src/configure-webpack/build-pages-config.js index e936fed..87ace7f 100644 --- a/src/configure-webpack/build-pages-config.js +++ b/src/configure-webpack/build-pages-config.js @@ -3,11 +3,11 @@ import { join } from 'path' import { optimize } from 'webpack' import actions from '../actions' -export default (pages = [], { action, projectPath }) => { +export default (pages = [], { action, chunks = {}, projectPath }) => { if (pages.length === 0 || action === actions.TEST_UNIT) { return {} } const entry = configureEntry(pages) - const plugins = configurePlugins(pages, action) + const plugins = configurePlugins(pages, chunks) const filenamePattern = action === actions.BUILD ? '[name]-[chunkhash]' : '[name]' // For better performance during development @@ -27,24 +27,63 @@ function configureEntry (pages) { let entry = {} pages.forEach((page) => { - entry[page] = [`./${page}`] + const pageName = getPageName(page) + entry[pageName] = [`./${pageName}`] }) return entry } -function configurePlugins (pages, action) { +function configurePlugins (pages, chunksConfig) { const plugins = pages.map((page) => { + const pageName = getPageName(page) + const chunks = Object.keys(chunksConfig) + .reduce((chunks, key) => { + if (chunksConfig[key]) { + chunks.unshift(key) + } + return chunks + }, [pageName]) + return new HtmlWebpackPlugin({ - template: `${page}.html`, - filename: `${page}.html`, - chunks: ['common', page] + template: `${pageName}.html`, + filename: `${pageName}.html`, + chunks }) }) if (pages.length > 1) { - plugins.push(new optimize.CommonsChunkPlugin({ name: 'common', minChunks: 2 })) + const chunks = pages.reduce((chunks, page) => { + if (typeof page === 'string') { + chunks.push(page) + } + if (page.independent !== true) { + chunks.push(page.name) + } + return chunks + }, []) + + if (chunksConfig.vendor) { + plugins.push(new optimize.CommonsChunkPlugin({ + name: 'vendor', + minChunks: (module) => { + return /node_modules/.test(module.context) + }, + chunks + })) + } + if (chunksConfig.common) { + plugins.push(new optimize.CommonsChunkPlugin({ + name: 'common', + minChunks: 2, + chunks + })) + } } return plugins } + +function getPageName (page) { + return (typeof page === 'string' ? page : page.name) +} diff --git a/src/configure-webpack/build-pages-config.spec.js b/src/configure-webpack/build-pages-config.spec.js index 4a33e77..44a3d57 100644 --- a/src/configure-webpack/build-pages-config.spec.js +++ b/src/configure-webpack/build-pages-config.spec.js @@ -41,7 +41,7 @@ describe('pages webpack preset', function () { }) it('should have a plugin setting up the HTML template', function () { - const webpackConfig = buildPagesConfig(['index'], baseConfig) + const webpackConfig = buildPagesConfig(['index'], { chunks: { common: true }, ...baseConfig }) const html = webpackConfig.plugins.filter((plugin) => plugin instanceof HtmlWebpackPlugin) expect(html.length).equal(1) @@ -73,35 +73,127 @@ describe('pages webpack preset', function () { }) }) - it('should have a plugin setting up the HTML template for each chunk', function () { - const webpackConfig = buildPagesConfig(['index', 'demo'], baseConfig) + describe('when chunks.common is "true"', function () { + it('should have a plugin setting up the HTML template for each chunk', function () { + const webpackConfig = buildPagesConfig(['index', 'demo'], { chunks: { common: true }, ...baseConfig }) - const html = webpackConfig.plugins.filter((plugin) => plugin instanceof HtmlWebpackPlugin) - expect(html.length).equal(2) + const html = webpackConfig.plugins.filter((plugin) => plugin instanceof HtmlWebpackPlugin) + expect(html.length).equal(2) - const firstOptions = html[0].options - expect(firstOptions.chunks).deep.eql([ 'common', 'index' ]) - expect(firstOptions.filename).deep.eql('index.html') - expect(firstOptions.template).deep.eql('index.html') + const firstOptions = html[0].options + expect(firstOptions.chunks).deep.eql([ 'common', 'index' ]) + expect(firstOptions.filename).deep.eql('index.html') + expect(firstOptions.template).deep.eql('index.html') - const secondOptions = html[1].options - expect(secondOptions.chunks).deep.eql([ 'common', 'demo' ]) - expect(secondOptions.filename).deep.eql('demo.html') - expect(secondOptions.template).deep.eql('demo.html') - }) + const secondOptions = html[1].options + expect(secondOptions.chunks).deep.eql([ 'common', 'demo' ]) + expect(secondOptions.filename).deep.eql('demo.html') + expect(secondOptions.template).deep.eql('demo.html') + }) - it('should have the CommonsChunkPlugin enabled', function () { - const webpackConfig = buildPagesConfig(['index', 'demo'], baseConfig) + it('should have the CommonsChunkPlugin enabled', function () { + const webpackConfig = buildPagesConfig(['index', 'demo'], { chunks: { common: true }, ...baseConfig }) - const commons = webpackConfig.plugins.filter((plugin) => plugin instanceof optimize.CommonsChunkPlugin) - expect(commons.length).equal(1) + const commons = webpackConfig.plugins.filter((plugin) => plugin instanceof optimize.CommonsChunkPlugin) + expect(commons.length).equal(1) + }) + + it('should set name to "common" in the CommonsChunkPlugin', function () { + const webpackConfig = buildPagesConfig(['index', 'demo'], { chunks: { common: true }, ...baseConfig }) + + const commons = webpackConfig.plugins.filter((plugin) => plugin instanceof optimize.CommonsChunkPlugin) + expect(commons[0].chunkNames[0]).equal('common') + }) + + it('should set minChunks to 2 in the CommonsChunkPlugin', function () { + const webpackConfig = buildPagesConfig(['index', 'demo'], { chunks: { common: true }, ...baseConfig }) + + const commons = webpackConfig.plugins.filter((plugin) => plugin instanceof optimize.CommonsChunkPlugin) + expect(commons[0].minChunks).equal(2) + }) + + it('should set pages as chunks in the CommonsChunkPlugin', function () { + const webpackConfig = buildPagesConfig(['index', 'demo'], { chunks: { common: true }, ...baseConfig }) + + const commons = webpackConfig.plugins.filter((plugin) => plugin instanceof optimize.CommonsChunkPlugin) + expect(commons[0].selectedChunks[0]).equal('index') + expect(commons[0].selectedChunks[2]).equal('demo') + }) }) - it('should set minChunks to 2 in the CommonsChunkPlugin', function () { - const webpackConfig = buildPagesConfig(['index', 'demo'], baseConfig) + describe('when chunks.vendor is "true"', function () { + it('should have a plugin setting up the HTML template for each chunk', function () { + const webpackConfig = buildPagesConfig(['index', 'demo'], { chunks: { vendor: true }, ...baseConfig }) - const commons = webpackConfig.plugins.filter((plugin) => plugin instanceof optimize.CommonsChunkPlugin) - expect(commons[0].minChunks).equal(2) + const html = webpackConfig.plugins.filter((plugin) => plugin instanceof HtmlWebpackPlugin) + expect(html.length).equal(2) + + const firstOptions = html[0].options + expect(firstOptions.chunks).deep.eql([ 'vendor', 'index' ]) + expect(firstOptions.filename).deep.eql('index.html') + expect(firstOptions.template).deep.eql('index.html') + + const secondOptions = html[1].options + expect(secondOptions.chunks).deep.eql([ 'vendor', 'demo' ]) + expect(secondOptions.filename).deep.eql('demo.html') + expect(secondOptions.template).deep.eql('demo.html') + }) + + it('should have the CommonsChunkPlugin enabled', function () { + const webpackConfig = buildPagesConfig(['index', 'demo'], { chunks: { vendor: true }, ...baseConfig }) + + const vendor = webpackConfig.plugins.filter((plugin) => plugin instanceof optimize.CommonsChunkPlugin) + expect(vendor.length).equal(1) + }) + + it('should set name to "vendor" in the CommonsChunkPlugin', function () { + const webpackConfig = buildPagesConfig(['index', 'demo'], { chunks: { vendor: true }, ...baseConfig }) + + const vendor = webpackConfig.plugins.filter((plugin) => plugin instanceof optimize.CommonsChunkPlugin) + expect(vendor[0].chunkNames[0]).equal('vendor') + }) + + it('should set minChunks to a function in the CommonsChunkPlugin', function () { + const webpackConfig = buildPagesConfig(['index', 'demo'], { chunks: { vendor: true }, ...baseConfig }) + + const vendor = webpackConfig.plugins.filter((plugin) => plugin instanceof optimize.CommonsChunkPlugin) + expect(vendor[0].minChunks).to.be.a('function') + }) + + it('should set pages as chunks in the CommonsChunkPlugin', function () { + const webpackConfig = buildPagesConfig(['index', 'demo'], { chunks: { vendor: true }, ...baseConfig }) + + const vendor = webpackConfig.plugins.filter((plugin) => plugin instanceof optimize.CommonsChunkPlugin) + expect(vendor[0].selectedChunks[0]).equal('index') + expect(vendor[0].selectedChunks[2]).equal('demo') + }) + }) + + describe('when a page is flagged as independent', function () { + it('should be excluded from both the vendor and common chunks', function () { + const webpackConfig = buildPagesConfig([ + 'index', + { name: 'demo', independent: true } + ], { + chunks: { + vendor: true, + common: true + }, + ...baseConfig + }) + + const plugins = webpackConfig.plugins.filter((plugin) => plugin instanceof optimize.CommonsChunkPlugin) + const vendor = plugins[0] + const common = plugins[1] + + expect(vendor.selectedChunks.length).equal(2) + expect(vendor.selectedChunks[0]).equal('index') + expect(vendor.selectedChunks[2]).equal(undefined) + + expect(common.selectedChunks.length).equal(2) + expect(common.selectedChunks[0]).equal('index') + expect(common.selectedChunks[2]).equal(undefined) + }) }) }) diff --git a/src/index.js b/src/index.js index c45c2da..ed99ced 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,5 @@ import path from 'path' +import merge from 'lodash.merge' import cli from './cli' import loadProjectSaguiConfig from './load-project-sagui-config' import configureKarma from './configure-karma' @@ -19,6 +20,10 @@ const DEFAULT_SAGUI_CONFIG = { optimize: false, coverage: false, pages: [], + chunks: { + vendor: false, + common: true + }, disableLoaders: [], javaScript: {}, additionalWebpackConfig: {}, @@ -34,12 +39,7 @@ const DEFAULT_SAGUI_CONFIG = { */ const sagui = (saguiConfig = {}) => new Promise((resolve, reject) => { try { - const finalSaguiConfig = { - ...DEFAULT_SAGUI_CONFIG, - ...saguiConfig, - ...loadProjectSaguiConfig(saguiConfig) - } - + const finalSaguiConfig = merge({}, DEFAULT_SAGUI_CONFIG, saguiConfig, loadProjectSaguiConfig(saguiConfig)) const webpackConfig = configureWebpack(finalSaguiConfig) const karmaConfig = configureKarma(finalSaguiConfig, webpackConfig) diff --git a/src/sagui-config-schema.json b/src/sagui-config-schema.json index 1744f73..dd78aaa 100644 --- a/src/sagui-config-schema.json +++ b/src/sagui-config-schema.json @@ -67,8 +67,38 @@ "description": "https://github.com/saguijs/sagui#libraries" }, "pages": { - "description": "https://github.com/saguijs/sagui#pages", - "type": "array" + "anyOf": [ + { + "type": "array" + }, + { + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "independent": { + "type": "boolean" + } + }, + "type": "object" + } + ], + "description": "https://github.com/saguijs/sagui#pages" + }, + "chunks": { + "additionalProperties": false, + "properties": { + "common": { + "description": "https://github.com/saguijs/sagui#chunkscommon", + "type": "boolean" + }, + "vendor": { + "description": "https://github.com/saguijs/sagui#chunksvendor", + "type": "boolean" + } + }, + "type": "object" }, "style": { "additionalProperties": false,