Skip to content

Commit

Permalink
Implement pipelines to compose multiple asset types together (#1065)
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett committed Mar 27, 2018
1 parent 3ddaec9 commit 8a95f70
Show file tree
Hide file tree
Showing 13 changed files with 239 additions and 65 deletions.
13 changes: 12 additions & 1 deletion src/Asset.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class Asset {
this.type = path.extname(this.name).slice(1);

this.processed = false;
this.contents = null;
this.contents = options.rendition ? options.rendition.value : null;
this.ast = null;
this.generated = null;
this.hash = null;
Expand Down Expand Up @@ -58,6 +58,13 @@ class Asset {
}

async getDependencies() {
if (
this.options.rendition &&
this.options.rendition.hasDependencies === false
) {
return;
}

await this.loadIfNeeded();

if (this.contents && this.mightHaveDependencies()) {
Expand Down Expand Up @@ -154,6 +161,10 @@ class Asset {
return this.generated;
}

async postProcess(generated) {
return generated;
}

generateHash() {
return objectHash(this.generated);
}
Expand Down
102 changes: 102 additions & 0 deletions src/Pipeline.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
const Parser = require('./Parser');
const path = require('path');
const md5 = require('./utils/md5');

/**
* A Pipeline composes multiple Asset types together.
*/
class Pipeline {
constructor(options) {
this.options = options;
this.parser = new Parser(options);
}

async process(path, pkg, options) {
let asset = this.parser.getAsset(path, pkg, options);
let generated = await this.processAsset(asset);
let generatedMap = {};
for (let rendition of generated) {
generatedMap[rendition.type] = rendition.value;
}

return {
dependencies: Array.from(asset.dependencies.values()),
generated: generatedMap,
hash: asset.hash,
cacheData: asset.cacheData
};
}

async processAsset(asset) {
try {
await asset.process();
} catch (err) {
throw asset.generateErrorMessage(err);
}

let inputType = path.extname(asset.name).slice(1);
let generated = [];

for (let rendition of this.iterateRenditions(asset)) {
let {type, value} = rendition;
if (typeof value !== 'string' || rendition.final) {
generated.push(rendition);
continue;
}

// Find an asset type for the rendition type.
// If the asset is not already an instance of this asset type, process it.
let AssetType = this.parser.findParser(
asset.name.slice(0, -inputType.length) + type
);
if (!(asset instanceof AssetType)) {
let opts = Object.assign({rendition}, asset.options);
let subAsset = new AssetType(asset.name, asset.package, opts);
subAsset.contents = value;
subAsset.dependencies = asset.dependencies;

let processed = await this.processAsset(subAsset);
generated = generated.concat(processed);
Object.assign(asset.cacheData, subAsset.cacheData);
asset.hash = md5(asset.hash + subAsset.hash);
} else {
generated.push(rendition);
}
}

// Post process. This allows assets a chance to modify the output produced by sub-asset types.
asset.generated = generated;
try {
generated = await asset.postProcess(generated);
} catch (err) {
throw asset.generateErrorMessage(err);
}

return generated;
}

*iterateRenditions(asset) {
if (Array.isArray(asset.generated)) {
return yield* asset.generated;
}

if (typeof asset.generated === 'string') {
return yield {
type: asset.type,
value: asset.generated
};
}

// Backward compatibility support for the old API.
// Assume all renditions are final - don't compose asset types together.
for (let type in asset.generated) {
yield {
type,
value: asset.generated[type],
final: true
};
}
}
}

module.exports = Pipeline;
12 changes: 11 additions & 1 deletion src/assets/CSSAsset.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,17 @@ class CSSAsset extends Asset {
'module.exports = ' + JSON.stringify(this.cssModules, false, 2) + ';';
}

return {css, js};
return [
{
type: 'css',
value: css
},
{
type: 'js',
value: js,
final: true
}
];
}

generateErrorMessage(err) {
Expand Down
29 changes: 20 additions & 9 deletions src/assets/CoffeeScriptAsset.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
const JSAsset = require('./JSAsset');
const Asset = require('../Asset');
const localRequire = require('../utils/localRequire');

class CoffeeScriptAsset extends JSAsset {
async parse(code) {
class CoffeeScriptAsset extends Asset {
constructor(name, pkg, options) {
super(name, pkg, options);
this.type = 'js';
}

async generate() {
// require coffeescript, installed locally in the app
let coffee = await localRequire('coffeescript', this.name);

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

let sourceMap;
if (transpiled.sourceMap) {
this.sourceMap = transpiled.sourceMap.generate();
this.sourceMap.sources = [this.relativeName];
this.sourceMap.sourcesContent = [this.contents];
sourceMap = transpiled.sourceMap.generate();
sourceMap.sources = [this.relativeName];
sourceMap.sourcesContent = [this.contents];
}

this.contents = this.options.sourceMaps ? transpiled.js : transpiled;
return await super.parse(this.contents);
return [
{
type: 'js',
value: this.options.sourceMaps ? transpiled.js : transpiled,
sourceMap
}
];
}
}

Expand Down
3 changes: 1 addition & 2 deletions src/assets/HTMLAsset.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,7 @@ class HTMLAsset extends Asset {
}

generate() {
let html = this.isAstDirty ? render(this.ast) : this.contents;
return {html};
return this.isAstDirty ? render(this.ast) : this.contents;
}
}

Expand Down
1 change: 1 addition & 0 deletions src/assets/JSAsset.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class JSAsset extends Asset {
this.isES6Module = false;
this.outputCode = null;
this.cacheData.env = {};
this.sourceMap = options.rendition ? options.rendition.sourceMap : null;
}

shouldInvalidate(cacheData) {
Expand Down
23 changes: 18 additions & 5 deletions src/assets/LESSAsset.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
const CSSAsset = require('./CSSAsset');
const Asset = require('../Asset');
const localRequire = require('../utils/localRequire');
const promisify = require('../utils/promisify');

class LESSAsset extends CSSAsset {
class LESSAsset extends Asset {
constructor(name, pkg, options) {
super(name, pkg, options);
this.type = 'css';
}

async parse(code) {
// less should be installed locally in the module that's being required
let less = await localRequire('less', this.name);
Expand All @@ -15,16 +20,24 @@ class LESSAsset extends CSSAsset {
opts.filename = this.name;
opts.plugins = (opts.plugins || []).concat(urlPlugin(this));

let res = await render(code, opts);
res.render = () => res.css;
return res;
return await render(code, opts);
}

collectDependencies() {
for (let dep of this.ast.imports) {
this.addDependency(dep, {includedInParent: true});
}
}

generate() {
return [
{
type: 'css',
value: this.ast.css,
hasDependencies: false
}
];
}
}

function urlPlugin(asset) {
Expand Down
16 changes: 9 additions & 7 deletions src/assets/ReasonAsset.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
const JSAsset = require('./JSAsset');
const Asset = require('../Asset');
const fs = require('../utils/fs');
const localRequire = require('../utils/localRequire');

class ReasonAsset extends JSAsset {
async parse() {
class ReasonAsset extends Asset {
constructor(name, pkg, options) {
super(name, pkg, options);
this.type = 'js';
}

async generate() {
const bsb = await localRequire('bsb-js', this.name);

// This runs BuckleScript - the Reason to JS compiler.
Expand All @@ -18,10 +23,7 @@ class ReasonAsset extends JSAsset {
// BuckleScript configuration to simplify the file processing.
const outputFile = this.name.replace(/\.(re|ml)$/, '.bs.js');
const outputContent = await fs.readFile(outputFile);
this.contents = outputContent.toString();

// After loading the compiled JS source, use the normal JS behavior.
return await super.parse(this.contents);
return outputContent.toString();
}
}

Expand Down
25 changes: 19 additions & 6 deletions src/assets/SASSAsset.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
const CSSAsset = require('./CSSAsset');
const Asset = require('../Asset');
const localRequire = require('../utils/localRequire');
const promisify = require('../utils/promisify');
const path = require('path');
const os = require('os');

class SASSAsset extends CSSAsset {
class SASSAsset extends Asset {
constructor(name, pkg, options) {
super(name, pkg, options);
this.type = 'css';
}

async parse(code) {
// node-sass should be installed locally in the module that's being required
let sass = await localRequire('node-sass', this.name);
Expand All @@ -17,7 +22,7 @@ class SASSAsset extends CSSAsset {
opts.includePaths = (opts.includePaths || []).concat(
path.dirname(this.name)
);
opts.data = opts.data ? (opts.data + os.EOL + code) : code;
opts.data = opts.data ? opts.data + os.EOL + code : code;
opts.indentedSyntax =
typeof opts.indentedSyntax === 'boolean'
? opts.indentedSyntax
Expand All @@ -30,16 +35,24 @@ class SASSAsset extends CSSAsset {
}
});

let res = await render(opts);
res.render = () => res.css.toString();
return res;
return await render(opts);
}

collectDependencies() {
for (let dep of this.ast.stats.includedFiles) {
this.addDependency(dep, {includedInParent: true});
}
}

generate() {
return [
{
type: 'css',
value: this.ast.css.toString(),
hasDependencies: false
}
];
}
}

module.exports = SASSAsset;
20 changes: 16 additions & 4 deletions src/assets/StylusAsset.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
const CSSAsset = require('./CSSAsset');
// const CSSAsset = require('./CSSAsset');
const Asset = require('../Asset');
const localRequire = require('../utils/localRequire');
const Resolver = require('../Resolver');
const syncPromise = require('../utils/syncPromise');

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

class StylusAsset extends CSSAsset {
class StylusAsset extends Asset {
constructor(name, pkg, options) {
super(name, pkg, options);
this.type = 'css';
}

async parse(code) {
// stylus should be installed locally in the module that's being required
let stylus = await localRequire('stylus', this.name);
Expand All @@ -26,8 +32,14 @@ class StylusAsset extends CSSAsset {
return style;
}

collectDependencies() {
// Do nothing. Dependencies are collected by our custom evaluator.
generate() {
return [
{
type: 'css',
value: this.ast.render(),
hasDependencies: false
}
];
}

generateErrorMessage(err) {
Expand Down

0 comments on commit 8a95f70

Please sign in to comment.