Skip to content
This repository has been archived by the owner on Feb 18, 2024. It is now read-only.

Commit

Permalink
Support per-page HTML template customisation via 'mains' (#1029)
Browse files Browse the repository at this point in the history
This adds support for defining each entry point in `mains` as an
object, where the path to the entry point is now defined under an
`entry` property, and any other properties can be used by presets
for page-specific options. (The short form using a string is still
supported.)

In the case of `@neutrinojs/web` (and presets that inherit from it),
these additional properties are then used to override the options
passed to `html-webpack-plugin`, allowing for page-specific
customisation of the generated HTML template.

For example:

```
module.exports = {
  options: {
    mains: {
      index: {
        entry: './index',
        // Options here take priority over the preset's `html` options below.
        title: 'Site Homepage',
      },
      admin: {
        entry: './admin',
        title: 'Admin Dashboard',
      },
      account: {
        entry: './user',
        inject: true,
        template: './my-custom-template.html',
      },
    }
  },
  use: ['@neutrinojs/web', {
    // Customise the defaults used for all pages.
    html: {
      minify: false,
    }
  }]
}
```

For a list of the available `html-webpack-plugin` options, see:
https://github.com/jantimon/html-webpack-plugin#options

Fixes #865.
  • Loading branch information
edmorley committed Aug 21, 2018
1 parent 4882da4 commit c03a8ca
Show file tree
Hide file tree
Showing 13 changed files with 226 additions and 102 deletions.
31 changes: 19 additions & 12 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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)
// ...
}
}
})
```
Expand Down
32 changes: 19 additions & 13 deletions docs/creating-presets.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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)
// ...
}
}
}
};
Expand Down
26 changes: 15 additions & 11 deletions docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
// ...
}
}
}
};
Expand Down
6 changes: 3 additions & 3 deletions packages/library/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
28 changes: 20 additions & 8 deletions packages/neutrino/Neutrino.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -138,38 +142,46 @@ 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,
get() {
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,
get() {
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);
}
});
}
Expand Down
37 changes: 20 additions & 17 deletions packages/neutrino/test/api_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
8 changes: 4 additions & 4 deletions packages/node/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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')
Expand Down
4 changes: 3 additions & 1 deletion packages/react-components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 || {};
Expand Down
27 changes: 21 additions & 6 deletions packages/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
Loading

0 comments on commit c03a8ca

Please sign in to comment.