Skip to content

Commit 85096e9

Browse files
authored
Multi Output (#27)
1 parent 69f1f44 commit 85096e9

File tree

7 files changed

+427
-224
lines changed

7 files changed

+427
-224
lines changed

README.md

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ module.exports = {
3131
module: {
3232
rules: [{
3333
test: /\.html$/,
34-
loader: 'reshape',
34+
loader: 'reshape-loader',
3535
options: { plugins: [/* plugins here */] }
3636
}]
3737
}
@@ -57,7 +57,7 @@ module.exports = {
5757
module: {
5858
rules: [{
5959
test: /\.html$/,
60-
loader: 'reshape',
60+
loader: 'reshape-loader',
6161
options: {
6262
plugins: (loaderContext) => {
6363
return [somePlugin({ file: loaderContext.resourcePath })]
@@ -88,9 +88,9 @@ module.exports = {
8888
rules: [{
8989
test: /\.html$/,
9090
use: [
91-
{ loader: 'html' },
91+
{ loader: 'source-loader' },
9292
{
93-
loader: 'reshape',
93+
loader: 'reshape-loader',
9494
options: {
9595
plugins: [expressions()],
9696
locals: { planet: 'world' }
@@ -109,6 +109,47 @@ console.log(html) // <p>Hello world!</p>
109109

110110
If you do this, you will want at least one other loader in order to integrate the returned source with webpack correctly. For most use cases, the [html-loader](https://github.com/webpack/html-loader) is recommended. If you want to export the html string directly for use in javascript or webpack plugins, we recommend the [source-loader](https://github.com/static-dev/source-loader). Whichever loader you choose, it should be the first loader, followed by reshape, as seen in the example above.
111111

112+
## Producing Multiple Outputs from a Single files
113+
114+
The reshape loader is unique in its ability to take in a single source file, and compile multiple outputs with different options for each output. This ability can be very useful for cases in which a single template is used with a set of different locals to produce variants purely from a data input, such as for internationalization.
115+
116+
In order to use multiple outputs, you can pass the `multi` option. This option should be an array of objects, each one will be merged with the base options (with priority given to the multi object), and used to produce a unique output. It is required that each `multi` object contains a `name` property, which is used to name the output. So for example, if we wanted to produce a static html result with a single template compiled with two different languages, it might look like this:
117+
118+
```html
119+
<!-- index.html -->
120+
<p>{{ greeting }}!</p>
121+
```
122+
123+
```js
124+
// webpack.config.js
125+
module.exports = {
126+
module: {
127+
rules: [{
128+
test: /\.html$/,
129+
use: [
130+
{ loader: 'source-loader' },
131+
{
132+
loader: 'reshape-loader',
133+
options: {
134+
multi: [
135+
{ locals: { greeting: 'hello' }, name: 'en' },
136+
{ locals: { greeting: 'hola' }, name: 'es' }
137+
]
138+
}
139+
}
140+
]
141+
}]
142+
}
143+
}
144+
```
145+
146+
```js
147+
const html = require('./index.html')
148+
console.log(html) // { en: "<p>hello!</p>", es: "<p>hola!</p>" }
149+
```
150+
151+
It should be noted that passing in anything as the `multi` option will return static html, regardless of any other options. If you want to use a template, you don't need the multi option, you can just execute the template with different sets of locals on the client side as needed.
152+
112153
## Custom Plugin Hooks
113154

114155
Reshape loader adds a custom hook that webpack plugins can utilize called `beforeLoaderCompile` (sync). This hook exposes the options as they stand immediately before being passed to reshape for compilation, allowing them to be read and/or modified by plugins. For example, if you wanted to make a plugin that adds a `test` key to the locals, it might look like this.

lib/index.js

Lines changed: 69 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@ module.exports = function (source) {
77
const cb = this.async()
88
let options = parseOptions.call(this, this.query)
99

10-
// if the output function doesn't need to be called, we'll take it as a string
11-
// and save the time eval'ing it
12-
if (!options.locals) {
10+
// if we are producing a template (no locals or multi options), we are for
11+
// sure going to be returning a string, so we don't need to eval into a
12+
// function. we can set the returnString generator option to slice off some
13+
// overhead here
14+
if (!options.locals && !options.multi) {
1315
if (!options.generatorOptions) { options.generatorOptions = {} }
1416
options.generatorOptions.returnString = true
1517
}
1618

17-
// set loader override options
19+
// set loader override options - filename and deps must be set by the loader
1820
options = Object.assign(options, {
1921
filename: this.resourcePath,
2022
dependencies: []
@@ -23,25 +25,64 @@ module.exports = function (source) {
2325
// custom plugin hook for options modification
2426
this._compiler.applyPlugins('before-loader-process', this, options)
2527

26-
// run reshape
27-
reshape(options)
28-
.process(source)
29-
.then((res) => {
30-
// If there are any dependencies reported, add them to webpack
31-
if (res.dependencies) {
32-
res.dependencies.map((dep) => this.addDependency(dep.file))
28+
// if the multi option was specified, we have a slightly different compile
29+
// process. basically, we run through each of the multi options, merge it
30+
// with priority into the base options, and compile the base template once
31+
// for each multi option. The output is an object with the key being the
32+
// specified "name", and the value being a string with the compiled contents
33+
if (options.multi) {
34+
let depsAdded = false
35+
promiseReduce.call(this, options.multi, (m, opt) => {
36+
// no name is a no-go
37+
if (!opt.name) {
38+
throw new Error('multi options must have a "name" property')
3339
}
34-
// If the locals option is specified, we call the output function and
35-
// return static html. Otherwise, we return the function & runtime to be
36-
// used client-side.
37-
if (options.locals) {
38-
cb(null, res.output(options.locals))
39-
} else {
40-
// TODO: more efficient shared runtime export
41-
cb(null, `var __runtime = ${serializeVerbatim(res.runtime)}; module.exports = ${res.output.substr(1)}`)
42-
}
43-
})
44-
.catch(cb)
40+
41+
// merge multi options back with default options
42+
const mergedOpts = Object.assign({}, options, opt)
43+
44+
// run it
45+
return reshape(mergedOpts)
46+
.process(source)
47+
.then((res) => {
48+
// Only add dependencies once to prevent dupes
49+
if (!depsAdded && res.dependencies) {
50+
res.dependencies.map((dep) => this.addDependency(dep.file))
51+
}
52+
53+
// Multi always produces a compiled template
54+
m[mergedOpts.name] = res.output(mergedOpts.locals)
55+
return m
56+
})
57+
// this needs to add module.exports and skip the source loader
58+
}, {}).then((res) => {
59+
// hack semi-specific to source-loader to ensure this is loaded
60+
// as a js object and not stringified
61+
this._module._jsSource = true
62+
this._module._outputMultiple = true
63+
cb(null, JSON.stringify(res))
64+
}).catch(cb)
65+
} else {
66+
// run reshape
67+
reshape(options)
68+
.process(source)
69+
.then((res) => {
70+
// If there are any dependencies reported, add them to webpack
71+
if (res.dependencies) {
72+
res.dependencies.map((dep) => this.addDependency(dep.file))
73+
}
74+
// If the locals option is specified, we call the output function and
75+
// return static html. Otherwise, we return the function & runtime to be
76+
// used client-side.
77+
if (options.locals) {
78+
cb(null, res.output(options.locals))
79+
} else {
80+
// TODO: more efficient shared runtime export
81+
cb(null, `var __runtime = ${serializeVerbatim(res.runtime)}; module.exports = ${res.output.substr(1)}`)
82+
}
83+
})
84+
.catch(cb)
85+
}
4586
}
4687

4788
// Allows any option to be passed as a function which gets webpack's context
@@ -55,7 +96,7 @@ function parseOptions (opts) {
5596
generatorOptions: convertFn.call(this, opts.generatorOptions),
5697
runtime: convertFn.call(this, opts.runtime),
5798
parser: convertFnSpecial.call(this, opts.parser),
58-
generator: convertFnSpecial.call(this, opts.generator)
99+
multi: convertFn.call(this, opts.multi)
59100
})
60101
}
61102

@@ -92,3 +133,8 @@ function serializeVerbatim (obj) {
92133
})
93134
return res
94135
}
136+
137+
function promiseReduce (arr, fn, memo) {
138+
return Promise.all(arr.map(fn.bind(this, memo)))
139+
.then(_ => memo)
140+
}

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,16 @@
1313
"reshape-include": "^1.0.0"
1414
},
1515
"devDependencies": {
16-
"ava": "^0.19.1",
16+
"ava": "^0.21.0",
1717
"coveralls": "^2.13.1",
18-
"nyc": "^11.0.2",
18+
"nyc": "^11.0.3",
1919
"reshape-custom-elements": "^0.1.0",
2020
"reshape-expressions": "^0.1.5",
2121
"snazzy": "^7.0.0",
22-
"source-loader": "^0.2.0",
22+
"source-loader": "^1.0.0",
2323
"standard": "^10.0.2",
2424
"sugarml": "^0.6.0",
25-
"webpack": "^3.0.0",
25+
"webpack": "^3.3.0",
2626
"when": "^3.7.8"
2727
},
2828
"engine": ">=6",

test/fixtures/multi/app.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
require('./index.html')

test/fixtures/multi/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<p>{{ greeting }}</p>

test/index.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,34 @@ test('custom plugin hook works', (t) => {
115115
})
116116
})
117117

118+
test('multi output', (t) => {
119+
return webpackCompile('multi', {
120+
plugins: [exp()],
121+
multi: [
122+
{ locals: { greeting: 'hello' }, name: 'en' },
123+
{ locals: { greeting: 'hola' }, name: 'es' }
124+
]
125+
}).then(({outputPath, src}) => {
126+
t.regex(src, /module\.exports = {"en":"<p>hello<\/p>\\n","es":"<p>hola<\/p>\\n"}/g)
127+
fs.unlinkSync(outputPath)
128+
})
129+
})
130+
131+
test('multi output without name property errors', (t) => {
132+
return webpackCompile('multi', {
133+
plugins: [exp()],
134+
multi: [
135+
{ locals: { greeting: 'hello' } },
136+
{ locals: { greeting: 'hola' } }
137+
]
138+
}).catch(({err, outputPath}) => {
139+
t.regex(err.toString(), /multi options must have a "name" property/)
140+
fs.unlinkSync(outputPath)
141+
})
142+
})
143+
118144
// Utility: compile a fixture with webpack, return results
119-
function webpackCompile (name, config, qs = {}) {
145+
function webpackCompile (name, config = {}, qs = {}) {
120146
const testPath = path.join(fixtures, name)
121147
const outputPath = path.join(testPath, 'bundle.js')
122148

0 commit comments

Comments
 (0)