diff --git a/docs/api.md b/docs/api.md index b53ba8643..97de31c15 100644 --- a/docs/api.md +++ b/docs/api.md @@ -61,9 +61,9 @@ console.log(api.options.source); // /project/lib ```js api.options.mains.index = 'app.js'; -console.log(api.options.mains.index); // /project/src/app.js +console.log(api.options.mains.index); // { entry: '/project/src/app.js' } api.options.source = 'lib'; -console.log(api.options.mains.index); // /project/lib/app.js +console.log(api.options.mains.index); // { entry: /project/lib/app.js } ``` ### `options.root` @@ -138,27 +138,34 @@ Neutrino({ ### `options.mains` Set the main entry points for the application. If the option is not set, Neutrino defaults it to: - + ```js { index: 'index' } ``` - + Notice the entry point has no extension; the extension is resolved by webpack. If relative paths are specified, they will be computed and resolved relative to `options.source`; absolute paths will be used as-is. +Multiple entry points and any page-specific configuration (if supported by the preset) can be specified like so: + ```js Neutrino({ mains: { - // If not specified, defaults to options.source + index.* - index: 'index', - - // Override to relative, resolves to options.source + entry.* - index: 'entry', - - // Override to absolute path - index: '/code/website/src/entry.js' + // Relative path, so resolves to options.source + home.* + index: 'home', + + // Absolute path, used as-is. + login: '/code/website/src/login.js', + + // Long form that allows passing page-specific configuration + // (such as html-webpack-plugin options in the case of @neutrinojs/web). + admin: { + entry: 'admin', + // any page-specific options here (see preset docs) + // ... + } } }) ``` diff --git a/docs/creating-presets.md b/docs/creating-presets.md index 93ae0d429..c05538cf2 100644 --- a/docs/creating-presets.md +++ b/docs/creating-presets.md @@ -272,10 +272,10 @@ Set the main entry points for the application. If the option is not set, Neutrin index: 'index' } ``` - + Notice the entry point has no extension; the extension is resolved by webpack. If relative paths are specified, they will be computed and resolved relative to `options.source`; absolute paths will be used as-is. - + By default these main files are not required to be in JavaScript format. They may also potentially be JSX, TypeScript, or any other preprocessor language. These extensions should be specified in middleware at `neutrino.config.resolve.extensions`. @@ -286,21 +286,27 @@ module.exports = neutrino => { // resolved to options.source + index neutrino.options.mains.index; }; +``` +Multiple entry points and any page-specific configuration (if supported by the preset) can be specified like so: + +```js module.exports = { options: { mains: { - // If not specified, defaults to options.source + index - index: 'index', - - // Override to relative, resolves to options.source + entry.* - index: 'entry', - - // Override to absolute path - index: '/code/website/src/entry.js', - - // Add additional main, resolves to options.source + admin.* - admin: 'admin' + // Relative path, so resolves to options.source + home.* + index: 'home', + + // Absolute path, used as-is. + login: '/code/website/src/login.js', + + // Long form that allows passing page-specific configuration + // (such as html-webpack-plugin options in the case of @neutrinojs/web). + admin: { + entry: 'admin', + // any page-specific options here (see preset docs) + // ... + } } } }; diff --git a/docs/customization.md b/docs/customization.md index 6451cc126..f69ff1a3a 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -122,21 +122,25 @@ Set the main entry points for the application. If the option is not set, Neutrin Notice the entry point has no extension; the extension is resolved by webpack. If relative paths are specified, they will be computed and resolved relative to `options.source`; absolute paths will be used as-is. +Multiple entry points and any page-specific configuration (if supported by the preset) can be specified like so: + ```js module.exports = { options: { mains: { - // If not specified, defaults to options.source + index - index: 'index', - - // Override to relative, resolves to options.source + entry.* - index: 'entry', - - // Override to absolute path - index: '/code/website/src/entry.js', - - // Add additional main, resolves to options.source + admin.* - admin: 'admin' + // Relative path, so resolves to options.source + home.* + index: 'home', + + // Absolute path, used as-is. + login: '/code/website/src/login.js', + + // Long form that allows passing page-specific configuration + // (such as html-webpack-plugin options in the case of @neutrinojs/web). + admin: { + entry: 'admin', + // any page-specific options here (see preset docs) + // ... + } } } }; diff --git a/packages/library/index.js b/packages/library/index.js index 0bac96b2e..1a5d9104b 100644 --- a/packages/library/index.js +++ b/packages/library/index.js @@ -49,9 +49,9 @@ module.exports = (neutrino, opts = {}) => { babel: options.babel }); - Object - .keys(neutrino.options.mains) - .forEach(key => neutrino.config.entry(key).add(neutrino.options.mains[key])); + Object.entries(neutrino.options.mains).forEach(([name, config]) => + neutrino.config.entry(name).add(config.entry) + ); neutrino.config .when(hasSourceMap, () => neutrino.use(banner)) diff --git a/packages/neutrino/Neutrino.js b/packages/neutrino/Neutrino.js index bf2c66a2a..c975a9244 100644 --- a/packages/neutrino/Neutrino.js +++ b/packages/neutrino/Neutrino.js @@ -32,6 +32,10 @@ const requireFromRoot = (moduleId, root) => { // eslint-disable-next-line global-require, import/no-dynamic-require return require(path); }; +// Support both a shorter string form and an object form that allows +// specifying any page-specific options supported by the preset. +const normalizeMainConfig = (config) => + (typeof config === 'string') ? { entry: config } : config; module.exports = class Neutrino { constructor(options) { @@ -138,9 +142,9 @@ module.exports = class Neutrino { bindMainsOnOptions(options, optionsSource) { Object - .keys(options.mains) - .forEach(key => { - let value = options.mains[key]; + .entries(options.mains) + .forEach(([key, value]) => { + let normalizedConfig = normalizeMainConfig(value); Reflect.defineProperty(options.mains, key, { enumerable: true, @@ -148,17 +152,21 @@ module.exports = class Neutrino { const source = optionsSource && optionsSource.source || options.source; - return normalizePath(source, value); + return { + ...normalizedConfig, + // Lazily normalise the path, in case `source` changes after mains is updated. + entry: normalizePath(source, normalizedConfig.entry) + }; }, set(newValue) { - value = newValue; + normalizedConfig = normalizeMainConfig(newValue); } }); }); this.mainsProxy = new Proxy(options.mains, { defineProperty: (target, prop, { value }) => { - let currentValue = value; + let normalizedConfig = normalizeMainConfig(value); return Reflect.defineProperty(target, prop, { enumerable: true, @@ -166,10 +174,14 @@ module.exports = class Neutrino { const source = optionsSource && optionsSource.source || options.source; - return normalizePath(source, currentValue); + return { + ...normalizedConfig, + // Lazily normalise the path, in case `source` changes after mains is updated. + entry: normalizePath(source, normalizedConfig.entry) + }; }, set(newValue) { - currentValue = newValue; + normalizedConfig = normalizeMainConfig(newValue); } }); } diff --git a/packages/neutrino/test/api_test.js b/packages/neutrino/test/api_test.js index f9409e9c7..1bf225aea 100644 --- a/packages/neutrino/test/api_test.js +++ b/packages/neutrino/test/api_test.js @@ -87,44 +87,47 @@ test('throws when legacy options.node_modules is set', t => { test('options.mains', t => { const api = new Neutrino(); - t.is(api.options.mains.index, join(process.cwd(), 'src/index')); + t.deepEqual(api.options.mains.index, { entry: join(process.cwd(), 'src/index') }); api.options.mains.index = './alpha.js'; - t.is(api.options.mains.index, join(process.cwd(), 'src/alpha.js')); + t.deepEqual(api.options.mains.index, { entry: join(process.cwd(), 'src/alpha.js') }); api.options.source = 'beta'; - t.is(api.options.mains.index, join(process.cwd(), 'beta/alpha.js')); + t.deepEqual(api.options.mains.index, { entry: join(process.cwd(), 'beta/alpha.js') }); api.options.root = '/gamma'; - t.is(api.options.mains.index, join('/gamma', 'beta/alpha.js')); + t.deepEqual(api.options.mains.index, { entry: join('/gamma', 'beta/alpha.js') }); api.options.mains.index = '/alpha.js'; - t.is(api.options.mains.index, '/alpha.js'); + t.deepEqual(api.options.mains.index, { entry: '/alpha.js' }); }); test('override options.mains', t => { const api = new Neutrino({ mains: { alpha: 'beta', - gamma: 'delta' + gamma: { + entry: 'delta', + title: 'Gamma Page' + } } }); - t.is(api.options.mains.alpha, join(process.cwd(), 'src/beta')); - api.options.mains.alpha = './alpha.js'; - t.is(api.options.mains.alpha, join(process.cwd(), 'src/alpha.js')); + t.deepEqual(api.options.mains.alpha, { entry: join(process.cwd(), 'src/beta') }); + api.options.mains.alpha = { entry: './alpha.js', minify: false }; + t.deepEqual(api.options.mains.alpha, { entry: join(process.cwd(), 'src/alpha.js'), minify: false }); api.options.source = 'epsilon'; - t.is(api.options.mains.alpha, join(process.cwd(), 'epsilon/alpha.js')); + t.deepEqual(api.options.mains.alpha, { entry: join(process.cwd(), 'epsilon/alpha.js'), minify: false }); api.options.root = '/zeta'; - t.is(api.options.mains.alpha, join('/zeta', 'epsilon/alpha.js')); + t.deepEqual(api.options.mains.alpha, { entry: join('/zeta', 'epsilon/alpha.js'), minify: false }); api.options.mains.alpha = '/alpha.js'; - t.is(api.options.mains.alpha, '/alpha.js'); + t.deepEqual(api.options.mains.alpha, { entry: '/alpha.js' }); - t.is(api.options.mains.gamma, join('/zeta', 'epsilon/delta')); + t.deepEqual(api.options.mains.gamma, { entry: join('/zeta', 'epsilon/delta'), title: 'Gamma Page' }); api.options.mains.gamma = './alpha.js'; - t.is(api.options.mains.gamma, join('/zeta', 'epsilon/alpha.js')); + t.deepEqual(api.options.mains.gamma, { entry: join('/zeta', 'epsilon/alpha.js') }); api.options.source = 'src'; - t.is(api.options.mains.gamma, join('/zeta', 'src/alpha.js')); + t.deepEqual(api.options.mains.gamma, { entry: join('/zeta', 'src/alpha.js') }); api.options.root = process.cwd(); - t.is(api.options.mains.gamma, join(process.cwd(), 'src/alpha.js')); + t.deepEqual(api.options.mains.gamma, { entry: join(process.cwd(), 'src/alpha.js') }); api.options.mains.gamma = '/alpha.js'; - t.is(api.options.mains.gamma, '/alpha.js'); + t.deepEqual(api.options.mains.gamma, { entry: '/alpha.js' }); }); test('creates an instance of webpack-chain', t => { diff --git a/packages/node/index.js b/packages/node/index.js index 9fbd4cf31..2141ce7a3 100644 --- a/packages/node/index.js +++ b/packages/node/index.js @@ -52,9 +52,9 @@ module.exports = (neutrino, opts = {}) => { }, options.babel) }); - Object - .keys(neutrino.options.mains) - .forEach(key => neutrino.config.entry(key).add(neutrino.options.mains[key])); + Object.entries(neutrino.options.mains).forEach(([name, config]) => + neutrino.config.entry(name).add(config.entry) + ); neutrino.config .when(sourceMap, () => neutrino.use(banner)) @@ -92,7 +92,7 @@ module.exports = (neutrino, opts = {}) => { const mainKeys = Object.keys(neutrino.options.mains); neutrino.use(startServer, { - name: getOutputForEntry(neutrino.options.mains[mainKeys[0]]) + name: getOutputForEntry(neutrino.options.mains[mainKeys[0]].entry) }); config .devtool('inline-sourcemap') diff --git a/packages/react-components/index.js b/packages/react-components/index.js index d467c0339..52446bf42 100644 --- a/packages/react-components/index.js +++ b/packages/react-components/index.js @@ -32,7 +32,9 @@ module.exports = (neutrino, opts = {}) => { readdirSync(components).forEach(component => { // eslint-disable-next-line no-param-reassign - neutrino.options.mains[basename(component, extname(component))] = join(components, component); + neutrino.options.mains[basename(component, extname(component))] = { + entry: join(components, component) + }; }); const pkg = neutrino.options.packageJson || {}; diff --git a/packages/react/README.md b/packages/react/README.md index 8705d072b..77ca657d2 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -245,17 +245,32 @@ array. You can also make these changes from the Neutrino API in custom middlewar By default Neutrino, and therefore this preset, creates a single **main** `index` entry point to your application, and this maps to the `index.*` file in the `src` directory. The extension is resolved by webpack. This value is provided by -`neutrino.options.mains` at `neutrino.options.mains.index`. This means that the Web preset is optimized toward the use -case of single-page applications over multi-page applications. If you wish to output multiple pages, you can detail -all your mains in your `.neutrinorc.js`. +`neutrino.options.mains` at `neutrino.options.mains.index`. + +If you wish to output multiple pages, you can configure them like so: ```js module.exports = { options: { mains: { - index: 'index', // outputs index.html from src/index.* - admin: 'admin', // outputs admin.html from src/admin.* - account: 'user' // outputs account.html from src/user.* + index: { + // outputs index.html from src/index.* + entry: 'index', + // Additional options are passed to html-webpack-plugin, and override + // any defaults set via the preset's `html` option. + title: 'Site Homepage', + }, + admin: { + // outputs admin.html from src/admin.* + entry: 'admin', + title: 'Admin Dashboard', + }, + account: { + // outputs account.html from src/user.* using a custom HTML template. + entry: 'user', + inject: true, + template: 'my-custom-template.html', + }, } }, use: ['@neutrinojs/react'] diff --git a/packages/vue/README.md b/packages/vue/README.md index 70a2795ee..9b8e3e1ec 100644 --- a/packages/vue/README.md +++ b/packages/vue/README.md @@ -252,17 +252,32 @@ for details on customization. By default Neutrino, and therefore this preset, creates a single **main** `index` entry point to your application, and this maps to the `index.*` file in the `src` directory. The extension is resolved by webpack. This value is provided by -`neutrino.options.mains` at `neutrino.options.mains.index`. This means that the Web preset is optimized toward the use -case of single-page applications over multi-page applications. If you wish to output multiple pages, you can detail -all your mains in your `.neutrinorc.js`. - +`neutrino.options.mains` at `neutrino.options.mains.index`. + +If you wish to output multiple pages, you can configure them like so: + ```js module.exports = { options: { mains: { - index: 'index', // outputs index.html from src/index.* - admin: 'admin', // outputs admin.html from src/admin.* - account: 'user' // outputs account.html from src/user.* + index: { + // outputs index.html from src/index.* + entry: 'index', + // Additional options are passed to html-webpack-plugin, and override + // any defaults set via the preset's `html` option. + title: 'Site Homepage', + }, + admin: { + // outputs admin.html from src/admin.* + entry: 'admin', + title: 'Admin Dashboard', + }, + account: { + // outputs account.html from src/user.* using a custom HTML template. + entry: 'user', + inject: true, + template: 'my-custom-template.html', + }, } }, use: ['@neutrinojs/vue'] diff --git a/packages/web/README.md b/packages/web/README.md index 328b93c27..65e6999c5 100644 --- a/packages/web/README.md +++ b/packages/web/README.md @@ -412,17 +412,32 @@ changes. By default Neutrino, and therefore this preset, creates a single **main** `index` entry point to your application, and this maps to the `index.*` file in the `src` directory. The extension is resolved by webpack. This value is provided by -`neutrino.options.mains` at `neutrino.options.mains.index`. This means that the Web preset is optimized toward the use -case of single-page applications over multi-page applications. If you wish to output multiple pages, you can detail -all your mains in your `.neutrinorc.js`. +`neutrino.options.mains` at `neutrino.options.mains.index`. + +If you wish to output multiple pages, you can configure them like so: ```js module.exports = { options: { mains: { - index: 'index', // outputs index.html from src/index.* - admin: 'admin', // outputs admin.html from src/admin.* - account: 'user' // outputs account.html from src/user.* + index: { + // outputs index.html from src/index.* + entry: 'index', + // Additional options are passed to html-webpack-plugin, and override + // any defaults set via the preset's `html` option. + title: 'Site Homepage', + }, + admin: { + // outputs admin.html from src/admin.* + entry: 'admin', + title: 'Admin Dashboard', + }, + account: { + // outputs account.html from src/user.* using a custom HTML template. + entry: 'user', + inject: true, + template: 'my-custom-template.html', + }, } }, use: ['@neutrinojs/web'] diff --git a/packages/web/index.js b/packages/web/index.js index 90548118c..6888cd03d 100644 --- a/packages/web/index.js +++ b/packages/web/index.js @@ -117,20 +117,24 @@ module.exports = (neutrino, opts = {}) => { .use(HtmlWebpackIncludeSiblingChunksPlugin); Object - .keys(neutrino.options.mains) - .forEach(key => { - neutrino.config - .entry(key) - .add(neutrino.options.mains[key]) - .when(options.html, () => { - neutrino.use(htmlTemplate, merge({ - pluginId: `html-${key}`, - filename: `${key}.html`, - // html-webpack-include-sibling-chunks-plugin dynamically updates this to - // include the runtime chunk and any chunks generated by splitChunks. - chunks: [key] - }, options.html)); - }); + .entries(neutrino.options.mains) + .forEach(([name, config]) => { + const { entry, ...htmlTemplateConfig } = config; + neutrino.config.entry(name).add(entry); + + if (options.html) { + neutrino.use(htmlTemplate, merge.all([ + { + pluginId: `html-${name}`, + filename: `${name}.html`, + // html-webpack-include-sibling-chunks-plugin dynamically updates this to + // include the runtime chunk and any chunks generated by splitChunks. + chunks: [name] + }, + options.html, + htmlTemplateConfig + ])); + } }); const jsFilename = process.env.NODE_ENV === 'production' diff --git a/packages/web/test/web_test.js b/packages/web/test/web_test.js index 451b24b11..df9856f81 100644 --- a/packages/web/test/web_test.js +++ b/packages/web/test/web_test.js @@ -105,6 +105,47 @@ test('supports env option using object form', t => { t.deepEqual(api.config.plugin('env').get('args'), [ env ]); }); +test('supports multiple mains with custom html-webpack-plugin options', t => { + const mains = { + index: './index', + admin: { + entry: './admin', + title: 'Admin Dashboard' + } + }; + const api = new Neutrino({ mains }); + + api.use(mw(), { html: { title: 'Default Title', minify: false } }); + + t.deepEqual(api.config.plugin('html-index').get('args'), [{ + appMountId: 'root', + chunks: [ + 'index' + ], + filename: 'index.html', + inject: false, + minify: false, + mobile: true, + template: require('html-webpack-template'), + title: 'Default Title', + xhtml: true + }]); + + t.deepEqual(api.config.plugin('html-admin').get('args'), [{ + appMountId: 'root', + chunks: [ + 'admin' + ], + filename: 'admin.html', + inject: false, + minify: false, + mobile: true, + template: require('html-webpack-template'), + title: 'Admin Dashboard', + xhtml: true + }]); +}); + test('throws when minify.babel defined', async t => { const api = new Neutrino();