UNPKG

8.29 kBJavaScriptView Raw
1const { basename, dirname, extname, relative, resolve, sep } = require('path');
2const fs = require('fs');
3const analyze = require('./analyze');
4const resolveDependency = require('./resolve-dependency');
5const { isMatch } = require('micromatch');
6const sharedlibEmit = require('./utils/sharedlib-emit');
7
8const { gracefulify } = require('graceful-fs');
9gracefulify(fs);
10
11function inPath (path, parent) {
12 return path.startsWith(parent) && path[parent.length] === sep;
13}
14
15module.exports = async function (files, opts = {}) {
16 const job = new Job(opts);
17
18 if (opts.readFile)
19 job.readFile = opts.readFile;
20 if (opts.stat)
21 job.stat = opts.stat;
22 if (opts.readlink)
23 job.readlink = opts.readlink;
24
25 job.ts = true;
26
27 await Promise.all(files.map(file => {
28 const path = resolve(file);
29 job.emitFile(job.realpath(path), 'initial');
30 if (path.endsWith('.js') || path.endsWith('.node') || job.ts && (path.endsWith('.ts') || path.endsWith('.tsx')))
31 return job.emitDependency(path);
32 }));
33
34 return {
35 fileList: [...job.fileList].sort(),
36 esmFileList: [...job.esmFileList].sort(),
37 reasons: job.reasons,
38 warnings: [...job.warnings]
39 };
40};
41
42class Job {
43 constructor ({
44 base = process.cwd(),
45 paths = {},
46 ignore,
47 log = false,
48 mixedModules = false,
49 analysis = {},
50 cache,
51 }) {
52 base = resolve(base);
53 this.ignoreFn = path => {
54 if (path.startsWith('..' + sep)) return true;
55 return false;
56 };
57 if (typeof ignore === 'string') ignore = [ignore];
58 if (typeof ignore === 'function') {
59 this.ignoreFn = path => {
60 if (path.startsWith('..' + sep)) return true;
61 if (ignore(path)) return true;
62 return false;
63 };
64 }
65 else if (Array.isArray(ignore)) {
66 const resolvedIgnores = ignore.map(ignore => relative(base, resolve(base || process.cwd(), ignore)));
67 this.ignoreFn = path => {
68 if (path.startsWith('..' + sep)) return true;
69 if (isMatch(path, resolvedIgnores)) return true;
70 return false;
71 }
72 }
73 this.base = base;
74 const resolvedPaths = {};
75 for (const path of Object.keys(paths)) {
76 const trailer = paths[path].endsWith('/');
77 const resolvedPath = resolve(base, paths[path]);
78 resolvedPaths[path] = resolvedPath + (trailer ? '/' : '');
79 }
80 this.paths = resolvedPaths;
81 this.log = log;
82 this.mixedModules = mixedModules;
83 this.reasons = Object.create(null);
84
85 this.analysis = {};
86 if (analysis !== false) {
87 Object.assign(this.analysis, {
88 // whether to glob any analysis like __dirname + '/dir/' or require('x/' + y)
89 // that might output any file in a directory
90 emitGlobs: true,
91 // whether __filename and __dirname style
92 // expressions should be analyzed as file references
93 computeFileReferences: true,
94 // evaluate known bindings to assist with glob and file reference analysis
95 evaluatePureExpressions: true,
96 }, analysis === true ? {} : analysis);
97 }
98
99 this.fileCache = cache && cache.fileCache || new Map();
100 this.statCache = cache && cache.statCache || new Map();
101 this.symlinkCache = cache && cache.symlinkCache || new Map();
102 this.analysisCache = cache && cache.analysisCache || new Map();
103
104 if (cache) {
105 cache.fileCache = this.fileCache;
106 cache.statCache = this.statCache;
107 cache.symlinkCache = this.symlinkCache;
108 cache.analysisCache = this.analysisCache;
109 }
110
111 this.fileList = new Set();
112 this.esmFileList = new Set();
113 this.processed = new Set();
114
115 this.warnings = new Set();
116 }
117
118 readlink (path) {
119 const cached = this.symlinkCache.get(path);
120 if (cached !== undefined) return cached;
121 try {
122 const link = fs.readlinkSync(path);
123 // also copy stat cache to symlink
124 const stats = this.statCache.get(path);
125 if (stats)
126 this.statCache.set(resolve(path, link), stats);
127 this.symlinkCache.set(path, link);
128 return link;
129 }
130 catch (e) {
131 if (e.code !== 'EINVAL' && e.code !== 'ENOENT' && e.code !== 'UNKNOWN')
132 throw e;
133 this.symlinkCache.set(path, null);
134 return null;
135 }
136 }
137
138 isFile (path) {
139 const stats = this.stat(path);
140 if (stats)
141 return stats.isFile();
142 return false;
143 }
144
145 isDir (path) {
146 const stats = this.stat(path);
147 if (stats)
148 return stats.isDirectory();
149 return false;
150 }
151
152 stat (path) {
153 const cached = this.statCache.get(path);
154 if (cached) return cached;
155 try {
156 const stats = fs.statSync(path);
157 this.statCache.set(path, stats);
158 return stats;
159 }
160 catch (e) {
161 if (e.code === 'ENOENT') {
162 this.statCache.set(path, null);
163 return null;
164 }
165 throw e;
166 }
167 }
168
169 readFile (path) {
170 const cached = this.fileCache.get(path);
171 if (cached !== undefined) return cached;
172 try {
173 const source = fs.readFileSync(path).toString();
174 this.fileCache.set(path, source);
175 return source;
176 }
177 catch (e) {
178 if (e.code === 'ENOENT' || e.code === 'EISDIR') {
179 this.fileCache.set(path, null);
180 return null;
181 }
182 throw e;
183 }
184 }
185
186 realpath (path, parent, seen = new Set()) {
187 if (seen.has(path)) throw new Error('Recursive symlink detected resolving ' + path);
188 seen.add(path);
189 const symlink = this.readlink(path);
190 // emit direct symlink paths only
191 if (symlink) {
192 const parentPath = dirname(path);
193 const resolved = resolve(parentPath, symlink);
194 const realParent = this.realpath(parentPath, parent);
195 if (inPath(path, realParent))
196 this.emitFile(path, 'resolve', parent);
197 return this.realpath(resolved, parent, seen);
198 }
199 // keep backtracking for realpath, emitting folder symlinks within base
200 if (!inPath(path, this.base))
201 return path;
202 return this.realpath(dirname(path), parent, seen) + sep + basename(path);
203 }
204
205 emitFile (path, reason, parent) {
206 if (this.fileList.has(path)) return;
207 path = relative(this.base, path);
208 if (parent)
209 parent = relative(this.base, parent);
210 const reasonEntry = this.reasons[path] || (this.reasons[path] = {
211 type: reason,
212 ignored: false,
213 parents: []
214 });
215 if (parent && reasonEntry.parents.indexOf(parent) === -1)
216 reasonEntry.parents.push(parent);
217 if (parent && this.ignoreFn(path, parent)) {
218 if (reasonEntry) reasonEntry.ignored = true;
219 return false;
220 }
221 this.fileList.add(path);
222 return true;
223 }
224
225 async emitDependency (path, parent) {
226 if (this.processed.has(path)) return;
227 this.processed.add(path);
228
229 const emitted = this.emitFile(path, 'dependency', parent);
230 if (!emitted) return;
231 if (path.endsWith('.json')) return;
232 if (path.endsWith('.node')) return await sharedlibEmit(path, this);
233
234 let deps, assets, isESM;
235
236 const cachedAnalysis = this.analysisCache.get(path);
237 if (cachedAnalysis) {
238 ({ deps, assets, isESM } = cachedAnalysis);
239 }
240 else {
241 const source = this.readFile(path);
242 if (source === null) throw new Error('File ' + path + ' does not exist.');
243 ({ deps, assets, isESM } = await analyze(path, source, this));
244 this.analysisCache.set(path, { deps, assets, isESM });
245 }
246
247 if (isESM)
248 this.esmFileList.add(relative(this.base, path));
249 await Promise.all([
250 ...[...assets].map(async asset => {
251 const ext = extname(asset);
252 if (ext === '.js' || ext === '.mjs' || ext === '.node' || ext === '' ||
253 this.ts && (ext === '.ts' || ext === '.tsx') && asset.startsWith(this.base) && asset.substr(this.base.length).indexOf(sep + 'node_modules' + sep) === -1)
254 await this.emitDependency(asset, path);
255 else
256 this.emitFile(this.realpath(asset, path), 'asset', path);
257 }),
258 ...[...deps].map(async dep => {
259 try {
260 var resolved = await resolveDependency(dep, path, this);
261 // ignore builtins
262 if (resolved.startsWith('node:')) return;
263 }
264 catch (e) {
265 this.warnings.add(new Error(`Failed to resolve dependency ${dep}:\n${e && e.message}`));
266 return;
267 }
268 await this.emitDependency(resolved, path);
269 })
270 ]);
271 }
272}