1 |
|
2 | const Asset = require('../Asset');
|
3 | const localRequire = require('../utils/localRequire');
|
4 | const Resolver = require('../Resolver');
|
5 | const fs = require('@parcel/fs');
|
6 | const {dirname, resolve, relative} = require('path');
|
7 | const {isGlob, glob} = require('../utils/glob');
|
8 |
|
9 | const URL_RE = /^(?:url\s*\(\s*)?['"]?(?:[#/]|(?:https?:)?\/\/)/i;
|
10 |
|
11 | class StylusAsset extends Asset {
|
12 | constructor(name, options) {
|
13 | super(name, options);
|
14 | this.type = 'css';
|
15 | }
|
16 |
|
17 | async parse(code) {
|
18 |
|
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 |
|
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 |
|
54 | async 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 |
|
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 |
|
128 |
|
129 |
|
130 |
|
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 |
|
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 |
|
170 | async 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 |
|
178 |
|
179 |
|
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 |
|
188 |
|
189 |
|
190 | if (resolved) {
|
191 | if (!Array.isArray(resolved)) {
|
192 | node.string = resolved;
|
193 | } else {
|
194 |
|
195 |
|
196 | return mergeBlocks(
|
197 | resolved.map(resolvedPath => {
|
198 | node.string = resolvedPath;
|
199 | return super.visitImport(imported.clone());
|
200 | })
|
201 | );
|
202 | }
|
203 | }
|
204 | }
|
205 |
|
206 |
|
207 | return super.visitImport(imported);
|
208 | }
|
209 | }
|
210 |
|
211 | return CustomEvaluator;
|
212 | }
|
213 |
|
214 |
|
215 |
|
216 |
|
217 | function 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 |
|
228 | module.exports = StylusAsset;
|