Skip to content

Instantly share code, notes, and snippets.

@wolfram77
Created February 25, 2020 13:38
Show Gist options
  • Select an option

  • Save wolfram77/117bcecf9db50c7e9859f50b9ec0c8b7 to your computer and use it in GitHub Desktop.

Select an option

Save wolfram77/117bcecf9db50c7e9859f50b9ec0c8b7 to your computer and use it in GitHub Desktop.
// # browserify
// # uglify
// 1. process .js -> single .js
// 2. process .js + package.json -> package.json
// 3. process .js + README.md -> README.md
// 4. create package and publish
const findNpmPrefix = require('find-npm-prefix');
const recast = require('recast');
const path = require('path');
const fs = require('fs');
// Global variables.
var OPTIONS = {
dependencies: false,
devDependencies: false,
allDependencies: false
};
// Get last value in array.
function last(arr, i=1) {
return arr[arr.length-i];
};
// Remove value from array.
function remove(arr, val) {
var i = arr.indexOf(val);
if(i>=0) arr.splice(i, 1);
return arr;
};
// Remove values at all indexes.
function removeAtAll(arr, idx) {
for(var i=idx.length-1; i>=0; i--)
arr.splice(idx[i], 1);
return arr;
};
// Get key of value in object.
function keyOf(obj, val) {
for(var k in obj)
if(obj[k]===val) return k;
return null;
};
// Add all values to set.
function addAll(set, vals) {
for(var v of vals)
set.add(v);
return set;
};
// Read json file at path.
function jsonRead(pth) {
if(!fs.existsSync(pth)) return null;
return JSON.parse(fs.readFileSync(pth, 'utf8'));
};
// Check if node is function.
function nodeIsFunction(ast) {
return /Function(Declaration|Expression)/.test(ast.type);
};
// Check if node is assignment.
function nodeIsAssignment(ast) {
if(ast.type==='VariableDeclarator') return true;
return ast.type==='AssignmentExpression';
};
// Check if node is require().
function nodeIsRequire(ast) {
if(ast.type!=='CallExpression') return false;
return ast.callee.name==='require';
};
// Check if node is exports.
function nodeIsExports(ast) {
if(ast.type!=='ExpressionStatement') return false;
if(ast.expression.left.type!=='MemberExpression') return false;
return ast.expression.left.object.name==='exports';
};
// Check if node is module.exports.
function nodeIsModuleExports(ast) {
if(ast.type!=='ExpressionStatement') return false;
if(ast.expression.left.type!=='MemberExpression') return false;
if(ast.expression.left.object.name!=='module') return false;
return ast.expression.left.property.name==='exports';
};
// Get assignment name.
function assignmentName(ast) {
if(ast.type==='VariableDeclarator') return ast.id.name;
else if(ast.type==='AssignmentExpression') return ast.left.name;
return null;
};
// Remove an assignment from hierarchy.
function assignmentRemove(asth) {
var ast1 = last(asth), ast2 = last(asth, 2), ast3 = last(asth, 3);
if(ast1.type==='AssignmentExpression') remove(ast3, ast2);
else if(ast1.type==='VariableDeclarator') {
if(ast2.length>1) remove(ast2, ast1);
else remove(last(asth, 4), ast3);
}
return asth;
};
// Get function parameter name.
function paramName(ast) {
if(ast.type==='Identifier') return ast.name;
else if(ast.type==='RestElement') return ast.argument.name;
else if(ast.type==='AssignmentPattern') return ast.left.name;
return null;
};
// Get function parameter names.
function functionParams(ast, set=new Set()) {
for(var p of ast.params)
set.add(paramName(p));
return set;
};
// Get variable declaration names.
function variableDeclarationNames(ast, set=new Set()) {
for(var d of ast.declarations)
set.add(d.id.name);
return set;
};
// Get declaration names.
function declarationNames(ast, set=new Set()) {
if(ast.type==='FunctionDeclaration') set.add(ast.id.name);
else if(ast.type==='VariableDeclaration') variableDeclarationNames(ast, set);
return set;
};
// Get global identifier names.
function bodyGlobals(ast, set=new Set()) {
for(var s of ast)
declarationNames(s, set);
return set;
};
// Get empty window identifier map.
function bodyEmptyWindow(ast, win=new Map()) {
for(var nam of bodyGlobals(ast))
win.set(nam, []);
return win;
};
// Get (scanned) window identifier map.
function bodyWindow(ast, win=bodyEmptyWindow(ast), exc=new Set()) {
if(ast==null || typeof ast!=='object') return win;
if(nodeIsFunction(ast)) {
bodyWindow(ast.id, win, exc);
var excn = functionParams(ast, new Set(exc));
return bodyWindow(ast.body, win, excn);
}
if(ast.type==='Identifier') {
if(!win.has(ast.name) || exc.has(ast.name)) return win;
win.get(ast.name).push(ast); return win;
}
for(var k in ast)
bodyWindow(ast[k], win, exc);
return win;
};
// Get actual name of window identifier.
function windowName(win, nam) {
if(!win.has(nam)) return nam;
return win.get(nam)[0].name;
};
// Rename a window identifier.
function windowRename(win, nam, to) {
for(var ast of win.get(nam))
ast.name = to;
return win;
};
// Add window identifier to globals.
function globalsAdd(glo, win, nam, suf) {
if(!glo.has(nam)) return glo.add(nam);
if(!glo.has(nam+suf)) {
windowRename(win, nam, nam+suf);
return glo.add(nam+suf);
}
for(var i=0; glo.has(nam+suf+i); i++) {}
windowRename(win, nam, nam+suf+i);
return glo.add(nam+suf+i);
};
// Add all window identifiers to globals.
function globalsAddAll(glo, win, suf) {
for(var nam of win.keys())
globalsAdd(glo, win, nam, suf);
return glo;
};
// Update exports to given name.
function bodyUpdateExports(ast, nam) {
for(var i=ast.length-1, idx=-1; i>=0; i--) {
if(!nodeIsExports(ast[i])) continue;
ast[idx=i].expression.left.object.name = nam;
}
if(idx<0) return null;
var astn = recast.parse(`\nconst ${nam} = {};`);
ast.splice(idx, 0, astn.program.body[0]);
return nam;
};
// Update module.exports to given name, if possible.
function bodyUpdateModuleExports(ast, nam) {
for(var i=ast.length-1, idx=-1, rgt=null; i>=0; i--) {
if(!nodeIsModuleExports(ast[i])) continue;
rgt = ast[idx=i].expression.right;
ast.splice(i, 1);
}
if(idx<0) return null;
if(rgt.type==='Identifier') return rgt.name;
var astn = recast.parse(`\nconst ${nam} = 0;`);
astn.program.body[0].declarations[0].init = rgt;
ast.splice(idx, 0, astn.program.body[0]);
return nam;
};
// Update require() using module load function.
// : support __dirname, __filename?
function bodyUpdateRequire(ast, astp, fn) {
if(ast==null || typeof ast!=='object') return ast;
if(!nodeIsRequire(ast)) {
astp.push(ast);
for(var astv of Object.values(ast))
bodyUpdateRequire(astv, astp, fn);
return astp.pop();
}
var nam = fn(ast.arguments[0].value), ast1 = last(astp);
if(nam==null) return ast;
if(nodeIsAssignment(ast1) && assignmentName(ast1)===nam) {
assignmentRemove(astp); return ast;
}
var astr = recast.parse(`const a = ${nam};`);
ast1[keyOf(ast1, ast)] = astr.program.body[0].declarations[0].init;
return ast;
};
// Bundle script with options
function bundleScript(pth, sym, exc=new Set(), top=false) {
var code = fs.readFileSync(pth, 'utf8'), paths = [path.dirname(pth)];
var ast = recast.parse(code), body = ast.program.body;
bodyUpdateRequire(body, [], val => {
if(exc.has(val)) return null;
var pth = require.resolve(val, {paths});
if(sym.exports.has(pth)) return sym.exports.get(pth).name;
return bundleScript(pth, sym, exc);
});
var suf = !top? sym.exports.size.toString():'', nam = 'exports'+suf;
if(!top) nam = bodyUpdateExports(body, nam)||bodyUpdateModuleExports(body, nam);
var win = bodyWindow(body); globalsAddAll(sym.globals, win, suf);
sym.exports.set(pth, {name: nam, suffix: suf, code: recast.print(ast).code});
return windowName(win, nam);
};
// Get exclude set for bundle.
function bundleExclude(pth, opt) {
var exc = new Set();
var pkg = jsonRead(path.join(pth, 'package.json'))||{};
var pkgl = jsonRead(path.join(pth, 'package-lock.json'))||{};
if(opt.dependencies && pkg.dependencies) addAll(exc, Object.keys(pkg.dependencies));
if(opt.devDependencies && pkg.devDependencies) addAll(exc, Object.keys(pkg.devDependencies));
if(opt.allDependencies && pkgl.dependencies) addAll(exc, Object.keys(pkgl.dependencies));
return exc;
};
async function bundle(fil, o) {
var o = Object.assign({}, OPTIONS, o), z = '';
var cwd = process.cwd();
var pth = path.join(cwd, fil);
var pfx = await findNpmPrefix(cwd);
var sym = {exports: new Map(), globals: new Set()};
var exc = bundleExclude(pfx, o);
bundleScript(pth, sym, exc, true);
for(var exp of sym.exports.values())
z += exp.code;
return z;
};
module.exports = bundle;
// Run on shell.
async function main() {
var data = await bundle('scripts/index.js', {dependencies: true});
fs.writeFileSync('index.js', data);
};
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment