|
| 1 | +"use strict" |
| 2 | + |
| 3 | +const fs = require("fs") |
| 4 | +const path = require("path") |
| 5 | +const execFileSync = require("child_process").execFileSync |
| 6 | +const util = require("util") |
| 7 | + |
| 8 | +const readFile = util.promisify(fs.readFile) |
| 9 | +const access = util.promisify(fs.access) |
| 10 | + |
| 11 | +function isFile(filepath) { |
| 12 | + return access(filepath).then(() => true, () => false) |
| 13 | +} |
| 14 | +function escapeRegExp(string) { |
| 15 | + return string.replace(/[|\\{}()[\]^$+*?.-]/g, "\\$&") |
| 16 | +} |
| 17 | +function escapeReplace(string) { |
| 18 | + return string.replace(/\$/g, "\\$&") |
| 19 | +} |
| 20 | + |
| 21 | +async function resolve(filepath, filename) { |
| 22 | + if (filename[0] !== ".") { |
| 23 | + // resolve as npm dependency |
| 24 | + const packagePath = `./node_modules/${filename}/package.json` |
| 25 | + let json, meta |
| 26 | + |
| 27 | + try { |
| 28 | + json = await readFile(packagePath, "utf8") |
| 29 | + } catch (e) { |
| 30 | + meta = {} |
| 31 | + } |
| 32 | + |
| 33 | + if (json) { |
| 34 | + try { |
| 35 | + meta = JSON.parse(json) |
| 36 | + } |
| 37 | + catch (e) { |
| 38 | + throw new Error(`invalid JSON for ${packagePath}: ${json}`) |
| 39 | + } |
| 40 | + } |
| 41 | + |
| 42 | + const main = `./node_modules/${filename}/${meta.main || `${filename}.js`}` |
| 43 | + return path.resolve(await isFile(main) ? main : `./node_modules/${filename}/index.js`) |
| 44 | + } |
| 45 | + else { |
| 46 | + // resolve as local dependency |
| 47 | + return path.resolve(path.dirname(filepath), filename + ".js") |
| 48 | + } |
| 49 | +} |
| 50 | + |
| 51 | +function matchAll(str, regexp) { |
| 52 | + regexp.lastIndex = 0 |
| 53 | + const result = [] |
| 54 | + let exec |
| 55 | + while ((exec = regexp.exec(str)) != null) result.push(exec) |
| 56 | + return result |
| 57 | +} |
| 58 | + |
| 59 | +let error |
| 60 | +module.exports = async (input) => { |
| 61 | + const modules = new Map() |
| 62 | + const bindings = new Map() |
| 63 | + const declaration = /^\s*(?:var|let|const|function)[\t ]+([\w_$]+)/gm |
| 64 | + const include = /(?:((?:var|let|const|,|)[\t ]*)([\w_$\.\[\]"'`]+)(\s*=\s*))?require\(([^\)]+)\)(\s*[`\.\(\[])?/gm |
| 65 | + let uuid = 0 |
| 66 | + async function process(filepath, data) { |
| 67 | + for (const [, binding] of matchAll(data, declaration)) bindings.set(binding, 0) |
| 68 | + |
| 69 | + const tasks = [] |
| 70 | + |
| 71 | + for (const [, def = "", variable = "", eq = "", dep, rest = ""] of matchAll(data, include)) { |
| 72 | + tasks.push({filename: JSON.parse(dep), def, variable, eq, rest}) |
| 73 | + } |
| 74 | + |
| 75 | + const imports = await Promise.all( |
| 76 | + tasks.map((t) => resolve(filepath, t.filename)) |
| 77 | + ) |
| 78 | + |
| 79 | + const results = [] |
| 80 | + for (const [i, task] of tasks.entries()) { |
| 81 | + const dependency = imports[i] |
| 82 | + let pre = "", def = task.def |
| 83 | + if (def[0] === ",") def = "\nvar ", pre = "\n" |
| 84 | + const localUUID = uuid // global uuid can update from nested `process` call, ensure same id is used on declaration and consumption |
| 85 | + const existingModule = modules.get(dependency) |
| 86 | + modules.set(dependency, task.rest ? `_${localUUID}` : task.variable) |
| 87 | + const code = await process( |
| 88 | + dependency, |
| 89 | + pre + ( |
| 90 | + existingModule == null |
| 91 | + ? await exportCode(task.filename, dependency, def, task.variable, task.eq, task.rest, localUUID) |
| 92 | + : def + task.variable + task.eq + existingModule |
| 93 | + ) |
| 94 | + ) |
| 95 | + uuid++ |
| 96 | + results.push(code + task.rest) |
| 97 | + } |
| 98 | + |
| 99 | + let i = 0 |
| 100 | + return data.replace(include, () => results[i++]) |
| 101 | + } |
| 102 | + |
| 103 | + async function exportCode(filename, filepath, def, variable, eq, rest, uuid) { |
| 104 | + let code = await readFile(filepath, "utf-8") |
| 105 | + // if there's a syntax error, report w/ proper stack trace |
| 106 | + try { |
| 107 | + new Function(code) |
| 108 | + } |
| 109 | + catch (e) { |
| 110 | + try { |
| 111 | + execFileSync("node", ["--check", filepath], { |
| 112 | + stdio: "pipe", |
| 113 | + }) |
| 114 | + } |
| 115 | + catch (e) { |
| 116 | + if (e.message !== error) { |
| 117 | + error = e.message |
| 118 | + console.log(`\x1b[31m${e.message}\x1b[0m`) |
| 119 | + } |
| 120 | + } |
| 121 | + } |
| 122 | + |
| 123 | + // disambiguate collisions |
| 124 | + const targetPromises = [] |
| 125 | + code.replace(include, (match, def, variable, eq, dep) => { |
| 126 | + targetPromises.push(resolve(filepath, JSON.parse(dep))) |
| 127 | + }) |
| 128 | + |
| 129 | + const ignoredTargets = await Promise.all(targetPromises) |
| 130 | + const ignored = new Set() |
| 131 | + |
| 132 | + for (const target of ignoredTargets) { |
| 133 | + const binding = modules.get(target) |
| 134 | + if (binding != null) ignored.add(binding) |
| 135 | + } |
| 136 | + |
| 137 | + if (new RegExp(`module\\.exports\\s*=\\s*${variable}\s*$`, "m").test(code)) ignored.add(variable) |
| 138 | + for (const [binding, count] of bindings) { |
| 139 | + if (!ignored.has(binding)) { |
| 140 | + const before = code |
| 141 | + code = code.replace( |
| 142 | + new RegExp(`(\\b)${escapeRegExp(binding)}\\b`, "g"), |
| 143 | + escapeReplace(binding) + count |
| 144 | + ) |
| 145 | + if (before !== code) bindings.set(binding, count + 1) |
| 146 | + } |
| 147 | + } |
| 148 | + |
| 149 | + // fix strings that got mangled by collision disambiguation |
| 150 | + const string = /(["'])((?:\\\1|.)*?)(\1)/g |
| 151 | + const candidates = Array.from(bindings, ([binding, count]) => escapeRegExp(binding) + (count - 1)).join("|") |
| 152 | + const variables = new RegExp(candidates, "g") |
| 153 | + code = code.replace(string, (match, open, data, close) => { |
| 154 | + const fixed = data.replace(variables, (match) => match.replace(/\d+$/, "")) |
| 155 | + return open + fixed + close |
| 156 | + }) |
| 157 | + |
| 158 | + //fix props |
| 159 | + const props = new RegExp(`((?:[^:]\\/\\/.*)?\\.\\s*)(${candidates})|([\\{,]\\s*)(${candidates})(\\s*:)`, "gm") |
| 160 | + code = code.replace(props, (match, dot, a, pre, b, post) => { |
| 161 | + // Don't do anything because dot was matched in a comment |
| 162 | + if (dot && dot.indexOf("//") === 1) return match |
| 163 | + if (dot) return dot + a.replace(/\d+$/, "") |
| 164 | + return pre + b.replace(/\d+$/, "") + post |
| 165 | + }) |
| 166 | + |
| 167 | + return code |
| 168 | + .replace(/("|')use strict\1;?/gm, "") // remove extraneous "use strict" |
| 169 | + .replace(/module\.exports\s*=\s*/gm, escapeReplace(rest ? `var _${uuid}` + eq : def + (rest ? "_" : "") + variable + eq)) // export |
| 170 | + + (rest ? `\n${def}${variable}${eq}_${uuid}` : "") // if `rest` is truthy, it means the expression is fluent or higher-order (e.g. require(path).foo or require(path)(foo) |
| 171 | + } |
| 172 | + |
| 173 | + const code = ";(function() {\n" + |
| 174 | + (await process(path.resolve(input), await readFile(input, "utf-8"))) |
| 175 | + .replace(/^\s*((?:var|let|const|)[\t ]*)([\w_$\.]+)(\s*=\s*)(\2)(?=[\s]+(\w)|;|$)/gm, "") // remove assignments to self |
| 176 | + .replace(/;+(\r|\n|$)/g, ";$1") // remove redundant semicolons |
| 177 | + .replace(/(\r|\n)+/g, "\n").replace(/(\r|\n)$/, "") + // remove multiline breaks |
| 178 | + "\n}());" |
| 179 | + |
| 180 | + //try {new Function(code); console.log(`build completed at ${new Date()}`)} catch (e) {} |
| 181 | + error = null |
| 182 | + return code |
| 183 | +} |
0 commit comments