1 | 'use strict'
|
2 |
|
3 | const path = require('path');
|
4 | const fs = require('fs-extra');
|
5 | const resolve = require('resolve');
|
6 | const babelParser = require('@babel/parser');
|
7 | const walk = require('babylon-walk');
|
8 | const {execSync} = require('child_process');
|
9 |
|
10 | function 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 |
|
32 |
|
33 |
|
34 | function 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 |
|
45 |
|
46 |
|
47 | return r
|
48 | }
|
49 | return true
|
50 | }
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 | function 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 |
|
67 | function tryResolve(basedir, i) {
|
68 | try {
|
69 | return resolve.sync(i, {basedir})
|
70 |
|
71 | } catch (e) {
|
72 |
|
73 | }
|
74 | }
|
75 |
|
76 | function addExt(f, ext = '.js') {
|
77 | const exts = ['.js', '.json', '.ts']
|
78 | return exts.includes(path.extname(f)) ? f : f + ext
|
79 | }
|
80 |
|
81 | function mtime(filename) {
|
82 | return +fs.statSync(filename).mtime;
|
83 | }
|
84 |
|
85 | function 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 |
|
101 | function 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 |
|
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 |
|
134 | function 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 |
|
145 | queue.push(...dependencies);
|
146 |
|
147 |
|
148 | modules[filePath] = dependencies;
|
149 | }
|
150 | }
|
151 |
|
152 | return modules;
|
153 | }
|
154 |
|
155 | const isEveryFileBefore = (files, time) => files.every(f => mtime(f) < time)
|
156 |
|
157 |
|
158 |
|
159 |
|
160 |
|
161 |
|
162 | function 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 |
|
173 | const 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 |
|
189 | module.exports = {
|
190 | isUpToDate,
|
191 | getDependenciesHashes,
|
192 | analyzeDependencies
|
193 | }
|