Last active
July 22, 2023 13:47
-
-
Save Explosion-Scratch/117a4e6458d0aee97b1a78816988e259 to your computer and use it in GitHub Desktop.
Auto install node modules from a file
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
// Usage: | |
// autoinstall file.js --manager pnpm | |
// autoinstall file1.js file2.js | |
// Build to binary: esbuild index.js --bundle --outfile=build.cjs --format=cjs --platform=node && pkg -t node16-linux,node16-win,node16-macos-x64,node16-macos-arm64 build.cjs -C GZip -o autoinstall && rm -rf build.cjs | |
import fs from "fs/promises"; | |
import path from "path"; | |
import { exec } from "child_process"; | |
const coreModules = [ | |
'assert', | |
'async_hooks', | |
'buffer', | |
'child_process', | |
'cluster', | |
'console', | |
'constants', | |
'crypto', | |
'dgram', | |
'diagnostics_channel', | |
'dns', | |
'domain', | |
'events', | |
'fs', | |
'http', | |
'http2', | |
'https', | |
'inspector', | |
'module', | |
'net', | |
'os', | |
'path', | |
'perf_hooks', | |
'process', | |
'punycode', | |
'querystring', | |
'readline', | |
'repl', | |
'stream', | |
'string_decoder', | |
'sys', | |
'timers', | |
'tls', | |
'trace_events', | |
'tty', | |
'url', | |
'util', | |
'v8', | |
'vm', | |
'wasi', | |
'worker_threads', | |
'zlib' | |
]; | |
/** | |
* Installs dependencies for a file by parsing requires and imports. | |
* @param {string} filePath Path to file | |
*/ | |
async function installDeps(filePath, manager) { | |
if (!filePath) { | |
throw new Error('File path required'); | |
} | |
const pkgPath = await getPackageJson(filePath); | |
let existing = []; | |
if (!pkgPath) { | |
console.log(`Running "${manager} init"`); | |
await shell(`${JSON.stringify(manager)} init`); | |
} else { | |
existing = JSON.parse(await fs.readFile(pkgPath, 'utf8')); | |
existing = Object.keys({ ...existing.dependencies, ...existing.devDependencies }) | |
} | |
let deps = await getDeps(filePath); | |
deps = deps.filter(i => ![...existing, ...coreModules].map(getModuleName).includes(getModuleName(i))); | |
for (let dep of deps.filter(i => isRelative(i))) { | |
await installDeps(getAbsolutePath(filePath, dep), manager); | |
} | |
await npmInstall(deps.filter(i => !isRelative(i)), manager); | |
} | |
async function getPackageJson(currentPath) { | |
currentPath = path.resolve(currentPath); | |
let rootFolder = path.parse(currentPath).root; | |
let packageJsonPath; | |
do { | |
const testPath = path.join(currentPath, 'package.json'); | |
if (await fileExists(testPath)) { | |
packageJsonPath = testPath; | |
break; | |
} | |
currentPath = path.dirname(currentPath); | |
} while (currentPath !== rootFolder); | |
return packageJsonPath; | |
} | |
/** | |
* Gets unique list of dependencies for a file | |
* @param {string} filePath | |
* @returns {Promise<string[]>} List of dependencies | |
*/ | |
async function getDeps(filePath) { | |
const fileContent = await fs.readFile(filePath, 'utf8'); | |
const requires = getRequires(fileContent); | |
const imports = getImports(fileContent); | |
return [...new Set([...requires, ...imports])]; | |
} | |
/** | |
* Gets require() dependencies from file content. | |
* @param {string} fileContent | |
* @returns {string[]} List of required dependencies | |
*/ | |
function getRequires(fileContent) { | |
return fileContent.match(/require\(['"][^'"]+?['"]\)/g)?.map(req => { | |
const match = req.match(/['"]([^'"]+)['"]/); | |
return match ? match[1] : null; | |
}) || []; | |
} | |
/** | |
* Gets import dependencies from file content. | |
* @param {string} fileContent | |
* @returns {string[]} List of imported dependencies | |
*/ | |
/** | |
* Gets import dependencies from file content. | |
* @param {string} fileContent | |
* @returns {string[]} List of imported dependencies | |
*/ | |
function getImports(fileContent) { | |
const defaultImports = fileContent.match(/import [^'" -]+ from ['"]([^'"]+?)['"]/g); | |
const totalImports = fileContent.match(/import ['"]([^'"]+?)['"]/g); | |
const namedImports = fileContent.match(/import \{[^}]+\} from ['"][^'"]+?['"]/g); | |
const namespaceImports = fileContent.match(/import \* as [^ ]+ from ['"][^'"]+?['"]/g); | |
const modules = [ | |
...(totalImports || [])?.map(imp => imp.match(/import ['"]([^'"]+?)['"]/)[1]), | |
...(defaultImports || [])?.map(imp => imp.match(/from ['"]([^'"]+?)['"]/)[1]), | |
...(namedImports || [])?.map(imp => imp.match(/from ['"]([^'"]+)['"]/)[1]), | |
...(namespaceImports || [])?.map(imp => imp.match(/from ['"]([^'"]+)['"]/)[1]) | |
]; | |
return modules; | |
} | |
/** | |
* Checks if a dependency path is relative. | |
* @param {string} dep | |
* @returns {boolean} True if relative path | |
*/ | |
function isRelative(dep) { | |
return dep.startsWith('.') || dep.startsWith('/'); | |
} | |
/** | |
* Gets absolute path from base and relative path. | |
* @param {string} basePath | |
* @param {string} relativePath | |
* @returns {string} Absolute path | |
*/ | |
function getAbsolutePath(basePath, relativePath) { | |
return path.resolve(path.dirname(basePath), relativePath); | |
} | |
/** | |
* Installs a module via npm | |
* @param {string} moduleName | |
* @returns {Promise} | |
*/ | |
async function npmInstall(modules, manager = "npm") { | |
if (!modules?.length) { | |
return; | |
} | |
modules = modules.map(getModuleName) | |
console.log(`Installing ${modules.length} modules from ${manager}:\n${modules.map(i => `\t- ${i}`).join("\n")}`); | |
const result = await shell(`${JSON.stringify(manager)} install ${modules.map((i) => JSON.stringify(i)).join(" ")} --save-prod`); | |
console.log(result.toString()); | |
} | |
function getModuleName(i) { | |
i = i.trim(); | |
if (!i.startsWith("@")) { | |
i = i.split("/")[0]; | |
} | |
return i; | |
} | |
async function fileExists(filePath) { | |
try { | |
await fs.access(filePath); | |
return true; | |
} catch (err) { | |
return false; | |
} | |
} | |
async function run() { | |
let files = process.argv.slice(2).filter((i, idx, arr) => i !== '--manager' && arr[idx - 1] !== '--manager'); | |
const manager = process.argv.find((i, idx, arr) => arr[idx - 1] === '--manager') || 'npm'; | |
console.log("Manager:", manager); | |
if (!["bun", "yarn", "pnpm", "npm"].includes(manager)) { | |
console.log(`Invalid manager: ${manager}`); | |
process.exit(1); | |
} | |
for (let file of files) { | |
try { | |
console.log(`Processing ${file}`); | |
await installDeps(file, manager); | |
console.log(`Done processing ${file}`); | |
} catch (err) { | |
console.error(`Failed to process ${file}: ${err}`); | |
} | |
} | |
} | |
function shell(command) { | |
return new Promise((resolve, reject) => { | |
exec(command, (error, stdout, stderr) => { | |
if (error) { | |
console.log(error.toString()); | |
reject(error); | |
} else { | |
resolve(stdout); | |
} | |
}); | |
}); | |
} | |
run(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment