Skip to content

Commit 8a95f70

Browse files
authored
Implement pipelines to compose multiple asset types together (#1065)
1 parent 3ddaec9 commit 8a95f70

13 files changed

Lines changed: 239 additions & 65 deletions

src/Asset.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class Asset {
2626
this.type = path.extname(this.name).slice(1);
2727

2828
this.processed = false;
29-
this.contents = null;
29+
this.contents = options.rendition ? options.rendition.value : null;
3030
this.ast = null;
3131
this.generated = null;
3232
this.hash = null;
@@ -58,6 +58,13 @@ class Asset {
5858
}
5959

6060
async getDependencies() {
61+
if (
62+
this.options.rendition &&
63+
this.options.rendition.hasDependencies === false
64+
) {
65+
return;
66+
}
67+
6168
await this.loadIfNeeded();
6269

6370
if (this.contents && this.mightHaveDependencies()) {
@@ -154,6 +161,10 @@ class Asset {
154161
return this.generated;
155162
}
156163

164+
async postProcess(generated) {
165+
return generated;
166+
}
167+
157168
generateHash() {
158169
return objectHash(this.generated);
159170
}

src/Pipeline.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
const Parser = require('./Parser');
2+
const path = require('path');
3+
const md5 = require('./utils/md5');
4+
5+
/**
6+
* A Pipeline composes multiple Asset types together.
7+
*/
8+
class Pipeline {
9+
constructor(options) {
10+
this.options = options;
11+
this.parser = new Parser(options);
12+
}
13+
14+
async process(path, pkg, options) {
15+
let asset = this.parser.getAsset(path, pkg, options);
16+
let generated = await this.processAsset(asset);
17+
let generatedMap = {};
18+
for (let rendition of generated) {
19+
generatedMap[rendition.type] = rendition.value;
20+
}
21+
22+
return {
23+
dependencies: Array.from(asset.dependencies.values()),
24+
generated: generatedMap,
25+
hash: asset.hash,
26+
cacheData: asset.cacheData
27+
};
28+
}
29+
30+
async processAsset(asset) {
31+
try {
32+
await asset.process();
33+
} catch (err) {
34+
throw asset.generateErrorMessage(err);
35+
}
36+
37+
let inputType = path.extname(asset.name).slice(1);
38+
let generated = [];
39+
40+
for (let rendition of this.iterateRenditions(asset)) {
41+
let {type, value} = rendition;
42+
if (typeof value !== 'string' || rendition.final) {
43+
generated.push(rendition);
44+
continue;
45+
}
46+
47+
// Find an asset type for the rendition type.
48+
// If the asset is not already an instance of this asset type, process it.
49+
let AssetType = this.parser.findParser(
50+
asset.name.slice(0, -inputType.length) + type
51+
);
52+
if (!(asset instanceof AssetType)) {
53+
let opts = Object.assign({rendition}, asset.options);
54+
let subAsset = new AssetType(asset.name, asset.package, opts);
55+
subAsset.contents = value;
56+
subAsset.dependencies = asset.dependencies;
57+
58+
let processed = await this.processAsset(subAsset);
59+
generated = generated.concat(processed);
60+
Object.assign(asset.cacheData, subAsset.cacheData);
61+
asset.hash = md5(asset.hash + subAsset.hash);
62+
} else {
63+
generated.push(rendition);
64+
}
65+
}
66+
67+
// Post process. This allows assets a chance to modify the output produced by sub-asset types.
68+
asset.generated = generated;
69+
try {
70+
generated = await asset.postProcess(generated);
71+
} catch (err) {
72+
throw asset.generateErrorMessage(err);
73+
}
74+
75+
return generated;
76+
}
77+
78+
*iterateRenditions(asset) {
79+
if (Array.isArray(asset.generated)) {
80+
return yield* asset.generated;
81+
}
82+
83+
if (typeof asset.generated === 'string') {
84+
return yield {
85+
type: asset.type,
86+
value: asset.generated
87+
};
88+
}
89+
90+
// Backward compatibility support for the old API.
91+
// Assume all renditions are final - don't compose asset types together.
92+
for (let type in asset.generated) {
93+
yield {
94+
type,
95+
value: asset.generated[type],
96+
final: true
97+
};
98+
}
99+
}
100+
}
101+
102+
module.exports = Pipeline;

src/assets/CSSAsset.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,17 @@ class CSSAsset extends Asset {
116116
'module.exports = ' + JSON.stringify(this.cssModules, false, 2) + ';';
117117
}
118118

119-
return {css, js};
119+
return [
120+
{
121+
type: 'css',
122+
value: css
123+
},
124+
{
125+
type: 'js',
126+
value: js,
127+
final: true
128+
}
129+
];
120130
}
121131

122132
generateErrorMessage(err) {

src/assets/CoffeeScriptAsset.js

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,35 @@
1-
const JSAsset = require('./JSAsset');
1+
const Asset = require('../Asset');
22
const localRequire = require('../utils/localRequire');
33

4-
class CoffeeScriptAsset extends JSAsset {
5-
async parse(code) {
4+
class CoffeeScriptAsset extends Asset {
5+
constructor(name, pkg, options) {
6+
super(name, pkg, options);
7+
this.type = 'js';
8+
}
9+
10+
async generate() {
611
// require coffeescript, installed locally in the app
712
let coffee = await localRequire('coffeescript', this.name);
813

914
// Transpile Module using CoffeeScript and parse result as ast format through babylon
10-
let transpiled = coffee.compile(code, {
15+
let transpiled = coffee.compile(this.contents, {
1116
sourceMap: this.options.sourceMaps
1217
});
1318

19+
let sourceMap;
1420
if (transpiled.sourceMap) {
15-
this.sourceMap = transpiled.sourceMap.generate();
16-
this.sourceMap.sources = [this.relativeName];
17-
this.sourceMap.sourcesContent = [this.contents];
21+
sourceMap = transpiled.sourceMap.generate();
22+
sourceMap.sources = [this.relativeName];
23+
sourceMap.sourcesContent = [this.contents];
1824
}
1925

20-
this.contents = this.options.sourceMaps ? transpiled.js : transpiled;
21-
return await super.parse(this.contents);
26+
return [
27+
{
28+
type: 'js',
29+
value: this.options.sourceMaps ? transpiled.js : transpiled,
30+
sourceMap
31+
}
32+
];
2233
}
2334
}
2435

src/assets/HTMLAsset.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,7 @@ class HTMLAsset extends Asset {
160160
}
161161

162162
generate() {
163-
let html = this.isAstDirty ? render(this.ast) : this.contents;
164-
return {html};
163+
return this.isAstDirty ? render(this.ast) : this.contents;
165164
}
166165
}
167166

src/assets/JSAsset.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class JSAsset extends Asset {
2727
this.isES6Module = false;
2828
this.outputCode = null;
2929
this.cacheData.env = {};
30+
this.sourceMap = options.rendition ? options.rendition.sourceMap : null;
3031
}
3132

3233
shouldInvalidate(cacheData) {

src/assets/LESSAsset.js

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
const CSSAsset = require('./CSSAsset');
1+
const Asset = require('../Asset');
22
const localRequire = require('../utils/localRequire');
33
const promisify = require('../utils/promisify');
44

5-
class LESSAsset extends CSSAsset {
5+
class LESSAsset extends Asset {
6+
constructor(name, pkg, options) {
7+
super(name, pkg, options);
8+
this.type = 'css';
9+
}
10+
611
async parse(code) {
712
// less should be installed locally in the module that's being required
813
let less = await localRequire('less', this.name);
@@ -15,16 +20,24 @@ class LESSAsset extends CSSAsset {
1520
opts.filename = this.name;
1621
opts.plugins = (opts.plugins || []).concat(urlPlugin(this));
1722

18-
let res = await render(code, opts);
19-
res.render = () => res.css;
20-
return res;
23+
return await render(code, opts);
2124
}
2225

2326
collectDependencies() {
2427
for (let dep of this.ast.imports) {
2528
this.addDependency(dep, {includedInParent: true});
2629
}
2730
}
31+
32+
generate() {
33+
return [
34+
{
35+
type: 'css',
36+
value: this.ast.css,
37+
hasDependencies: false
38+
}
39+
];
40+
}
2841
}
2942

3043
function urlPlugin(asset) {

src/assets/ReasonAsset.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
const JSAsset = require('./JSAsset');
1+
const Asset = require('../Asset');
22
const fs = require('../utils/fs');
33
const localRequire = require('../utils/localRequire');
44

5-
class ReasonAsset extends JSAsset {
6-
async parse() {
5+
class ReasonAsset extends Asset {
6+
constructor(name, pkg, options) {
7+
super(name, pkg, options);
8+
this.type = 'js';
9+
}
10+
11+
async generate() {
712
const bsb = await localRequire('bsb-js', this.name);
813

914
// This runs BuckleScript - the Reason to JS compiler.
@@ -18,10 +23,7 @@ class ReasonAsset extends JSAsset {
1823
// BuckleScript configuration to simplify the file processing.
1924
const outputFile = this.name.replace(/\.(re|ml)$/, '.bs.js');
2025
const outputContent = await fs.readFile(outputFile);
21-
this.contents = outputContent.toString();
22-
23-
// After loading the compiled JS source, use the normal JS behavior.
24-
return await super.parse(this.contents);
26+
return outputContent.toString();
2527
}
2628
}
2729

src/assets/SASSAsset.js

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
const CSSAsset = require('./CSSAsset');
1+
const Asset = require('../Asset');
22
const localRequire = require('../utils/localRequire');
33
const promisify = require('../utils/promisify');
44
const path = require('path');
55
const os = require('os');
66

7-
class SASSAsset extends CSSAsset {
7+
class SASSAsset extends Asset {
8+
constructor(name, pkg, options) {
9+
super(name, pkg, options);
10+
this.type = 'css';
11+
}
12+
813
async parse(code) {
914
// node-sass should be installed locally in the module that's being required
1015
let sass = await localRequire('node-sass', this.name);
@@ -17,7 +22,7 @@ class SASSAsset extends CSSAsset {
1722
opts.includePaths = (opts.includePaths || []).concat(
1823
path.dirname(this.name)
1924
);
20-
opts.data = opts.data ? (opts.data + os.EOL + code) : code;
25+
opts.data = opts.data ? opts.data + os.EOL + code : code;
2126
opts.indentedSyntax =
2227
typeof opts.indentedSyntax === 'boolean'
2328
? opts.indentedSyntax
@@ -30,16 +35,24 @@ class SASSAsset extends CSSAsset {
3035
}
3136
});
3237

33-
let res = await render(opts);
34-
res.render = () => res.css.toString();
35-
return res;
38+
return await render(opts);
3639
}
3740

3841
collectDependencies() {
3942
for (let dep of this.ast.stats.includedFiles) {
4043
this.addDependency(dep, {includedInParent: true});
4144
}
4245
}
46+
47+
generate() {
48+
return [
49+
{
50+
type: 'css',
51+
value: this.ast.css.toString(),
52+
hasDependencies: false
53+
}
54+
];
55+
}
4356
}
4457

4558
module.exports = SASSAsset;

src/assets/StylusAsset.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1-
const CSSAsset = require('./CSSAsset');
1+
// const CSSAsset = require('./CSSAsset');
2+
const Asset = require('../Asset');
23
const localRequire = require('../utils/localRequire');
34
const Resolver = require('../Resolver');
45
const syncPromise = require('../utils/syncPromise');
56

67
const URL_RE = /^(?:url\s*\(\s*)?['"]?(?:[#/]|(?:https?:)?\/\/)/i;
78

8-
class StylusAsset extends CSSAsset {
9+
class StylusAsset extends Asset {
10+
constructor(name, pkg, options) {
11+
super(name, pkg, options);
12+
this.type = 'css';
13+
}
14+
915
async parse(code) {
1016
// stylus should be installed locally in the module that's being required
1117
let stylus = await localRequire('stylus', this.name);
@@ -26,8 +32,14 @@ class StylusAsset extends CSSAsset {
2632
return style;
2733
}
2834

29-
collectDependencies() {
30-
// Do nothing. Dependencies are collected by our custom evaluator.
35+
generate() {
36+
return [
37+
{
38+
type: 'css',
39+
value: this.ast.render(),
40+
hasDependencies: false
41+
}
42+
];
3143
}
3244

3345
generateErrorMessage(err) {

0 commit comments

Comments
 (0)