UNPKG

7.61 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 buildDependencyError(content, {
53 type,
54 identifier,
55 request
56}, {
57 styles,
58 resource
59}, loc) {
60 let idents = styles.map(s => s.identifier);
61 let closest;
62 let minDistance = 2;
63 idents.forEach(ident => {
64 const d = _fastLevenshtein.default.get(ident, identifier);
65
66 if (d < minDistance) {
67 minDistance = d;
68 closest = ident;
69 }
70 });
71 const isDefaultImport = type === 'ImportDefaultSpecifier';
72
73 if (!closest && isDefaultImport) {
74 closest = idents.find(ident => ident === (0, _createFilename.getNameFromFile)(resource));
75 }
76
77 if (closest) idents = idents.filter(ident => ident !== closest);
78 idents = idents.map(s => _chalk.default.yellow(s)).join(', ');
79 const alternative = isDefaultImport ? `Instead try: ${_chalk.default.yellow(`import ${closest} from '${request}';`)}` : `Did you mean to import as ${_chalk.default.yellow(closest)} instead?`;
80 return new AstroturfLoaderError( // eslint-disable-next-line prefer-template
81 `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, {
82 start: loc.start
83 }, {
84 highlightCode: true,
85 message: !isDefaultImport ? `(Imported as ${_chalk.default.bold(identifier)})` : ''
86 }) + `\n\n${closest ? `${alternative}\n\nAlso available: ${idents}` : `Available: ${idents}`}`);
87}
88
89function collectStyles(src, filename, resolveDependency, opts) {
90 // maybe eventually return the ast directly if babel-loader supports it
91 try {
92 const {
93 metadata
94 } = (0, _traverse.default)(src, filename, { ...opts,
95 resolveDependency,
96 writeFiles: false,
97 generateInterpolations: true
98 });
99 return metadata.astroturf;
100 } catch (err) {
101 throw new AstroturfLoaderError(err);
102 }
103}
104
105function replaceStyleTemplates(src, locations) {
106 locations = (0, _sortBy.default)(locations, i => i.start || 0);
107 let offset = 0;
108
109 function splice(str, start = 0, end = 0, replace) {
110 const result = str.slice(0, start + offset) + replace + str.slice(end + offset);
111 offset += replace.length - (end - start);
112 return result;
113 }
114
115 locations.forEach(({
116 start,
117 end,
118 code
119 }) => {
120 if (code.endsWith(';')) code = code.slice(0, -1); // remove trailing semicolon
121
122 src = splice(src, start, end, code);
123 });
124 return src;
125}
126
127const LOADER_PLUGIN = Symbol('loader added VM plugin');
128const SEEN = Symbol('astroturf seen modules');
129
130module.exports = function loader(content, map, meta) {
131 const {
132 resourcePath,
133 _compilation: compilation
134 } = this;
135 const cb = this.async();
136
137 const timeout = async (ms, promise, err) => {
138 const handle = setTimeout(() => {
139 this.emitWarning(err);
140 }, ms);
141
142 try {
143 return await promise;
144 } finally {
145 clearTimeout(handle);
146 }
147 };
148
149 if (!compilation[SEEN]) compilation[SEEN] = new Map();
150
151 const loadModule = _util.default.promisify((request, done) => this.loadModule(request, (err, _, __, module) => done(err, module)));
152
153 const resolve = _util.default.promisify(this.resolve);
154
155 const buildDependency = async request => {
156 const resource = await resolve((0, _path.dirname)(resourcePath), request);
157 const maybeCycle = compilation[SEEN].has(resource); // It's hard to know if a seen module is due to a cycle or just already done
158 // I'm sure there is a cleaner way to handle this but IDK what it is, so we bail
159 // after a second of perceived deadlock
160
161 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);
162 };
163
164 const options = _loaderUtils.default.getOptions(this) || {};
165 const dependencies = [];
166
167 function resolveDependency(interpolation, localStyle, node) {
168 const {
169 identifier,
170 request
171 } = interpolation;
172 if (!interpolation.identifier) return null;
173 const {
174 loc
175 } = node;
176 const memberProperty = node.property && node.property.name;
177 const imported = `###ASTROTURF_IMPORTED_${dependencies.length}###`;
178 const source = `###ASTROTURF_SOURCE_${dependencies.length}###`;
179 debug(`resolving dependency: ${request}`);
180 dependencies.push(buildDependency(request).then(module => {
181 const style = module.styles.find(s => s.identifier === identifier);
182
183 if (!style) {
184 throw buildDependencyError(content, interpolation, module, loc);
185 }
186
187 debug(`resolved request to: ${style.absoluteFilePath}`);
188 localStyle.value = localStyle.value.replace(source, `~${style.absoluteFilePath}`).replace(imported, style.isStyledComponent ? 'cls1' : memberProperty);
189 }));
190 return {
191 source,
192 imported
193 };
194 }
195
196 const {
197 styles = [],
198 changeset
199 } = collectStyles(content, resourcePath, resolveDependency, options);
200
201 if (meta) {
202 meta.styles = styles;
203 }
204
205 if (!styles.length) {
206 return cb(null, content);
207 }
208
209 compilation[SEEN].set(resourcePath, styles);
210 this._module.styles = styles;
211 let {
212 emitVirtualFile
213 } = this; // The plugin isn't loaded
214
215 if (!emitVirtualFile) {
216 const {
217 compiler
218 } = compilation;
219 let plugin = compiler[LOADER_PLUGIN];
220
221 if (!plugin) {
222 debug('adding plugin to compiiler');
223 plugin = _VirtualModulePlugin.default.bootstrap(compilation);
224 compiler[LOADER_PLUGIN] = plugin;
225 }
226
227 emitVirtualFile = plugin.addFile;
228 } // console.log('WAIT', resourcePath);
229
230
231 return Promise.all(dependencies).then(() => {
232 styles.forEach(style => {
233 const mtime = emitVirtualFile(style.absoluteFilePath, style.value);
234 compilation.fileTimestamps.set(style.absoluteFilePath, +mtime);
235 });
236 const result = replaceStyleTemplates(content, changeset);
237 cb(null, result);
238 }).catch(cb); // console.log('DONE', resourcePath);
239};
\No newline at end of file