Skip to content

Commit

Permalink
Make CSS assets async (#1390)
Browse files Browse the repository at this point in the history
Remove `deasync` from CSS assets.
Fixes #1331 and bring some performance improvements.

#### Status

- [x] Sass
- [x] Less
- [x] Stylus
  • Loading branch information
fathyb authored and Jasper De Moor committed May 28, 2018
1 parent 9861c46 commit 0d63879
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 53 deletions.
25 changes: 13 additions & 12 deletions src/assets/LESSAsset.js
Expand Up @@ -2,7 +2,6 @@ const Asset = require('../Asset');
const localRequire = require('../utils/localRequire');
const promisify = require('../utils/promisify');
const Resolver = require('../Resolver');
const syncPromise = require('../utils/syncPromise');
const fs = require('../utils/fs');
const path = require('path');

Expand Down Expand Up @@ -72,21 +71,23 @@ function getFileManager(less, options) {
});

class LessFileManager extends less.FileManager {
async resolve(filename, currentDirectory) {
return (await resolver.resolve(
filename,
path.join(currentDirectory, 'index')
)).path;
supports() {
return true;
}

async loadFile(filename, currentDirectory) {
filename = await this.resolve(filename, currentDirectory);
let contents = await fs.readFile(filename, 'utf8');
return {contents, filename};
supportsSync() {
return false;
}

loadFileSync(filename, currentDirectory) {
return syncPromise(this.loadFile(filename, currentDirectory));
async loadFile(filename, currentDirectory) {
let resolved = await resolver.resolve(
filename,
path.join(currentDirectory, 'index')
);
return {
contents: await fs.readFile(resolved.path, 'utf8'),
filename: resolved.path
};
}
}

Expand Down
43 changes: 26 additions & 17 deletions src/assets/SASSAsset.js
Expand Up @@ -4,7 +4,6 @@ const promisify = require('../utils/promisify');
const path = require('path');
const os = require('os');
const Resolver = require('../Resolver');
const syncPromise = require('../utils/syncPromise');

class SASSAsset extends Asset {
constructor(name, options) {
Expand Down Expand Up @@ -37,26 +36,21 @@ class SASSAsset extends Asset {
return new sass.types.String(`url(${JSON.stringify(filename)})`);
}
});

opts.importer = opts.importer || [];
opts.importer = Array.isArray(opts.importer) ? opts.importer : [opts.importer];
opts.importer.push((url, prev, done) => {
let resolved;
try {
if (!/^(~|\.\/|\/)/.test(url)) {
url = './' + url;
} else if (!/^(~\/|\.\/|\/)/.test(url)) {
url = url.substring(1);
}
resolved = syncPromise(
resolver.resolve(url, prev === 'stdin' ? this.name : prev)
).path;
} catch (e) {
resolved = url;
if (!/^(~|\.\/|\/)/.test(url)) {
url = './' + url;
} else if (!/^(~\/|\.\/|\/)/.test(url)) {
url = url.substring(1);
}
return done({
file: resolved
});
resolver
.resolve(url, prev === 'stdin' ? this.name : prev)
.then(resolved => resolved.path)
.catch(() => url)
.then(file => done({file}))
.catch(err => done(normalizeError(err)));
});

return await render(opts);
Expand All @@ -80,3 +74,18 @@ class SASSAsset extends Asset {
}

module.exports = SASSAsset;

// Ensures an error inherits from Error
function normalizeError(err) {
let message = 'Unknown error';

if (err) {
if (err instanceof Error) {
return err;
}

message = err.stack || err.message || err;
}

return new Error(message);
}
67 changes: 50 additions & 17 deletions src/assets/StylusAsset.js
Expand Up @@ -2,7 +2,6 @@
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;

Expand All @@ -21,13 +20,12 @@ class StylusAsset extends Asset {
let style = stylus(code, opts);
style.set('filename', this.name);
style.set('include css', true);
style.set('Evaluator', await createEvaluator(this));

// Setup a handler for the URL function so we add dependencies for linked assets.
style.define('url', node => {
let filename = this.addURLDependency(node.val, node.filename);
return new stylus.nodes.Literal(`url(${JSON.stringify(filename)})`);
});
style.set('Evaluator', await createEvaluator(code, this, style.options));

return style;
}
Expand All @@ -50,18 +48,53 @@ class StylusAsset extends Asset {
}
}

async function createEvaluator(asset) {
const Evaluator = await localRequire(
'stylus/lib/visitor/evaluator',
asset.name
async function getDependencies(code, asset, options) {
const [Parser, DepsResolver, nodes] = await Promise.all(
['parser', 'visitor/deps-resolver', 'nodes'].map(dep =>
localRequire('stylus/lib/' + dep, asset.name)
)
);
const utils = await localRequire('stylus/lib/utils', asset.name);
const resolver = new Resolver(

nodes.filename = asset.name;
class ImportVisitor extends DepsResolver {
visitImport(imported) {
let path = imported.path.first.string;

if (!deps.has(path)) {
deps.set(path, resolver.resolve(path, asset.name).then(m => m.path));
}
}
}

let parser = new Parser(code, options);
let ast = parser.parse();
let deps = new Map();
let resolver = new Resolver(
Object.assign({}, asset.options, {
extensions: ['.styl', '.css']
})
);

new ImportVisitor(ast, options).visit(ast);

// Return a map with all await'd paths
return new Map(
await Promise.all(
Array.from(deps.entries()).map(async ([path, resolved]) => [
path,
await resolved.catch(() => null)
])
)
);
}

async function createEvaluator(code, asset, options) {
const deps = await getDependencies(code, asset, options);
const Evaluator = await localRequire(
'stylus/lib/visitor/evaluator',
asset.name
);
const utils = await localRequire('stylus/lib/utils', asset.name);
// This is a custom stylus evaluator that extends stylus with support for the node
// require resolution algorithm. It also adds all dependencies to the parcel asset
// tree so the file watcher works correctly, etc.
Expand All @@ -70,15 +103,15 @@ async function createEvaluator(asset) {
let node = this.visit(imported.path).first;
let path = node.string;
if (node.name !== 'url' && path && !URL_RE.test(path)) {
try {
// First try resolving using the node require resolution algorithm.
// This allows stylus files in node_modules to be resolved properly.
// If we find something, update the AST so stylus gets the absolute path to load later.
node.string = syncPromise(
resolver.resolve(path, imported.filename)
).path;
let resolved = deps.get(path);

// First try resolving using the node require resolution algorithm.
// This allows stylus files in node_modules to be resolved properly.
// If we find something, update the AST so stylus gets the absolute path to load later.
if (resolved) {
node.string = resolved;
asset.addDependency(node.string, {includedInParent: true});
} catch (err) {
} else {
// If we couldn't resolve, try the normal stylus resolver.
// We just need to do this to keep track of the dependencies - stylus does the real work.

Expand Down
24 changes: 17 additions & 7 deletions src/utils/pipeSpawn.js
Expand Up @@ -2,13 +2,23 @@ const spawn = require('cross-spawn');
const logger = require('../Logger');

function pipeSpawn(cmd, params, opts) {
const cp = spawn(cmd, params, Object.assign({
env: Object.assign({
FORCE_COLOR: logger.color,
npm_config_color: logger.color ? 'always': '',
npm_config_progress: true
}, process.env)
}, opts));
const cp = spawn(
cmd,
params,
Object.assign(
{
env: Object.assign(
{
FORCE_COLOR: logger.color,
npm_config_color: logger.color ? 'always' : '',
npm_config_progress: true
},
process.env
)
},
opts
)
);

cp.stdout.setEncoding('utf8').on('data', d => logger.writeRaw(d));
cp.stderr.setEncoding('utf8').on('data', d => logger.writeRaw(d));
Expand Down

0 comments on commit 0d63879

Please sign in to comment.