1 | "use strict";
|
2 |
|
3 | var _path = require("path");
|
4 |
|
5 | var _util = _interopRequireDefault(require("util"));
|
6 |
|
7 | var _chalk = _interopRequireDefault(require("chalk"));
|
8 |
|
9 | var _fastLevenshtein = _interopRequireDefault(require("fast-levenshtein"));
|
10 |
|
11 | var _loaderUtils = _interopRequireDefault(require("loader-utils"));
|
12 |
|
13 | var _sortBy = _interopRequireDefault(require("lodash/sortBy"));
|
14 |
|
15 | var _codeFrame = require("@babel/code-frame");
|
16 |
|
17 | var _traverse = _interopRequireDefault(require("./traverse"));
|
18 |
|
19 | var _createFilename = require("./utils/createFilename");
|
20 |
|
21 | var _VirtualModulePlugin = _interopRequireDefault(require("./VirtualModulePlugin"));
|
22 |
|
23 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
24 |
|
25 | const debug = _util.default.debuglog('astroturf:loader');
|
26 |
|
27 |
|
28 | function 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 |
|
49 | AstroturfLoaderError.prototype = Object.create(Error.prototype);
|
50 | AstroturfLoaderError.prototype.constructor = AstroturfLoaderError;
|
51 |
|
52 | function timeout(ms, promise, err) {
|
53 | return Promise.race([promise, new Promise((resolve, reject) => {
|
54 | setTimeout(() => reject(err), ms);
|
55 | })]);
|
56 | }
|
57 |
|
58 | function 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(
|
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 |
|
95 | function collectStyles(src, filename, resolveDependency, opts) {
|
96 | const tagName = opts.tagName || 'css';
|
97 | const styledTag = opts.styledTag || 'styled';
|
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 | }
|
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 |
|
120 | function 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);
|
136 |
|
137 | src = splice(src, start, end, code);
|
138 | });
|
139 | return src;
|
140 | }
|
141 |
|
142 | const LOADER_PLUGIN = Symbol('loader added VM plugin');
|
143 | const SEEN = Symbol('astroturf seen modules');
|
144 |
|
145 | module.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);
|
160 |
|
161 |
|
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;
|
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 | }
|
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);
|
241 | }; |
\ | No newline at end of file |