UNPKG

11.6 kBJavaScriptView Raw
1"use strict";
2var __importDefault = (this && this.__importDefault) || function (mod) {
3 return (mod && mod.__esModule) ? mod : { "default": mod };
4};
5Object.defineProperty(exports, "__esModule", { value: true });
6exports.scanImportsFromFiles = exports.scanImports = exports.scanDepList = exports.matchDynamicImportValue = exports.getInstallTargets = void 0;
7const es_module_lexer_1 = require("es-module-lexer");
8const glob_1 = __importDefault(require("glob"));
9const path_1 = __importDefault(require("path"));
10const strip_comments_1 = __importDefault(require("strip-comments"));
11const url_1 = __importDefault(require("url"));
12const logger_1 = require("./logger");
13const util_1 = require("./util");
14const p_queue_1 = __importDefault(require("p-queue"));
15const CONCURRENT_FILE_READS = 1000;
16// [@\w] - Match a word-character or @ (valid package name)
17// (?!.*(:\/\/)) - Ignore if previous match was a protocol (ex: http://)
18const BARE_SPECIFIER_REGEX = /^[@\w](?!.*(:\/\/))/;
19const ESM_IMPORT_REGEX = /import(?:["'\s]*([\w*${}\n\r\t, ]+)\s*from\s*)?\s*["'](.*?)["']/gm;
20const ESM_DYNAMIC_IMPORT_REGEX = /(?<!\.)\bimport\((?:['"].+['"]|`[^$]+`)\)/gm;
21const HAS_NAMED_IMPORTS_REGEX = /^[\w\s\,]*\{(.*)\}/s;
22const STRIP_AS = /\s+as\s+.*/; // for `import { foo as bar }`, strips “as bar”
23const DEFAULT_IMPORT_REGEX = /import\s+(\w)+(,\s\{[\w\s]*\})?\s+from/s;
24function createInstallTarget(specifier, all = true) {
25 return {
26 specifier,
27 all,
28 default: false,
29 namespace: false,
30 named: [],
31 };
32}
33async function getInstallTargets(config, knownEntrypoints, scannedFiles) {
34 let installTargets = [];
35 if (knownEntrypoints.length > 0) {
36 installTargets.push(...scanDepList(knownEntrypoints, config.root));
37 }
38 // TODO: remove this if block; move logic inside scanImports
39 if (scannedFiles) {
40 installTargets.push(...(await scanImportsFromFiles(scannedFiles, config)));
41 }
42 else {
43 installTargets.push(...(await scanImports(process.env.NODE_ENV === 'test', config)));
44 }
45 return installTargets;
46}
47exports.getInstallTargets = getInstallTargets;
48function matchDynamicImportValue(importStatement) {
49 const matched = strip_comments_1.default(importStatement).match(/^\s*('([^']+)'|"([^"]+)")\s*$/m);
50 return (matched === null || matched === void 0 ? void 0 : matched[2]) || (matched === null || matched === void 0 ? void 0 : matched[3]) || null;
51}
52exports.matchDynamicImportValue = matchDynamicImportValue;
53function getWebModuleSpecifierFromCode(code, imp) {
54 // import.meta: we can ignore
55 if (imp.d === -2) {
56 return null;
57 }
58 // Static imports: easy to parse
59 if (imp.d === -1) {
60 return code.substring(imp.s, imp.e);
61 }
62 // Dynamic imports: a bit trickier to parse. Today, we only support string literals.
63 const importStatement = code.substring(imp.s, imp.e);
64 return matchDynamicImportValue(importStatement);
65}
66/**
67 * parses an import specifier, looking for a web modules to install. If a web module is not detected,
68 * null is returned.
69 */
70function parseWebModuleSpecifier(specifier) {
71 if (!specifier) {
72 return null;
73 }
74 // If specifier is a "bare module specifier" (ie: package name) just return it directly
75 if (BARE_SPECIFIER_REGEX.test(specifier)) {
76 return specifier;
77 }
78 return null;
79}
80function parseImportStatement(code, imp) {
81 const webModuleSpecifier = parseWebModuleSpecifier(getWebModuleSpecifierFromCode(code, imp));
82 if (!webModuleSpecifier) {
83 return null;
84 }
85 const importStatement = strip_comments_1.default(code.substring(imp.ss, imp.se));
86 if (/^import\s+type/.test(importStatement)) {
87 return null;
88 }
89 const isDynamicImport = imp.d > -1;
90 const hasDefaultImport = !isDynamicImport && DEFAULT_IMPORT_REGEX.test(importStatement);
91 const hasNamespaceImport = !isDynamicImport && importStatement.includes('*');
92 const namedImports = (importStatement.match(HAS_NAMED_IMPORTS_REGEX) || [, ''])[1]
93 .split(',') // split `import { a, b, c }` by comma
94 .map((name) => name.replace(STRIP_AS, '').trim()) // remove “ as …” and trim
95 .filter(util_1.isTruthy);
96 return {
97 specifier: webModuleSpecifier,
98 all: isDynamicImport || (!hasDefaultImport && !hasNamespaceImport && namedImports.length === 0),
99 default: hasDefaultImport,
100 namespace: hasNamespaceImport,
101 named: namedImports,
102 };
103}
104function cleanCodeForParsing(code) {
105 code = strip_comments_1.default(code);
106 const allMatches = [];
107 let match;
108 const importRegex = new RegExp(ESM_IMPORT_REGEX);
109 while ((match = importRegex.exec(code))) {
110 allMatches.push(match);
111 }
112 const dynamicImportRegex = new RegExp(ESM_DYNAMIC_IMPORT_REGEX);
113 while ((match = dynamicImportRegex.exec(code))) {
114 allMatches.push(match);
115 }
116 return allMatches.map(([full]) => full).join('\n');
117}
118function parseJsForInstallTargets(contents) {
119 let imports;
120 // Attempt #1: Parse the file as JavaScript. JSX and some decorator
121 // syntax will break this.
122 try {
123 [imports] = es_module_lexer_1.parse(contents) || [];
124 }
125 catch (err) {
126 // Attempt #2: Parse only the import statements themselves.
127 // This lets us guarentee we aren't sending any broken syntax to our parser,
128 // but at the expense of possible false +/- caused by our regex extractor.
129 contents = cleanCodeForParsing(contents);
130 [imports] = es_module_lexer_1.parse(contents) || [];
131 }
132 return (imports
133 .map((imp) => parseImportStatement(contents, imp))
134 .filter(util_1.isTruthy)
135 // Babel macros are not install targets!
136 .filter((target) => !/[./]macro(\.js)?$/.test(target.specifier)));
137}
138function parseCssForInstallTargets(code) {
139 const installTargets = [];
140 let match;
141 const importRegex = new RegExp(util_1.CSS_REGEX);
142 while ((match = importRegex.exec(code))) {
143 const [, spec] = match;
144 const webModuleSpecifier = parseWebModuleSpecifier(spec);
145 if (webModuleSpecifier) {
146 installTargets.push(createInstallTarget(webModuleSpecifier));
147 }
148 }
149 return installTargets;
150}
151function parseFileForInstallTargets({ locOnDisk, baseExt, contents, root, }) {
152 const relativeLoc = path_1.default.relative(root, locOnDisk);
153 try {
154 switch (baseExt) {
155 case '.css':
156 case '.less':
157 case '.sass':
158 case '.scss': {
159 logger_1.logger.debug(`Scanning ${relativeLoc} for imports as CSS`);
160 return parseCssForInstallTargets(contents);
161 }
162 case '.html':
163 case '.svelte':
164 case '.vue': {
165 logger_1.logger.debug(`Scanning ${relativeLoc} for imports as HTML`);
166 return [
167 ...parseCssForInstallTargets(extractCssFromHtml(contents)),
168 ...parseJsForInstallTargets(extractJsFromHtml({ contents, baseExt })),
169 ];
170 }
171 case '.js':
172 case '.jsx':
173 case '.mjs':
174 case '.ts':
175 case '.tsx': {
176 logger_1.logger.debug(`Scanning ${relativeLoc} for imports as JS`);
177 return parseJsForInstallTargets(contents);
178 }
179 default: {
180 logger_1.logger.debug(`Skip scanning ${relativeLoc} for imports (unknown file extension ${baseExt})`);
181 return [];
182 }
183 }
184 }
185 catch (err) {
186 // Another error! No hope left, just abort.
187 logger_1.logger.error(`! ${locOnDisk}`);
188 throw err;
189 }
190}
191/** Extract only JS within <script type="module"> tags (works for .svelte and .vue files, too) */
192function extractJsFromHtml({ contents, baseExt }) {
193 // TODO: Replace with matchAll once Node v10 is out of TLS.
194 // const allMatches = [...result.matchAll(new RegExp(HTML_JS_REGEX))];
195 const allMatches = [];
196 let match;
197 let regex = new RegExp(util_1.HTML_JS_REGEX);
198 if (baseExt === '.svelte' || baseExt === '.vue') {
199 regex = new RegExp(util_1.SVELTE_VUE_REGEX); // scan <script> tags, not <script type="module">
200 }
201 while ((match = regex.exec(contents))) {
202 allMatches.push(match);
203 }
204 return allMatches
205 .map((match) => match[2]) // match[2] is the code inside the <script></script> element
206 .filter((s) => s.trim())
207 .join('\n');
208}
209/** Extract only CSS within <style> tags (works for .svelte and .vue files, too) */
210function extractCssFromHtml(contents) {
211 // TODO: Replace with matchAll once Node v10 is out of TLS.
212 // const allMatches = [...result.matchAll(new RegExp(HTML_JS_REGEX))];
213 const allMatches = [];
214 let match;
215 let regex = new RegExp(util_1.HTML_STYLE_REGEX);
216 while ((match = regex.exec(contents))) {
217 allMatches.push(match);
218 }
219 return allMatches
220 .map((match) => match[2]) // match[2] is the code inside the <style></style> element
221 .filter((s) => s.trim())
222 .join('\n');
223}
224function scanDepList(depList, cwd) {
225 return depList
226 .map((whitelistItem) => {
227 if (!glob_1.default.hasMagic(whitelistItem)) {
228 return [createInstallTarget(whitelistItem, true)];
229 }
230 else {
231 const nodeModulesLoc = path_1.default.join(cwd, 'node_modules');
232 return scanDepList(glob_1.default.sync(whitelistItem, { cwd: nodeModulesLoc, nodir: true }), cwd);
233 }
234 })
235 .reduce((flat, item) => flat.concat(item), []);
236}
237exports.scanDepList = scanDepList;
238async function scanImports(includeTests, config) {
239 await es_module_lexer_1.init;
240 const ignore = includeTests ? config.exclude : [...config.exclude, ...config.testOptions.files];
241 const includeFileSets = await Promise.all(Object.keys(config.mount).map((fromDisk) => {
242 return glob_1.default.sync(`**/*`, {
243 ignore,
244 cwd: fromDisk,
245 absolute: true,
246 nodir: true,
247 });
248 }));
249 const includeFiles = Array.from(new Set([].concat.apply([], includeFileSets)));
250 if (includeFiles.length === 0) {
251 return [];
252 }
253 // Scan every matched JS file for web dependency imports
254 const loadFileQueue = new p_queue_1.default({ concurrency: CONCURRENT_FILE_READS });
255 const getLoadedFiles = async (filePath) => loadFileQueue.add(async () => {
256 return {
257 baseExt: util_1.getExtension(filePath),
258 root: config.root,
259 locOnDisk: filePath,
260 contents: await util_1.readFile(url_1.default.pathToFileURL(filePath)),
261 };
262 });
263 const loadedFiles = await Promise.all(includeFiles.map(getLoadedFiles));
264 return scanImportsFromFiles(loadedFiles.filter(util_1.isTruthy), config);
265}
266exports.scanImports = scanImports;
267async function scanImportsFromFiles(loadedFiles, config) {
268 await es_module_lexer_1.init;
269 return loadedFiles
270 .filter((sourceFile) => !Buffer.isBuffer(sourceFile.contents)) // filter out binary files from import scanning
271 .map((sourceFile) => parseFileForInstallTargets(sourceFile))
272 .reduce((flat, item) => flat.concat(item), [])
273 .filter((target) => {
274 const aliasEntry = util_1.findMatchingAliasEntry(config, target.specifier);
275 return !aliasEntry || aliasEntry.type === 'package';
276 })
277 .sort((impA, impB) => impA.specifier.localeCompare(impB.specifier));
278}
279exports.scanImportsFromFiles = scanImportsFromFiles;