1 | import { init as initESModuleLexer, parse } from 'es-module-lexer';
|
2 | import fs from 'fs';
|
3 | import glob from 'glob';
|
4 | import * as colors from 'kleur/colors';
|
5 | import mime from 'mime-types';
|
6 | import nodePath from 'path';
|
7 | import stripComments from 'strip-comments';
|
8 | import validatePackageName from 'validate-npm-package-name';
|
9 | import { isTruthy, findMatchingMountScript, HTML_JS_REGEX, getExt } from './util';
|
10 | const WEB_MODULES_TOKEN = 'web_modules/';
|
11 | const WEB_MODULES_TOKEN_LENGTH = WEB_MODULES_TOKEN.length;
|
12 |
|
13 |
|
14 | const BARE_SPECIFIER_REGEX = /^[@\w](?!.*(:\/\/))/;
|
15 | const ESM_IMPORT_REGEX = /import(?:["'\s]*([\w*${}\n\r\t, ]+)\s*from\s*)?\s*["'](.*?)["']/gm;
|
16 | const ESM_DYNAMIC_IMPORT_REGEX = /import\((?:['"].+['"]|`[^$]+`)\)/gm;
|
17 | const HAS_NAMED_IMPORTS_REGEX = /^[\w\s\,]*\{(.*)\}/s;
|
18 | const SPLIT_NAMED_IMPORTS_REGEX = /\bas\s+\w+|,/s;
|
19 | const DEFAULT_IMPORT_REGEX = /import\s+(\w)+(,\s\{[\w\s]*\})?\s+from/s;
|
20 | function stripJsExtension(dep) {
|
21 | return dep.replace(/\.m?js$/i, '');
|
22 | }
|
23 | function createInstallTarget(specifier, all = true) {
|
24 | return {
|
25 | specifier,
|
26 | all,
|
27 | default: false,
|
28 | namespace: false,
|
29 | named: [],
|
30 | };
|
31 | }
|
32 | function removeSpecifierQueryString(specifier) {
|
33 | const queryStringIndex = specifier.indexOf('?');
|
34 | if (queryStringIndex >= 0) {
|
35 | specifier = specifier.substring(0, queryStringIndex);
|
36 | }
|
37 | return specifier;
|
38 | }
|
39 | function getWebModuleSpecifierFromCode(code, imp) {
|
40 |
|
41 | if (imp.d === -2) {
|
42 | return null;
|
43 | }
|
44 |
|
45 | if (imp.d === -1) {
|
46 | return code.substring(imp.s, imp.e);
|
47 | }
|
48 |
|
49 | const importStatement = code.substring(imp.s, imp.e);
|
50 | const importSpecifierMatch = importStatement.match(/^\s*['"](.*)['"]\s*$/m);
|
51 | return importSpecifierMatch ? importSpecifierMatch[1] : null;
|
52 | }
|
53 |
|
54 |
|
55 |
|
56 |
|
57 | function parseWebModuleSpecifier(specifier) {
|
58 | if (!specifier) {
|
59 | return null;
|
60 | }
|
61 |
|
62 | if (BARE_SPECIFIER_REGEX.test(specifier)) {
|
63 | return specifier;
|
64 | }
|
65 |
|
66 | const cleanedSpecifier = removeSpecifierQueryString(specifier);
|
67 |
|
68 | const webModulesIndex = cleanedSpecifier.indexOf(WEB_MODULES_TOKEN);
|
69 | if (webModulesIndex === -1) {
|
70 | return null;
|
71 | }
|
72 |
|
73 |
|
74 | const resolvedSpecifier = cleanedSpecifier.substring(webModulesIndex + WEB_MODULES_TOKEN_LENGTH);
|
75 | const resolvedSpecifierWithoutExtension = stripJsExtension(resolvedSpecifier);
|
76 | if (validatePackageName(resolvedSpecifierWithoutExtension).validForNewPackages) {
|
77 | return resolvedSpecifierWithoutExtension;
|
78 | }
|
79 |
|
80 | return resolvedSpecifier;
|
81 | }
|
82 | function parseImportStatement(code, imp) {
|
83 | const webModuleSpecifier = parseWebModuleSpecifier(getWebModuleSpecifierFromCode(code, imp));
|
84 | if (!webModuleSpecifier) {
|
85 | return null;
|
86 | }
|
87 | const importStatement = code.substring(imp.ss, imp.se);
|
88 | if (/^import\s+type/.test(importStatement)) {
|
89 | return null;
|
90 | }
|
91 | const isDynamicImport = imp.d > -1;
|
92 | const hasDefaultImport = !isDynamicImport && DEFAULT_IMPORT_REGEX.test(importStatement);
|
93 | const hasNamespaceImport = !isDynamicImport && importStatement.includes('*');
|
94 | const namedImports = (importStatement.match(HAS_NAMED_IMPORTS_REGEX) || [, ''])[1]
|
95 | .split(SPLIT_NAMED_IMPORTS_REGEX)
|
96 | .map((name) => name.trim())
|
97 | .filter(isTruthy);
|
98 | return {
|
99 | specifier: webModuleSpecifier,
|
100 | all: isDynamicImport || (!hasDefaultImport && !hasNamespaceImport && namedImports.length === 0),
|
101 | default: hasDefaultImport,
|
102 | namespace: hasNamespaceImport,
|
103 | named: namedImports,
|
104 | };
|
105 | }
|
106 | function cleanCodeForParsing(code) {
|
107 | code = stripComments(code);
|
108 | const allMatches = [];
|
109 | let match;
|
110 | const importRegex = new RegExp(ESM_IMPORT_REGEX);
|
111 | while ((match = importRegex.exec(code))) {
|
112 | allMatches.push(match);
|
113 | }
|
114 | const dynamicImportRegex = new RegExp(ESM_DYNAMIC_IMPORT_REGEX);
|
115 | while ((match = dynamicImportRegex.exec(code))) {
|
116 | allMatches.push(match);
|
117 | }
|
118 | return allMatches.map(([full]) => full).join('\n');
|
119 | }
|
120 | function parseCodeForInstallTargets({ locOnDisk, baseExt, code, }) {
|
121 | let imports;
|
122 |
|
123 |
|
124 | try {
|
125 | if (baseExt === '.jsx' || baseExt === '.tsx') {
|
126 |
|
127 |
|
128 | throw new Error('JSX must be cleaned before parsing');
|
129 | }
|
130 | [imports] = parse(code) || [];
|
131 | }
|
132 | catch (err) {
|
133 |
|
134 |
|
135 |
|
136 | try {
|
137 | code = cleanCodeForParsing(code);
|
138 | [imports] = parse(code) || [];
|
139 | }
|
140 | catch (err) {
|
141 |
|
142 | console.error(colors.red(`! ${locOnDisk}`));
|
143 | throw err;
|
144 | }
|
145 | }
|
146 | const allImports = imports
|
147 | .map((imp) => parseImportStatement(code, imp))
|
148 | .filter(isTruthy)
|
149 |
|
150 | .filter((imp) => !/[./]macro(\.js)?$/.test(imp.specifier));
|
151 | return allImports;
|
152 | }
|
153 | export function scanDepList(depList, cwd) {
|
154 | return depList
|
155 | .map((whitelistItem) => {
|
156 | if (!glob.hasMagic(whitelistItem)) {
|
157 | return [createInstallTarget(whitelistItem, true)];
|
158 | }
|
159 | else {
|
160 | const nodeModulesLoc = nodePath.join(cwd, 'node_modules');
|
161 | return scanDepList(glob.sync(whitelistItem, { cwd: nodeModulesLoc, nodir: true }), cwd);
|
162 | }
|
163 | })
|
164 | .reduce((flat, item) => flat.concat(item), []);
|
165 | }
|
166 | export async function scanImports(cwd, config) {
|
167 | await initESModuleLexer;
|
168 | const includeFileSets = await Promise.all(config.scripts.map(({ type, args }) => {
|
169 | if (type !== 'mount') {
|
170 | return [];
|
171 | }
|
172 | if (args.fromDisk.includes('web_modules')) {
|
173 | return [];
|
174 | }
|
175 | const dirDisk = nodePath.resolve(cwd, args.fromDisk);
|
176 | return glob.sync(`**/*`, {
|
177 | ignore: config.exclude.concat(['**/web_modules/**/*']),
|
178 | cwd: dirDisk,
|
179 | absolute: true,
|
180 | nodir: true,
|
181 | });
|
182 | }));
|
183 | const includeFiles = Array.from(new Set([].concat.apply([], includeFileSets)));
|
184 | if (includeFiles.length === 0) {
|
185 | return [];
|
186 | }
|
187 |
|
188 | const loadedFiles = await Promise.all(includeFiles.map(async (filePath) => {
|
189 | const { baseExt, expandedExt } = getExt(filePath);
|
190 |
|
191 | if (filePath.startsWith('.')) {
|
192 | return null;
|
193 | }
|
194 | switch (baseExt) {
|
195 |
|
196 | case '': {
|
197 | return null;
|
198 | }
|
199 |
|
200 | case '.js':
|
201 | case '.jsx':
|
202 | case '.mjs':
|
203 | case '.ts':
|
204 | case '.tsx': {
|
205 | return {
|
206 | baseExt,
|
207 | expandedExt,
|
208 | locOnDisk: filePath,
|
209 | code: await fs.promises.readFile(filePath, 'utf-8'),
|
210 | };
|
211 | }
|
212 | case '.html':
|
213 | case '.vue':
|
214 | case '.svelte': {
|
215 | const result = await fs.promises.readFile(filePath, 'utf-8');
|
216 |
|
217 |
|
218 | const allMatches = [];
|
219 | let match;
|
220 | const regex = new RegExp(HTML_JS_REGEX);
|
221 | while ((match = regex.exec(result))) {
|
222 | allMatches.push(match);
|
223 | }
|
224 | return {
|
225 | baseExt,
|
226 | expandedExt,
|
227 | locOnDisk: filePath,
|
228 |
|
229 | code: allMatches
|
230 | .map((match) => match[2])
|
231 | .filter((s) => s.trim())
|
232 | .join('\n'),
|
233 | };
|
234 | }
|
235 | }
|
236 |
|
237 | if (!mime.lookup(baseExt)) {
|
238 | console.warn(colors.dim(`ignoring unsupported file "${nodePath.relative(process.cwd(), filePath)}"`));
|
239 | }
|
240 | return null;
|
241 | }));
|
242 | return scanImportsFromFiles(loadedFiles.filter(isTruthy), config);
|
243 | }
|
244 | export async function scanImportsFromFiles(loadedFiles, { scripts }) {
|
245 | return (loadedFiles
|
246 | .map(parseCodeForInstallTargets)
|
247 | .reduce((flat, item) => flat.concat(item), [])
|
248 |
|
249 | .filter((target) => !findMatchingMountScript(scripts, target.specifier))
|
250 | .sort((impA, impB) => impA.specifier.localeCompare(impB.specifier)));
|
251 | }
|