UNPKG

4.39 kBJavaScriptView Raw
1'use strict'
2
3const path = require('path');
4const fs = require('fs-extra');
5const resolve = require('resolve');
6const babelParser = require('@babel/parser');
7const walk = require('babylon-walk');
8const {execSync} = require('child_process');
9
10function getDependencies(filePath) {
11 const content = fs.readFileSync(filePath, 'utf-8');
12 const ast = babelParser.parse(content, {sourceType: 'module', plugins: ['typescript', 'objectRestSpread', 'classProperties']})
13
14 const visitors = {
15 ImportDeclaration(node, state) {
16 state.push(node.source.value)
17 },
18 CallExpression(node, state) {
19 if (node.callee.name === 'require' && node.arguments.length > 0 && node.arguments[0].type === 'StringLiteral') {
20 state.push(node.arguments[0].value)
21 }
22 }
23 };
24
25 const childDeps = [];
26 walk.recursive(ast, visitors, childDeps);
27 return childDeps;
28}
29
30/**
31 * @param {string} p
32 * @return boolean
33 */
34function shouldFollow(p) {
35 if (!p) {
36 return false
37 }
38 if (/node_modules/.test(p)) {
39 const arr = p.split('/')
40 const pkgPath = arr.splice(0, arr.findIndex(x => x === 'node_modules') + 2).join('/')
41
42 const stats = fs.lstatSync(pkgPath)
43 const r = stats.isSymbolicLink()
44 // if (r) {
45 // console.log(p, 'is symlink')
46 // }
47 return r
48 }
49 return true
50}
51
52// function isSymLink(p) {
53// const stats = fs.lstatSync(p)
54// return stats.isSymbolicLink()
55// }
56
57function tryResolveExt(dir, i) {
58 const vars = ['.js', '.ts']
59 for (const v of vars) {
60 const r = tryResolve(dir, addExt(i, v))
61 if (r) {
62 return r
63 }
64 }
65}
66
67function tryResolve(basedir, i) {
68 try {
69 return resolve.sync(i, {basedir})
70 // return requireUtil.resolve(i)
71 } catch (e) {
72 // console.log(i, e)
73 }
74}
75
76function addExt(f, ext = '.js') {
77 const exts = ['.js', '.json', '.ts']
78 return exts.includes(path.extname(f)) ? f : f + ext
79}
80
81function mtime(filename) {
82 return +fs.statSync(filename).mtime;
83}
84
85function loadCache(cacheFilePath) {
86 if (!fs.existsSync(cacheFilePath)) {
87 return null;
88 }
89
90 let data;
91
92 try {
93 data = fs.readJsonSync(cacheFilePath);
94 } catch (err) {
95 return null;
96 }
97
98 return {data, time: mtime(cacheFilePath)};
99}
100
101function analyzeFile(filePath, cache) {
102 if (cache && cache.data[filePath] && mtime(filePath) <= cache.time) {
103 return cache.data[filePath];
104 }
105
106 let dependencies = [];
107
108 try {
109 switch (path.extname(filePath)) {
110 case '.ts':
111 case '.js':
112 dependencies = getDependencies(filePath);
113 break;
114
115 default:
116 break;
117 }
118 } catch (error) {
119 // fail gracefully, we treat this module as if it has no child dependencies
120 }
121
122 return dependencies
123 .map(childFilePath => {
124 const absoluteChildPath = tryResolveExt(
125 path.dirname(filePath),
126 childFilePath
127 );
128
129 return absoluteChildPath;
130 })
131 .filter(absoluteChildPath => shouldFollow(absoluteChildPath));
132}
133
134function analyzeDependencies(entryFilePath, statsFilePath) {
135 const modules = {};
136 const cache = loadCache(statsFilePath);
137
138 const queue = [entryFilePath];
139
140 for (const filePath of queue) {
141 if (!modules[filePath]) {
142 const dependencies = analyzeFile(filePath, cache);
143
144 // push to queue
145 queue.push(...dependencies);
146
147 // set our state
148 modules[filePath] = dependencies;
149 }
150 }
151
152 return modules;
153}
154
155const isEveryFileBefore = (files, time) => files.every(f => mtime(f) < time)
156
157/**
158 * @param {string[]} deps
159 * @param {string} cacheFilePath
160 * @return {boolean}
161 */
162function isUpToDate(deps, cacheFilePath) {
163 const depsArray = Object.keys(deps)
164
165 try {
166 const outTime = mtime(cacheFilePath)
167 return isEveryFileBefore(depsArray, outTime)
168 } catch (e) {
169 return false
170 }
171}
172
173const getDependenciesHashes = (dependencies) => {
174 try {
175 const depsArray = Object.keys(dependencies);
176 return execSync(`git ls-tree --abbrev=7 --full-name -r HEAD ${depsArray.join(' ')}`, {encoding: 'utf8'}).split('\n').reduce((total, item) => {
177 if (item) {
178 const [, , hash] = item.split(/\s/);
179 return total.concat(hash);
180 }
181 return total;
182 }, []);
183 } catch (e) {
184 console.warn("Can't use `git ls-tree` for the current cache scenario. Using fallback to `mtime` file check");
185 return undefined;
186 }
187};
188
189module.exports = {
190 isUpToDate,
191 getDependenciesHashes,
192 analyzeDependencies
193}