UNPKG

7.88 kBJavaScriptView Raw
1"use strict";
2
3var _path = require("path");
4
5var _util = _interopRequireDefault(require("util"));
6
7var _chalk = _interopRequireDefault(require("chalk"));
8
9var _fastLevenshtein = _interopRequireDefault(require("fast-levenshtein"));
10
11var _loaderUtils = _interopRequireDefault(require("loader-utils"));
12
13var _sortBy = _interopRequireDefault(require("lodash/sortBy"));
14
15var _codeFrame = require("@babel/code-frame");
16
17var _traverse = _interopRequireDefault(require("./traverse"));
18
19var _createFilename = require("./utils/createFilename");
20
21var _VirtualModulePlugin = _interopRequireDefault(require("./VirtualModulePlugin"));
22
23function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
24
25const debug = _util.default.debuglog('astroturf:loader'); // can'ts use class syntax b/c babel doesn't transpile it correctly for Error
26
27
28function AstroturfLoaderError(errorOrMessage, codeFrame = errorOrMessage.codeFrame) {
29 Error.call(this);
30 this.name = 'AstroturfLoaderError';
31
32 if (typeof errorOrMessage !== 'string') {
33 this.message = errorOrMessage.message;
34 this.error = errorOrMessage;
35
36 try {
37 this.stack = errorOrMessage.stack.replace(/^(.*?):/, `${this.name}:`);
38 } catch (err) {
39 Error.captureStackTrace(this, AstroturfLoaderError);
40 }
41 } else {
42 this.message = errorOrMessage;
43 Error.captureStackTrace(this, AstroturfLoaderError);
44 }
45
46 if (codeFrame) this.message += `\n\n${codeFrame}\n`;
47}
48
49AstroturfLoaderError.prototype = Object.create(Error.prototype);
50AstroturfLoaderError.prototype.constructor = AstroturfLoaderError;
51
52function timeout(ms, promise, err) {
53 return Promise.race([promise, new Promise((resolve, reject) => {
54 setTimeout(() => reject(err), ms);
55 })]);
56}
57
58function buildDependencyError(content, {
59 type,
60 identifier,
61 request
62}, {
63 styles,
64 resource
65}, loc) {
66 let idents = styles.map(s => s.identifier);
67 let closest;
68 let minDistance = 2;
69 idents.forEach(ident => {
70 const d = _fastLevenshtein.default.get(ident, identifier);
71
72 if (d < minDistance) {
73 minDistance = d;
74 closest = ident;
75 }
76 });
77 const isDefaultImport = type === 'ImportDefaultSpecifier';
78
79 if (!closest && isDefaultImport) {
80 closest = idents.find(ident => ident === (0, _createFilename.getNameFromFile)(resource));
81 }
82
83 if (closest) idents = idents.filter(ident => ident !== closest);
84 idents = idents.map(s => _chalk.default.yellow(s)).join(', ');
85 const alternative = isDefaultImport ? `Instead try: ${_chalk.default.yellow(`import ${closest} from '${request}';`)}` : `Did you mean to import as ${_chalk.default.yellow(closest)} instead?`;
86 return new AstroturfLoaderError( // eslint-disable-next-line prefer-template
87 `Could not find a style associated with the interpolated value. ` + `Styles should use the same name used by the intended component or class set in the imported file.\n\n` + (0, _codeFrame.codeFrameColumns)(content, {
88 start: loc.start
89 }, {
90 highlightCode: true,
91 message: !isDefaultImport ? `(Imported as ${_chalk.default.bold(identifier)})` : ''
92 }) + `\n\n${closest ? `${alternative}\n\nAlso available: ${idents}` : `Available: ${idents}`}`);
93}
94
95function collectStyles(src, filename, resolveDependency, opts) {
96 const tagName = opts.tagName || 'css';
97 const styledTag = opts.styledTag || 'styled'; // quick regex as an optimization to avoid parsing each file
98
99 if (!src.match(new RegExp(`(${tagName}|${styledTag}(.|\\n|\\r)+?)\\s*\`([\\s\\S]*?)\``, 'gmi')) && opts.cssPropEnabled && !src.match(/css=("|')/g)) {
100 return {
101 styles: []
102 };
103 } // maybe eventually return the ast directly if babel-loader supports it
104
105
106 try {
107 const {
108 metadata
109 } = (0, _traverse.default)(src, filename, { ...opts,
110 resolveDependency,
111 writeFiles: false,
112 generateInterpolations: true
113 });
114 return metadata.astroturf;
115 } catch (err) {
116 throw new AstroturfLoaderError(err);
117 }
118}
119
120function replaceStyleTemplates(src, locations) {
121 locations = (0, _sortBy.default)(locations, i => i.start || 0);
122 let offset = 0;
123
124 function splice(str, start = 0, end = 0, replace) {
125 const result = str.slice(0, start + offset) + replace + str.slice(end + offset);
126 offset += replace.length - (end - start);
127 return result;
128 }
129
130 locations.forEach(({
131 start,
132 end,
133 code
134 }) => {
135 if (code.endsWith(';')) code = code.slice(0, -1); // remove trailing semicolon
136
137 src = splice(src, start, end, code);
138 });
139 return src;
140}
141
142const LOADER_PLUGIN = Symbol('loader added VM plugin');
143const SEEN = Symbol('astroturf seen modules');
144
145module.exports = function loader(content, map, meta) {
146 const {
147 resourcePath,
148 _compilation: compilation
149 } = this;
150 const cb = this.async();
151 if (!compilation[SEEN]) compilation[SEEN] = new Map();
152
153 const loadModule = _util.default.promisify((request, done) => this.loadModule(request, (err, _, __, module) => done(err, module)));
154
155 const resolve = _util.default.promisify(this.resolve);
156
157 const buildDependency = async request => {
158 const resource = await resolve((0, _path.dirname)(resourcePath), request);
159 const maybeCycle = compilation[SEEN].has(resource); // It's hard to know if a seen module is due to a cycle or just already done
160 // I'm sure there is a cleaner way to handle this but IDK what it is, so we bail
161 // after a second of perceived deadlock
162
163 return maybeCycle ? timeout(10000, loadModule(resource), new AstroturfLoaderError('A possible cyclical style interpolation was detected in an interpolated stylesheet or component which is not supported.\n' + `while importing "${request}" in ${resourcePath}`)) : loadModule(resource);
164 };
165
166 const options = _loaderUtils.default.getOptions(this) || {};
167 const dependencies = [];
168
169 function resolveDependency(interpolation, localStyle, node) {
170 const {
171 identifier,
172 request
173 } = interpolation;
174 if (!interpolation.identifier) return null;
175 const {
176 loc
177 } = node;
178 const memberProperty = node.property && node.property.name;
179 const imported = `###ASTROTURF_IMPORTED_${dependencies.length}###`;
180 const source = `###ASTROTURF_SOURCE_${dependencies.length}###`;
181 debug(`resolving dependency: ${request}`);
182 dependencies.push(buildDependency(request).then(module => {
183 const style = module.styles.find(s => s.identifier === identifier);
184
185 if (!style) {
186 throw buildDependencyError(content, interpolation, module, loc);
187 }
188
189 debug(`resolved request to: ${style.absoluteFilePath}`);
190 localStyle.value = localStyle.value.replace(source, `~${style.absoluteFilePath}`).replace(imported, style.isStyledComponent ? 'cls1' : memberProperty);
191 }));
192 return {
193 source,
194 imported
195 };
196 }
197
198 const {
199 styles = [],
200 changeset
201 } = collectStyles(content, resourcePath, resolveDependency, options);
202
203 if (meta) {
204 meta.styles = styles;
205 }
206
207 if (!styles.length) {
208 return cb(null, content);
209 }
210
211 compilation[SEEN].set(resourcePath, styles);
212 this._module.styles = styles;
213 let {
214 emitVirtualFile
215 } = this; // The plugin isn't loaded
216
217 if (!emitVirtualFile) {
218 const {
219 compiler
220 } = compilation;
221 let plugin = compiler[LOADER_PLUGIN];
222
223 if (!plugin) {
224 debug('adding plugin to compiiler');
225 plugin = _VirtualModulePlugin.default.bootstrap(compilation);
226 compiler[LOADER_PLUGIN] = plugin;
227 }
228
229 emitVirtualFile = plugin.addFile;
230 } // console.log('WAIT', resourcePath);
231
232
233 return Promise.all(dependencies).then(() => {
234 styles.forEach(style => {
235 const mtime = emitVirtualFile(style.absoluteFilePath, style.value);
236 compilation.fileTimestamps.set(style.absoluteFilePath, +mtime);
237 });
238 const result = replaceStyleTemplates(content, changeset);
239 cb(null, result);
240 }).catch(cb); // console.log('DONE', resourcePath);
241};
\No newline at end of file