Skip to content

Commit 44685df

Browse files
Adam Miskiewiczide
authored andcommitted
[expo] Add strip-haste script (Haste 1/3)
1 parent 9fb31d1 commit 44685df

File tree

1 file changed

+301
-0
lines changed

1 file changed

+301
-0
lines changed

scripts/strip-haste.js

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
#!/usr/bin/env node
2+
3+
const babel = require('@babel/core');
4+
const babelParser = require('@babel/parser');
5+
const fs = require('fs');
6+
const JestHasteMap = require('jest-haste-map');
7+
const _ = require('lodash');
8+
const path = require('path');
9+
const mkdirp = require('mkdirp');
10+
11+
const MetroConfig = require('metro-config');
12+
const metroDefaults = MetroConfig
13+
.getDefaultConfig
14+
.getDefaultValues(path.resolve(__dirname, '..'))
15+
.resolver;
16+
17+
const ROOTS = [
18+
path.resolve(__dirname, '..', 'Libraries'),
19+
path.resolve(__dirname, '..', 'jest'),
20+
path.resolve(__dirname, '..', 'integrationTests'),
21+
path.resolve(__dirname, '..', 'RNTester'),
22+
];
23+
24+
const OVERRIDES = {
25+
'React': 'react',
26+
};
27+
28+
const ignoreREs = [];
29+
30+
async function main() {
31+
// console.log('Cleaning...');
32+
// rimraf.sync(LibrariesDest);
33+
34+
const haste = createHasteMap();
35+
console.log('Loading dependency graph...');
36+
const { moduleMap } = await haste.build();
37+
console.log('Loaded dependency graph.');
38+
// await transformRequires({
39+
// source: ROOTS[1],
40+
// dest: ROOTS[1],
41+
// moduleMap: moduleMap.getRawModuleMap().map,
42+
// });
43+
for (let rootDir of ROOTS) {
44+
await transformRequires({
45+
source: rootDir,
46+
dest: rootDir,
47+
moduleMap: moduleMap.getRawModuleMap().map,
48+
});
49+
}
50+
}
51+
52+
async function transformRequires({ source, dest, moduleMap }) {
53+
const sourceDir = fs.readdirSync(source);
54+
for (let filename of sourceDir) {
55+
const filePath = path.resolve(source, filename);
56+
if (_.some(ignoreREs.map(r => filePath.match(r)))) {
57+
continue;
58+
}
59+
const fileStats = fs.statSync(filePath);
60+
if (fileStats.isDirectory()) {
61+
await transformRequires({
62+
source: filePath,
63+
dest: path.join(dest, filename),
64+
moduleMap,
65+
});
66+
} else {
67+
await _transformRequiresInFile(
68+
filePath,
69+
path.join(dest, filename),
70+
moduleMap
71+
);
72+
}
73+
}
74+
}
75+
76+
function _transformRequiresInFile(sourceFilePath, destFilePath, moduleMap) {
77+
const dirname = path.dirname(destFilePath);
78+
// Make the directory if it doesn't exist
79+
mkdirp.sync(dirname);
80+
81+
// If not a JS file, just copy the file
82+
if (!sourceFilePath.endsWith('.js')) {
83+
// console.log(`Writing ${destFilePath}...`);
84+
// fs
85+
// .createReadStream(sourceFilePath)
86+
// .pipe(fs.createWriteStream(destFilePath));
87+
return;
88+
}
89+
90+
// Get dependencies
91+
const code = fs.readFileSync(sourceFilePath, 'utf8');
92+
console.log(`Writing ${destFilePath}...`);
93+
const { dependencyOffsets, dependencies } = extractDependencies(code);
94+
95+
const dependencyMap = dependencies.reduce((result, dep, i) => {
96+
if (!moduleMap.has(dep)) {
97+
return result;
98+
}
99+
100+
let depPath;
101+
if (OVERRIDES[dep]) {
102+
depPath = OVERRIDES[dep];
103+
} else {
104+
const mod = moduleMap.get(dep);
105+
let modulePath;
106+
if (mod.g) {
107+
modulePath = mod.g[0];
108+
} else if (mod.ios) {
109+
modulePath = mod.ios[0];
110+
} else if (mod.android) {
111+
modulePath = mod.android[0];
112+
} else {
113+
return result;
114+
}
115+
116+
depPath = path.relative(path.dirname(sourceFilePath), modulePath);
117+
if (!depPath.startsWith('.')) {
118+
depPath = `./${depPath}`;
119+
}
120+
depPath = depPath.replace(/(.*)\.[^.]+$/, '$1'); // remove extension
121+
depPath = depPath.replace(/(.*).(android|ios)/, '$1'); // remove platform ext
122+
}
123+
124+
return Object.assign({}, result, {
125+
[dep]: {
126+
offset: dependencyOffsets[i],
127+
replacement: depPath,
128+
},
129+
});
130+
}, {});
131+
132+
const newCode = dependencyOffsets
133+
.reduceRight(
134+
([unhandled, handled], offset) => [
135+
unhandled.slice(0, offset),
136+
replaceDependency(unhandled.slice(offset) + handled, dependencyMap),
137+
],
138+
[code, '']
139+
)
140+
.join('');
141+
142+
fs.writeFileSync(destFilePath, newCode);
143+
}
144+
145+
function createHasteMap() {
146+
return new JestHasteMap({
147+
extensions: metroDefaults.sourceExts.concat(metroDefaults.assetExts),
148+
hasteImplModulePath: path.resolve(__dirname, '../jest/hasteImpl'),
149+
maxWorkers: 1,
150+
ignorePattern: /\/__tests__\//,
151+
mocksPattern: '',
152+
platforms: metroDefaults.platforms,
153+
providesModuleNodeModules: [],
154+
resetCache: true,
155+
retainAllFiles: true,
156+
rootDir: path.resolve(__dirname, '..'),
157+
roots: ROOTS,
158+
useWatchman: true,
159+
watch: false,
160+
});
161+
}
162+
163+
const reDepencencyString = /^(['"])([^'"']*)\1/;
164+
function replaceDependency(stringWithDependencyIDAtStart, dependencyMap) {
165+
const match = reDepencencyString.exec(stringWithDependencyIDAtStart);
166+
const dependencyName = match && match[2];
167+
if (match != null && dependencyName in dependencyMap) {
168+
const { length } = match[0];
169+
const { replacement } = dependencyMap[dependencyName];
170+
return `'${replacement}'` + stringWithDependencyIDAtStart.slice(length);
171+
} else {
172+
return stringWithDependencyIDAtStart;
173+
}
174+
}
175+
176+
/**
177+
* Extracts dependencies (module IDs imported with the `require` function) from
178+
* a string containing code. This walks the full AST for correctness (versus
179+
* using, for example, regular expressions, that would be faster but inexact.)
180+
*
181+
* The result of the dependency extraction is an de-duplicated array of
182+
* dependencies, and an array of offsets to the string literals with module IDs.
183+
* The index points to the opening quote.
184+
*/
185+
function extractDependencies(code) {
186+
const ast = babelParser.parse(code, {
187+
sourceType: 'module',
188+
plugins: [
189+
'classProperties',
190+
'jsx',
191+
'flow',
192+
'exportExtensions',
193+
'asyncGenerators',
194+
'objectRestSpread',
195+
'optionalChaining',
196+
'nullishCoalescingOperator',
197+
],
198+
});
199+
const dependencies = new Set();
200+
const dependencyOffsets = [];
201+
const types = require('@babel/types');
202+
203+
const transformedFunctions = [
204+
'require',
205+
'require.resolve',
206+
'jest.requireActual',
207+
// 'jest.requireMock',
208+
'System.import',
209+
'mockComponent',
210+
];
211+
212+
const isJest = node => {
213+
try {
214+
let callee;
215+
if (node.isCallExpression()) {
216+
callee = node.get('callee');
217+
} else if (node.isMemberExpression()) {
218+
callee = node;
219+
}
220+
return (
221+
callee.get('object').isIdentifier({ name: 'jest' }) ||
222+
(callee.isMemberExpression() && isJest(callee.get('object')))
223+
);
224+
} catch (e) {
225+
return false;
226+
}
227+
};
228+
229+
const isValidJestFunc = node => {
230+
return (
231+
node.isIdentifier({ name: 'mock' }) ||
232+
node.isIdentifier({ name: 'unmock' }) ||
233+
node.isIdentifier({ name: 'doMock' }) ||
234+
node.isIdentifier({ name: 'dontMock' }) ||
235+
node.isIdentifier({ name: 'setMock' }) ||
236+
node.isIdentifier({ name: 'genMockFromModule' })
237+
);
238+
};
239+
240+
const transformCall = nodePath => {
241+
if (isJest(nodePath)) {
242+
const calleeProperty = nodePath.get('callee.property');
243+
if (isValidJestFunc(calleeProperty)) {
244+
const arg = nodePath.get('arguments.0');
245+
if (!arg || arg.type !== 'StringLiteral') {
246+
return;
247+
}
248+
dependencyOffsets.push(parseInt(arg.node.start, 10));
249+
dependencies.add(arg.node.value);
250+
}
251+
} else {
252+
const calleePath = nodePath.get('callee');
253+
const isNormalCall = transformedFunctions.some(pattern =>
254+
_matchesPattern(types, calleePath, pattern)
255+
);
256+
if (isNormalCall) {
257+
const arg = nodePath.get('arguments.0');
258+
if (!arg || arg.type !== 'StringLiteral') {
259+
return;
260+
}
261+
dependencyOffsets.push(parseInt(arg.node.start, 10));
262+
dependencies.add(arg.node.value);
263+
}
264+
}
265+
};
266+
267+
babel.traverse(ast, {
268+
ImportDeclaration: (nodePath) => {
269+
const sourceNode = nodePath.get('source').node;
270+
dependencyOffsets.push(parseInt(sourceNode.start, 10));
271+
dependencies.add(sourceNode.value);
272+
},
273+
CallExpression: transformCall,
274+
});
275+
276+
return {
277+
dependencyOffsets: [...dependencyOffsets].sort((a, b) => a - b),
278+
dependencies: Array.from(dependencies),
279+
};
280+
}
281+
282+
function _matchesPattern(types, calleePath, pattern) {
283+
const { node } = calleePath;
284+
285+
if (types.isMemberExpression(node)) {
286+
return calleePath.matchesPattern(pattern);
287+
}
288+
289+
if (!types.isIdentifier(node) || pattern.includes('.')) {
290+
return false;
291+
}
292+
293+
const name = pattern.split('.')[0];
294+
295+
return node.name === name;
296+
}
297+
298+
main().catch(e => {
299+
console.trace(e.message);
300+
throw e;
301+
});

0 commit comments

Comments
 (0)