UNPKG

6.57 kBJavaScriptView Raw
1// const CSSAsset = require('./CSSAsset');
2const Asset = require('../Asset');
3const localRequire = require('../utils/localRequire');
4const Resolver = require('../Resolver');
5const fs = require('@parcel/fs');
6const {dirname, resolve, relative} = require('path');
7const {isGlob, glob} = require('../utils/glob');
8
9const URL_RE = /^(?:url\s*\(\s*)?['"]?(?:[#/]|(?:https?:)?\/\/)/i;
10
11class StylusAsset extends Asset {
12 constructor(name, options) {
13 super(name, options);
14 this.type = 'css';
15 }
16
17 async parse(code) {
18 // stylus should be installed locally in the module that's being required
19 let stylus = await localRequire('stylus', this.name);
20 let opts = await this.getConfig(['.stylusrc', '.stylusrc.js'], {
21 packageKey: 'stylus'
22 });
23 let style = stylus(code, opts);
24 style.set('filename', this.name);
25 style.set('include css', true);
26 // Setup a handler for the URL function so we add dependencies for linked assets.
27 style.define('url', node => {
28 let filename = this.addURLDependency(node.val, node.filename);
29 return new stylus.nodes.Literal(`url(${JSON.stringify(filename)})`);
30 });
31 style.set('Evaluator', await createEvaluator(code, this, style.options));
32
33 return style;
34 }
35
36 generate() {
37 return [
38 {
39 type: 'css',
40 value: this.ast.render(),
41 hasDependencies: false
42 }
43 ];
44 }
45
46 generateErrorMessage(err) {
47 let index = err.message.indexOf('\n');
48 err.codeFrame = err.message.slice(index + 1);
49 err.message = err.message.slice(0, index);
50 return err;
51 }
52}
53
54async function getDependencies(
55 code,
56 filepath,
57 asset,
58 options,
59 seen = new Set()
60) {
61 seen.add(filepath);
62 const [Parser, DepsResolver, nodes, utils] = await Promise.all(
63 ['parser', 'visitor/deps-resolver', 'nodes', 'utils'].map(dep =>
64 localRequire('stylus/lib/' + dep, filepath)
65 )
66 );
67
68 nodes.filename = asset.name;
69
70 let parser = new Parser(code, options);
71 let ast = parser.parse();
72 let deps = new Map();
73 let resolver = new Resolver(
74 Object.assign({}, asset.options, {
75 extensions: ['.styl', '.css']
76 })
77 );
78
79 class ImportVisitor extends DepsResolver {
80 visitImport(imported) {
81 let path = imported.path.first.string;
82
83 if (!deps.has(path)) {
84 if (isGlob(path)) {
85 deps.set(
86 path,
87 glob(resolve(dirname(filepath), path), {
88 onlyFiles: true
89 }).then(entries =>
90 Promise.all(
91 entries.map(entry =>
92 resolver.resolve(
93 './' + relative(dirname(filepath), entry),
94 filepath
95 )
96 )
97 )
98 )
99 );
100 } else {
101 deps.set(path, resolver.resolve(path, filepath));
102 }
103 }
104 }
105 }
106
107 new ImportVisitor(ast, options).visit(ast);
108
109 // Recursively process depdendencies, and return a map with all resolved paths.
110 let res = new Map();
111 await Promise.all(
112 Array.from(deps.entries()).map(async ([path, resolved]) => {
113 try {
114 resolved = await resolved;
115 resolved = Array.isArray(resolved)
116 ? resolved.map(r => r.path)
117 : resolved.path;
118 } catch (err) {
119 resolved = null;
120 }
121
122 let found;
123 if (resolved) {
124 found = Array.isArray(resolved) ? resolved : [resolved];
125 res.set(path, resolved);
126 } else {
127 // If we couldn't resolve, try the normal stylus resolver.
128 // We just need to do this to keep track of the dependencies - stylus does the real work.
129
130 // support optional .styl
131 let originalPath = path;
132 if (!/\.styl$/i.test(path)) {
133 path += '.styl';
134 }
135
136 let paths = (options.paths || []).concat(dirname(filepath || '.'));
137 found = utils.find(path, paths, filepath);
138 if (!found) {
139 found = utils.lookupIndex(originalPath, paths, filepath);
140 }
141
142 if (!found) {
143 throw new Error('failed to locate file ' + originalPath);
144 }
145 }
146
147 // Recursively process resolved files as well to get nested deps
148 for (let resolved of found) {
149 if (!seen.has(resolved)) {
150 asset.addDependency(resolved, {includedInParent: true});
151
152 let code = await fs.readFile(resolved, 'utf8');
153 for (let [path, resolvedPath] of await getDependencies(
154 code,
155 resolved,
156 asset,
157 options,
158 seen
159 )) {
160 res.set(path, resolvedPath);
161 }
162 }
163 }
164 })
165 );
166
167 return res;
168}
169
170async function createEvaluator(code, asset, options) {
171 const deps = await getDependencies(code, asset.name, asset, options);
172 const Evaluator = await localRequire(
173 'stylus/lib/visitor/evaluator',
174 asset.name
175 );
176
177 // This is a custom stylus evaluator that extends stylus with support for the node
178 // require resolution algorithm. It also adds all dependencies to the parcel asset
179 // tree so the file watcher works correctly, etc.
180 class CustomEvaluator extends Evaluator {
181 visitImport(imported) {
182 let node = this.visit(imported.path).first;
183 let path = node.string;
184 if (node.name !== 'url' && path && !URL_RE.test(path)) {
185 let resolved = deps.get(path);
186
187 // First try resolving using the node require resolution algorithm.
188 // This allows stylus files in node_modules to be resolved properly.
189 // If we find something, update the AST so stylus gets the absolute path to load later.
190 if (resolved) {
191 if (!Array.isArray(resolved)) {
192 node.string = resolved;
193 } else {
194 // If the import resolves to multiple files (i.e. glob),
195 // replace it with a separate import node for each file
196 return mergeBlocks(
197 resolved.map(resolvedPath => {
198 node.string = resolvedPath;
199 return super.visitImport(imported.clone());
200 })
201 );
202 }
203 }
204 }
205
206 // Done. Let stylus do its thing.
207 return super.visitImport(imported);
208 }
209 }
210
211 return CustomEvaluator;
212}
213
214/**
215 * Puts the content of all given node blocks into the first one, essentially merging them.
216 */
217function mergeBlocks(blocks) {
218 let finalBlock;
219 for (const block of blocks) {
220 if (!finalBlock) finalBlock = block;
221 else {
222 block.nodes.forEach(node => finalBlock.push(node));
223 }
224 }
225 return finalBlock;
226}
227
228module.exports = StylusAsset;