incinerator
Advanced tools
Comparing version 0.1.1 to 0.2.0
@@ -7,4 +7,4 @@ { | ||
"test": "echo no test", | ||
"build": "webpack", | ||
"dev": "webpack --watch" | ||
"build": "webpack --mode production src/index.js", | ||
"dev": "webpack --watch --mode production src/index.js" | ||
}, | ||
@@ -11,0 +11,0 @@ "license": "ISC", |
199
index.js
let { resolve } = require("path"); | ||
let { | ||
readFileSync: readFile, | ||
writeFileSync: writeFile, | ||
readdirSync: readdir, | ||
statSync: stat | ||
} = require("fs"); | ||
let { readFile, writeFile, readdir, stat } = require("fs-extra"); | ||
let parser = require("@babel/parser"); | ||
@@ -13,2 +8,3 @@ let traverse = require("@babel/traverse").default; | ||
let ws = require("ws"); | ||
let chalkAnimation = require("chalk-animation"); | ||
@@ -18,29 +14,14 @@ let rootDir = resolve(process.argv[2] || ""); | ||
function iterateSourceFiles(dir, callback) { | ||
readdir(dir) | ||
.map(file => resolve(dir, file)) | ||
.forEach(file => { | ||
if (stat(file).isDirectory()) { | ||
iterateSourceFiles(file, callback); | ||
} else { | ||
callback(file); | ||
} | ||
}); | ||
async function iterateSourceFiles(root, callback) { | ||
let fileStat = await stat(root); | ||
if (fileStat.isDirectory()) { | ||
let files = await readdir(root); | ||
return Promise.all( | ||
files.map(file => iterateSourceFiles(resolve(root, file), callback)) | ||
); | ||
} else { | ||
await callback(root); | ||
} | ||
} | ||
let jsFiles = []; | ||
iterateSourceFiles(rootDir, file => { | ||
let source = readFile(file, "utf-8"); | ||
let ast = null; | ||
try { | ||
ast = parser.parse(source); | ||
} catch (err) { | ||
if (err.name !== "SyntaxError") throw err; | ||
} | ||
if (ast) { | ||
jsFiles.push({ file, ast }); | ||
} | ||
}); | ||
function toAST(code) { | ||
@@ -90,35 +71,39 @@ let ast = parser.parse(code); | ||
function writeAST(path, ast) { | ||
writeFile(path, generate(ast).code); | ||
async function writeAST(path, ast) { | ||
await writeFile(path, generate(ast).code + "\n"); | ||
} | ||
let functionId = 0; | ||
let functionPathMap = new Map(); | ||
async function confirmIncineration() { | ||
let stdin; | ||
await new Promise(resolve => { | ||
stdin = process.openStdin(); | ||
stdin.addListener("data", data => { | ||
let str = data.toString(); | ||
if (str.trim().toLowerCase() === "incinerate!") { | ||
resolve(); | ||
} else if (str.includes("!")) { | ||
console.log("Well, anyway I'll incinerate!"); | ||
resolve(); | ||
} else { | ||
process.stdout.write("> "); | ||
} | ||
}); | ||
let wss = new ws.Server({ port }); | ||
wss.on("connection", ws => { | ||
ws.on("message", msg => { | ||
functionPathMap.delete(parseInt(msg, 10)); | ||
process.stdout.write("Waiting for 'incinerate!'\n> "); | ||
}); | ||
}); | ||
stdin.end(); | ||
} | ||
jsFiles.forEach(({ file, ast }) => { | ||
removeTaggings(ast); | ||
function removeUnusedVars(ast) { | ||
// referencePaths are cached, so create new ast | ||
ast = parser.parse(generate(ast).code); | ||
// add new taggings | ||
let topLevelDecls = []; | ||
traverse(ast, { | ||
Function(path) { | ||
VariableDeclarator(path) { | ||
if ( | ||
path.node.id && | ||
path.node.id.name.startsWith(incineratorFunctionPrefix) | ||
t.isIdentifier(path.get("id")) && | ||
t.isProgram(path.parentPath.parentPath) | ||
) { | ||
// skip | ||
} else { | ||
let id = functionId++; | ||
functionPathMap.set(id, path); | ||
let body = path.get("body"); | ||
if (t.isBlock(body)) { | ||
body.unshiftContainer("body", tagging(id)); | ||
} | ||
topLevelDecls.push(path); | ||
} | ||
@@ -128,21 +113,87 @@ } | ||
writeAST(file, ast); | ||
}); | ||
let pathsToRemove = []; | ||
topLevelDecls.forEach(path => { | ||
let name = path.node.id.name; | ||
let scope = path.findParent(t.isProgram).scope; | ||
if (scope.bindings[name].referencePaths.length === 0) { | ||
pathsToRemove.push(path); | ||
} | ||
}); | ||
let stdin = process.openStdin(); | ||
stdin.addListener("data", data => { | ||
let str = data.toString(); | ||
if (str.includes("!")) { | ||
incinerate(); | ||
// do until there's no unused vars | ||
if (pathsToRemove.length) { | ||
pathsToRemove.forEach(path => path.remove()); | ||
return removeUnusedVars(ast); | ||
} else { | ||
return ast; | ||
} | ||
}); | ||
} | ||
console.log("Waiting for 'incinerate!'..."); | ||
async function main() { | ||
let jsFiles = []; | ||
await iterateSourceFiles(rootDir, async file => { | ||
let source = await readFile(file, "utf-8"); | ||
let ast = null; | ||
try { | ||
ast = parser.parse(source); | ||
} catch (err) { | ||
if (err.name !== "SyntaxError") throw err; | ||
} | ||
function incinerate() { | ||
if (ast) { | ||
jsFiles.push({ file, ast }); | ||
} | ||
}); | ||
let functionId = 0; | ||
let functionPathMap = new Map(); | ||
let wss = new ws.Server({ port }); | ||
wss.on("connection", ws => { | ||
ws.on("message", msg => { | ||
functionPathMap.delete(parseInt(msg, 10)); | ||
}); | ||
}); | ||
await Promise.all( | ||
jsFiles.map(async ({ file, ast }) => { | ||
removeTaggings(ast); | ||
// add new taggings | ||
traverse(ast, { | ||
Function(path) { | ||
if ( | ||
path.node.id && | ||
path.node.id.name.startsWith(incineratorFunctionPrefix) | ||
) { | ||
// skip | ||
} else { | ||
let id = functionId++; | ||
functionPathMap.set(id, path); | ||
let body = path.get("body"); | ||
if (t.isBlock(body)) { | ||
body.unshiftContainer("body", tagging(id)); | ||
} | ||
} | ||
} | ||
}); | ||
await writeAST(file, ast); | ||
}) | ||
); | ||
await confirmIncineration(); | ||
let text = chalkAnimation.rainbow("\nIncinerating!"); | ||
// left paths are unused, let's incinerate them! | ||
for (let path of functionPathMap.values()) { | ||
if (t.isFunctionDeclaration(path)) { | ||
path.remove(); | ||
// If it's a declaration, replace with empty declaration | ||
path.replaceWith( | ||
t.variableDeclaration("var", [t.variableDeclarator(path.node.id)]) | ||
); | ||
} else { | ||
// for the others, just empty its params and body | ||
path.node.params = []; | ||
@@ -153,9 +204,15 @@ path.node.body = t.blockStatement([]); | ||
jsFiles.forEach(({ file, ast }) => { | ||
removeTaggings(ast); | ||
writeAST(file, ast); | ||
}); | ||
await Promise.all( | ||
jsFiles.map(async ({ file, ast }) => { | ||
removeTaggings(ast); | ||
await writeAST(file, removeUnusedVars(ast)); | ||
}) | ||
); | ||
wss.close(); | ||
stdin.end(); | ||
// show text one more sec because it's rainbow | ||
setTimeout(() => text.stop(), 1000); | ||
} | ||
main(); |
{ | ||
"name": "incinerator", | ||
"version": "0.1.1", | ||
"version": "0.2.0", | ||
"description": "Incinerate unused code!", | ||
@@ -28,2 +28,4 @@ "homepage": "https://github.com/utatti/incinerator", | ||
"@babel/types": "^7.0.0-beta.51", | ||
"chalk-animation": "^1.6.0", | ||
"fs-extra": "^6.0.1", | ||
"ws": "^5.2.1" | ||
@@ -30,0 +32,0 @@ }, |
@@ -11,3 +11,3 @@ # Incinerator | ||
means Incinerator checks if a part of code is actually used in *runtime* and | ||
remove unless it is. For the time being, only function blocks are checked. | ||
removes it if unused. For the time being, only function blocks are checked. | ||
@@ -34,12 +34,12 @@ ## Install | ||
[](https://youtu.be/tj1S0QQOuAM) | ||
[](https://youtu.be/OjHFX_utqBM) | ||
## How Incinerator works | ||
I recommend reading [the code](index.js), as it's only about 150 line long. | ||
I recommend reading [the code](index.js), as it's only ~200 lines long. | ||
1. Tag every function with a unique ID | ||
1. Check if a function is called in runtime via WebSocket | ||
1. When finished, remove uncalled functions | ||
1. Remove unused variables or undefined symbols manually | ||
1. When finished, empty uncalled functions | ||
1. Remove unused top-level variables to remove unused dependencies | ||
@@ -46,0 +46,0 @@ Please also consider playing with an [example](example). |
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
0
370526
7
61
10990
+ Addedchalk-animation@^1.6.0
+ Addedfs-extra@^6.0.1
+ Added@types/tinycolor2@1.4.6(transitive)
+ Addedansi-styles@3.2.1(transitive)
+ Addedarray-find-index@1.0.2(transitive)
+ Addedarrify@1.0.1(transitive)
+ Addedcamelcase@4.1.0(transitive)
+ Addedcamelcase-keys@4.2.0(transitive)
+ Addedchalk@2.4.2(transitive)
+ Addedchalk-animation@1.6.0(transitive)
+ Addedcolor-convert@1.9.3(transitive)
+ Addedcolor-name@1.1.3(transitive)
+ Addedcurrently-unhandled@0.4.1(transitive)
+ Addeddecamelize@1.2.0(transitive)
+ Addeddecamelize-keys@1.1.1(transitive)
+ Addederror-ex@1.3.2(transitive)
+ Addedescape-string-regexp@1.0.5(transitive)
+ Addedfind-up@2.1.0(transitive)
+ Addedfs-extra@6.0.1(transitive)
+ Addedfunction-bind@1.1.2(transitive)
+ Addedgraceful-fs@4.2.11(transitive)
+ Addedgradient-string@1.2.0(transitive)
+ Addedhas-flag@3.0.0(transitive)
+ Addedhasown@2.0.2(transitive)
+ Addedhosted-git-info@2.8.9(transitive)
+ Addedindent-string@3.2.0(transitive)
+ Addedis-arrayish@0.2.1(transitive)
+ Addedis-core-module@2.16.1(transitive)
+ Addedis-plain-obj@1.1.0(transitive)
+ Addedjson-parse-better-errors@1.0.2(transitive)
+ Addedjsonfile@4.0.0(transitive)
+ Addedload-json-file@4.0.0(transitive)
+ Addedlocate-path@2.0.0(transitive)
+ Addedloud-rejection@1.6.0(transitive)
+ Addedmap-obj@1.0.12.0.0(transitive)
+ Addedmeow@4.0.1(transitive)
+ Addedminimist@1.2.8(transitive)
+ Addedminimist-options@3.0.2(transitive)
+ Addednormalize-package-data@2.5.0(transitive)
+ Addedp-limit@1.3.0(transitive)
+ Addedp-locate@2.0.0(transitive)
+ Addedp-try@1.0.0(transitive)
+ Addedparse-json@4.0.0(transitive)
+ Addedpath-exists@3.0.0(transitive)
+ Addedpath-parse@1.0.7(transitive)
+ Addedpath-type@3.0.0(transitive)
+ Addedpify@3.0.0(transitive)
+ Addedquick-lru@1.1.0(transitive)
+ Addedread-pkg@3.0.0(transitive)
+ Addedread-pkg-up@3.0.0(transitive)
+ Addedredent@2.0.0(transitive)
+ Addedresolve@1.22.10(transitive)
+ Addedsemver@5.7.2(transitive)
+ Addedsignal-exit@3.0.7(transitive)
+ Addedspdx-correct@3.2.0(transitive)
+ Addedspdx-exceptions@2.5.0(transitive)
+ Addedspdx-expression-parse@3.0.1(transitive)
+ Addedspdx-license-ids@3.0.21(transitive)
+ Addedstrip-bom@3.0.0(transitive)
+ Addedstrip-indent@2.0.0(transitive)
+ Addedsupports-color@5.5.0(transitive)
+ Addedsupports-preserve-symlinks-flag@1.0.0(transitive)
+ Addedtinycolor2@1.6.0(transitive)
+ Addedtinygradient@0.4.3(transitive)
+ Addedtrim-newlines@2.0.0(transitive)
+ Addeduniversalify@0.1.2(transitive)
+ Addedvalidate-npm-package-license@3.0.4(transitive)