Skip to content

Instantly share code, notes, and snippets.

@Explosion-Scratch
Last active July 22, 2023 13:47
Show Gist options
  • Save Explosion-Scratch/117a4e6458d0aee97b1a78816988e259 to your computer and use it in GitHub Desktop.
Save Explosion-Scratch/117a4e6458d0aee97b1a78816988e259 to your computer and use it in GitHub Desktop.
Auto install node modules from a file
#!/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