1 | import path from 'node:path';
|
2 | import process from 'node:process';
|
3 | import fs from 'node:fs';
|
4 | import { fileURLToPath } from 'node:url';
|
5 | import { createInterface } from 'node:readline';
|
6 | import spawn from 'cross-spawn';
|
7 | import fuzzysort from 'fuzzysort';
|
8 | import chalk from 'chalk-template';
|
9 |
|
10 | const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
11 | const appName = path.basename(process.argv[1]);
|
12 | const 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 | `;
|
17 | const npmLockFile = 'package-lock.json';
|
18 | const yarnLockFile = 'yarn.lock';
|
19 | const pnpmLockFile = 'pnpm-lock.yaml';
|
20 | const chalkTemplate = (string_) => chalk(Object.assign([], { raw: [string_] }));
|
21 |
|
22 | export 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 |
|
87 | function 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 |
|
103 | components[0] = path.sep;
|
104 | }
|
105 |
|
106 | return find(components);
|
107 | }
|
108 |
|
109 | function getScripts(packageFile) {
|
110 | const projectPackageFile = fs.readFileSync(packageFile);
|
111 | const projectPackage = JSON.parse(projectPackageFile);
|
112 | return projectPackage.scripts || [];
|
113 | }
|
114 |
|
115 | function 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 |
|
140 | function matchScript(string_, scriptNames) {
|
141 | const match = fuzzysort.go(string_, scriptNames, { limit: 1 })[0];
|
142 | return match || undefined;
|
143 | }
|
144 |
|
145 | function 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 |
|
159 | async 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 |
|
172 | async 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 |
|
205 | function 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 |
|
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 |
|
233 | fs.unlinkSync(lockFile);
|
234 | }
|
235 | }
|
236 |
|
237 | console.log(chalk`Running {green ${packageManager} install}...`);
|
238 | spawn.sync(packageManager, ['install'], { stdio: 'inherit' });
|
239 | }
|