-
-
Notifications
You must be signed in to change notification settings - Fork 579
/
prune-globals.js
159 lines (149 loc) · 6.01 KB
/
prune-globals.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
// Usage: node prune-globals.js
// This script will document which `window.* = ...` assignments are used by other files in the src directory.
// Also its purpose is very specific; it only looks for global assignments following a "// Temporary globals" comment.
const fs = require("fs");
const path = require("path");
const espree = require("espree");
// Assuming all files are in the src directory and not subdirectories
const srcDir = "./src";
// Silly wrapper function to read a file and return its content
function readFile(filePath) {
return fs.readFileSync(filePath, "utf8");
}
// Function to write content to a file
function writeFile(filePath, content) {
console.log("Writing file", filePath);
fs.writeFileSync(filePath, content, "utf8");
}
// function escapeRegExp(string) {
// return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
// }
// Function to find non-ESM usages of an identifier
// Unstructured naive search
// function findDependencies(identifier, fileContent, excludeFiles = []) {
// // console.log("Finding dependencies for", identifier);
// const importRegex = new RegExp(`^\\s*import .*${escapeRegExp(identifier)}.* from`, "m");
// // Identifiers starting with $ will not work with \b because $ is not part of \w
// // const usageRegex = new RegExp(`\\b${escapeRegExp(identifier)}\\b`, "m");
// // So instead we use a negative lookbehind and a negative lookahead
// // This is not Unicode-friendly, but it should work for ASCII
// const usageRegex = new RegExp(`(?<![0-9A-Z_$])${escapeRegExp(identifier)}(?![0-9A-Z_$])`, "m");
// const dependencies = [];
// for (const [filePath, content] of Object.entries(fileContent)) {
// if (excludeFiles.includes(filePath)) {
// continue;
// }
// if (usageRegex.test(content)) {
// if (!importRegex.test(content)) {
// // console.log(`'${filePath}' includes '${identifier}' but doesn't match ${importRegex}`);
// // console.log("Usage:\n ", content.match(new RegExp(`.*${escapeRegExp(identifier)}.*`, "m"))[0]);
// dependencies.push(filePath);
// }
// }
// }
// return dependencies;
// }
// Somewhat more structured search using tokens but not fully utilizing AST
// This will correctly ignore comments and strings, but will still give false positives for identifiers in local scopes and such.
function findDependencies(identifier, fileContentTree, excludeFiles = []) {
const dependencies = [];
for (const [filePath, tree] of Object.entries(fileContentTree)) {
if (excludeFiles.includes(filePath)) {
continue;
}
console.log("Checking", filePath, "for", identifier);
// Look for imports of the identifier:
let foundImport = false;
for (const node of tree.body) {
if (node.type === "ImportDeclaration") {
for (const specifier of node.specifiers) {
if (specifier.imported.name === identifier) {
console.log("Found ESM (non-global) import of", identifier, "in", filePath);
foundImport = true;
break;
}
}
}
}
if (foundImport) {
continue;
}
// Look for other usages of the identifier:
// console.log("Tokens:", tree.tokens);
for (const token of tree.tokens) {
if (token.type === "Identifier" && token.value === identifier) {
// const parent = token.parent;
dependencies.push(filePath);
console.log("Found", identifier, "in", filePath);
break;
}
}
}
return dependencies;
}
// Function to process each file in the src directory
function processFiles() {
const fileUpperContent = {};
const fileUpperContentTree = {};
const fileLowerContent = {};
fs.readdir(srcDir, (err, files) => {
if (err) {
console.error("Error reading directory:", err);
return;
}
// First read all files into memory, since we'll need to look at each file
// in reference to every other file, and there's not that many files.
for (const file of files) {
if (path.extname(file) === ".js") {
const filePath = path.join(srcDir, file);
const content = readFile(filePath);
// Break the files into content above and below (and including) the "// Temporary globals" comment,
// in order to avoid matching the identifier within the comments generated by this script.
// The identifier matching is still naive, but this lets the script be idempotent.
// By passing only upper content to findDependencies,
// while the replacements are only made to the lower content,
// we avoid changes to the content that would affect the ultimate behavior of the script.
const startIndex = content.indexOf("// Temporary globals");
if (startIndex !== -1) {
fileUpperContent[filePath] = content.slice(0, startIndex);
fileLowerContent[filePath] = content.slice(startIndex);
} else {
fileUpperContent[filePath] = content;
fileLowerContent[filePath] = "";
}
try {
fileUpperContentTree[filePath] = espree.parse(fileUpperContent[filePath], {
ecmaVersion: 2020,
sourceType: fileUpperContent[filePath].match(/import .* from/) ? "module" : "script",
tokens: true,
});
} catch (e) {
console.error(`Error parsing ${filePath}:`, e);
}
}
}
// Then process each file.
for (const filePath of Object.keys(fileUpperContent)) {
console.log("--------", filePath);
const upper = fileUpperContent[filePath];
const lower = fileLowerContent[filePath];
// Match and replace in one fell swoop
const updatedContent = upper + lower.replace(
/(?:\/\/\s*)?(window\.(.*?) = .*;)(\s*\/\/.*)?/g,
(_match, assignment, identifier, _comment) => {
const dependencies = findDependencies(identifier, fileUpperContentTree, [filePath]);
const formatPath = (filePath) => path.relative(srcDir, filePath).replace(/\\/g, "/");
const formattedPaths = dependencies.map(formatPath).join(", ");
console.log(`Dependencies for ${identifier}: ${formattedPaths || "(none found)"}`);
if (dependencies.length) {
return `${assignment} // may be used by ${formattedPaths}`;
} else {
return `// ${assignment} // unused`;
}
}
);
writeFile(filePath, updatedContent);
}
});
}
processFiles();