UNPKG

6.74 kBJavaScriptView Raw
1import path from 'node:path';
2import process from 'node:process';
3import fs from 'node:fs';
4import { fileURLToPath } from 'node:url';
5import { createInterface } from 'node:readline';
6import spawn from 'cross-spawn';
7import fuzzysort from 'fuzzysort';
8import chalk from 'chalk-template';
9
10const __dirname = path.dirname(fileURLToPath(import.meta.url));
11const appName = path.basename(process.argv[1]);
12const help = chalk`{bold Usage:} ${appName} {green <fuzzy_script_name>}|{cyan <action>} [script_options]
13{bold Actions:}
14 {cyan -u, --update} Show outdated packages and run an interactive update
15 {cyan -r, --refresh} Delete node_modules and lockfile, and reinstall packages
16`;
17const npmLockFile = 'package-lock.json';
18const yarnLockFile = 'yarn.lock';
19const pnpmLockFile = 'pnpm-lock.yaml';
20const chalkTemplate = (string_) => chalk(Object.assign([], { raw: [string_] }));
21
22export async function fuzzyRun(args, packageManager = undefined) {
23 try {
24 const packageFile = findFileUp(process.cwd(), 'package.json');
25 if (!packageFile) {
26 throw new Error(chalk`Error, {yellow package.json} not found\n`);
27 }
28
29 const packageDir = path.dirname(packageFile);
30 const scripts = getScripts(packageFile);
31
32 if (args.length === 0 || args[0] === '--help') {
33 console.log(help);
34 showScripts(scripts);
35 }
36
37 packageManager = packageManager || getPackageManager(packageDir);
38 const name = args[0];
39
40 if (name === '--version') {
41 const pkg = fs.readFileSync(path.join(__dirname, 'package.json'));
42 const pkgJson = JSON.parse(pkg);
43 return console.log(pkgJson.version);
44 }
45
46 if (name === '-u' || name === '--update') {
47 return updatePackages(packageManager);
48 }
49
50 if (name === '-r' || name === '--refresh') {
51 return refreshPackages(packageManager, packageDir);
52 }
53
54 let scriptName = name;
55
56 if (!scripts[name]) {
57 const match = matchScript(name, Object.keys(scripts));
58 if (!match) {
59 console.error(chalk`No script match for {yellow ${name}}\n`);
60 showScripts(scripts);
61 }
62
63 const highlightedName = fuzzysort.highlight(match, '{underline ', '}');
64 console.log(chalkTemplate(`Running {green ${highlightedName}}`));
65 scriptName = match.target;
66 }
67
68 spawn.sync(
69 packageManager,
70 [
71 'run',
72 scriptName,
73 ...(packageManager === 'npm' ? ['--'] : []),
74 ...args.slice(1)
75 ],
76 { stdio: 'inherit' }
77 );
78 } catch (error) {
79 if (error.message) {
80 console.error(error.message);
81 }
82
83 process.exitCode = -1;
84 }
85}
86
87function findFileUp(basePath, file) {
88 const find = (components) => {
89 if (components.length === 0) {
90 return undefined;
91 }
92
93 const dir = path.join(...components);
94 const packageFile = path.join(dir, file);
95 return fs.existsSync(packageFile)
96 ? packageFile
97 : find(components.slice(0, -1));
98 };
99
100 const components = basePath.split(/[/\\]/);
101 if (components.length > 0 && components[0].length === 0) {
102 // When path starts with a slash, the first path component is empty string
103 components[0] = path.sep;
104 }
105
106 return find(components);
107}
108
109function getScripts(packageFile) {
110 const projectPackageFile = fs.readFileSync(packageFile);
111 const projectPackage = JSON.parse(projectPackageFile);
112 return projectPackage.scripts || [];
113}
114
115function getPackageManager(packageDir) {
116 let packageManager = process.env.NODE_PACKAGE_MANAGER;
117 if (packageManager && packageManager !== 'npm' && packageManager !== 'yarn') {
118 throw new Error(
119 chalk`{yellow Unsupported package manager: ${packageManager}}\n`
120 );
121 }
122
123 if (!packageManager) {
124 const hasNpmLock = findFileUp(packageDir, npmLockFile) !== undefined;
125 const hasYarnLock = findFileUp(packageDir, yarnLockFile) !== undefined;
126 const hasPnpmLock = findFileUp(packageDir, pnpmLockFile) !== undefined;
127
128 if (hasPnpmLock && !hasNpmLock && !hasYarnLock) {
129 packageManager = 'pnpm';
130 } else if (hasYarnLock && !hasNpmLock) {
131 packageManager = 'yarn';
132 } else {
133 packageManager = 'npm';
134 }
135 }
136
137 return packageManager;
138}
139
140function matchScript(string_, scriptNames) {
141 const match = fuzzysort.go(string_, scriptNames, { limit: 1 })[0];
142 return match || undefined;
143}
144
145function showScripts(scripts) {
146 scripts = Object.keys(scripts);
147 if (scripts.length === 0) {
148 throw new Error(
149 chalk`{yellow No scripts found in your} package.json {yellow file}\n`
150 );
151 }
152
153 const scriptNames = scripts.map((script) => chalk`{green ${script}}`);
154 throw new Error(
155 chalk`{bold Available NPM Scripts:}\n- ${scriptNames.join('\n- ')}\n`
156 );
157}
158
159async function askForInput(question) {
160 return new Promise((resolve, _reject) => {
161 const read = createInterface({
162 input: process.stdin,
163 output: process.stdout
164 });
165 read.question(question, (answer) => {
166 read.close();
167 resolve(answer);
168 });
169 });
170}
171
172async function updatePackages(packageManager) {
173 const { status } = spawn.sync(packageManager, ['outdated'], {
174 stdio: 'inherit'
175 });
176 if (status === 0) {
177 console.log(`Nothing to update.\n`);
178 return;
179 }
180
181 const answer = await askForInput(`\nDo you want to update now? [Y/n] `);
182 if (answer !== '' && answer.toLowerCase() !== 'y') {
183 return;
184 }
185
186 spawn.sync(
187 packageManager,
188 [packageManager === 'yarn' ? 'upgrade' : 'update'],
189 { stdio: 'inherit' }
190 );
191
192 if (packageManager === 'yarn') {
193 spawn.sync('yarn', ['upgrade-interactive', '--latest'], {
194 stdio: 'inherit'
195 });
196 } else {
197 if (packageManager === 'pnpm') {
198 process.env.NPM_CHECK_INSTALLER = 'pnpm';
199 }
200
201 spawn.sync('npx', ['-y', 'npm-check', '-u'], { stdio: 'inherit' });
202 }
203}
204
205function refreshPackages(packageManager, packageDir) {
206 const nodeModulesDir = path.join(packageDir, 'node_modules');
207 console.log(chalk`Removing {green node_modules}...`);
208
209 if (fs.existsSync(nodeModulesDir)) {
210 if (fs.rmSync) {
211 fs.rmSync(nodeModulesDir, { recursive: true });
212 } else {
213 // Compatibility Node.js < 14
214 fs.rmdirSync(nodeModulesDir, { recursive: true });
215 }
216 }
217
218 let lockFile = npmLockFile;
219 if (packageManager === 'yarn') {
220 lockFile = yarnLockFile;
221 } else if (packageManager === 'pnpm') {
222 lockFile = pnpmLockFile;
223 }
224
225 console.log(chalk`Removing {green ${lockFile}}...`);
226 lockFile = path.join(packageDir, lockFile);
227
228 if (fs.existsSync(lockFile)) {
229 if (fs.rmSync) {
230 fs.rmSync(lockFile);
231 } else {
232 // Compatibility Node.js < 14
233 fs.unlinkSync(lockFile);
234 }
235 }
236
237 console.log(chalk`Running {green ${packageManager} install}...`);
238 spawn.sync(packageManager, ['install'], { stdio: 'inherit' });
239}