Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ It may be useful for standalone scripts that do not require a `package.json`. Al
- [Installation](#installation)
- [CommonJS](#commonjs)
- [ES Modules](#es-modules)
- [Cleanup](#cleanup)
- [Programmatic cleanup](#programmatic-cleanup)
- [CLI cleanup command](#cli-cleanup-command)
- [Examples](#examples)
- [Questions and issues](#questions-and-issues)
- [Contributing](#contributing)
Expand Down Expand Up @@ -367,6 +370,69 @@ const _ = await use('[email protected]');
console.log(`_.add(1, 2) = ${_.add(1, 2)}`);
```

## Cleanup

`use-m` automatically installs packages globally in Node.js and Bun environments to provide fast, cached access across different projects. Over time, you may want to clean up these globally installed packages to free up disk space.

### Programmatic cleanup

You can programmatically clean up all packages installed by `use-m` using the `use.cleanup()` function:

```javascript
import { use } from 'use-m';

// Clean up all use-m installed packages
const result = await use.cleanup();

console.log(`Cleaned ${result.cleaned.length} packages:`, result.cleaned);
console.log(`Environment: ${result.environment}`);

if (result.errors) {
console.log('Errors:', result.errors);
}
```

The cleanup function:
- **Node.js/npm**: Removes all globally installed packages matching the `use-m` naming pattern (`package-name-v-version`)
- **Bun**: Removes all globally installed packages from Bun's global directory
- **Browser/Deno**: Returns a skip message since these environments don't require cleanup (packages are loaded from CDNs)

The function returns an object with:
- `cleaned`: Array of package names that were successfully removed
- `environment`: The detected environment (`'npm'` or `'bun'`)
- `errors`: Array of any errors encountered during cleanup (optional)
- `skipped`: Message explaining why cleanup was skipped (for browser/Deno environments)

### CLI cleanup command

You can also use the command-line interface to clean up packages:

```bash
# Using npx
npx use-m cleanup

# If installed globally
use-m cleanup

# Using the CLI directly
node cli.mjs cleanup
```

The CLI command will:
1. Display which environment is being cleaned (npm or bun)
2. List all packages that were successfully removed
3. Show any errors that occurred during the cleanup process

Example output:
```
Starting cleanup of use-m packages...
Environment: npm
Successfully removed 3 package(s):
- lodash-v-4.17.21
- express-v-4.18.0
- yargs-v-17.7.2
```

## Examples

You can check out [usage examples source code](https://github.com/link-foundation/use-m/tree/main/examples). You can also explore our [tests](https://github.com/link-foundation/use-m/tree/main/tests) to get even more examples.
Expand Down
34 changes: 34 additions & 0 deletions cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,40 @@ yargs(hideBin(process.argv))
type: 'boolean',
description: 'Output the path to loader.js file',
})
.command('cleanup', 'Remove all globally installed packages by use-m', () => {}, async (argv) => {
try {
console.log('Starting cleanup of use-m packages...');
const result = await use.cleanup();

if (result.skipped) {
console.log(result.skipped);
return;
}

console.log(`Environment: ${result.environment}`);

if (result.cleaned.length > 0) {
console.log(`Successfully removed ${result.cleaned.length} package(s):`);
result.cleaned.forEach(pkg => console.log(` - ${pkg}`));
} else {
console.log('No use-m packages found to clean up.');
}

if (result.errors && result.errors.length > 0) {
console.log('\nErrors encountered:');
result.errors.forEach(error => {
if (error.package) {
console.log(` - ${error.package}: ${error.error}`);
} else {
console.log(` - ${error.error}`);
}
});
}
} catch (error) {
console.error('Failed to cleanup packages:', error.message);
process.exit(1);
}
})
.help('help')
.alias('help', 'h')
.version(getVersion()) // Use yargs' built-in version functionality
Expand Down
51 changes: 51 additions & 0 deletions tests/cleanup.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { describe, test, expect } from '../test-adapter.mjs';
import { use } from 'use-m';
const moduleName = `[${import.meta.url.split('.').pop()} module]`;

describe(`${moduleName} cleanup functionality`, () => {
test(`${moduleName} cleanup function exists`, async () => {
expect(typeof use.cleanup).toBe('function');
});

test(`${moduleName} cleanup returns proper structure`, async () => {
const result = await use.cleanup();

// Should have the expected structure
expect(result).toHaveProperty('cleaned');
expect(Array.isArray(result.cleaned)).toBe(true);

// Should have environment info unless skipped
if (!result.skipped) {
expect(result).toHaveProperty('environment');
expect(['npm', 'bun'].includes(result.environment)).toBe(true);
}
}, 15000);

test(`${moduleName} cleanup in browser/deno should skip`, async () => {
// Mock browser environment by temporarily removing process
const originalProcess = global.process;
const originalBun = global.Bun;

delete global.process;
delete global.Bun;

Check failure on line 30 in tests/cleanup.test.mjs

View workflow job for this annotation

GitHub Actions / Test on bun

TypeError: Unable to delete property.

at <anonymous> (/home/runner/work/use-m/use-m/tests/cleanup.test.mjs:30:19) at <anonymous> (/home/runner/work/use-m/use-m/tests/cleanup.test.mjs:24:61)

Check failure on line 30 in tests/cleanup.test.mjs

View workflow job for this annotation

GitHub Actions / Test on bun

TypeError: Unable to delete property.

at <anonymous> (/Users/runner/work/use-m/use-m/tests/cleanup.test.mjs:30:19) at <anonymous> (/Users/runner/work/use-m/use-m/tests/cleanup.test.mjs:24:61)

try {
const result = await use.cleanup();
expect(result.skipped).toBeDefined();
expect(result.skipped).toBe('Browser/Deno environment does not require cleanup');
} finally {
// Restore original globals
global.process = originalProcess;
global.Bun = originalBun;
}
});

test(`${moduleName} cleanup handles empty directories gracefully`, async () => {
// This test ensures cleanup doesn't fail when there are no packages to clean
const result = await use.cleanup();

// Should not throw an error even if no packages are found
expect(result).toHaveProperty('cleaned');
expect(Array.isArray(result.cleaned)).toBe(true);
}, 15000);
});
92 changes: 92 additions & 0 deletions use.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@
// This shouldn't happen but provide a fallback
throw new Error(`Failed to resolve 'process' module in Deno environment.`);
}
return ({ default: process, ...process });

Check failure on line 180 in use.cjs

View workflow job for this annotation

GitHub Actions / Test on bun

ReferenceError: process is not defined

at node (/Users/runner/work/use-m/use-m/use.cjs:180:26) at <anonymous> (/Users/runner/work/use-m/use-m/use.cjs:243:30) at builtin (/Users/runner/work/use-m/use-m/use.cjs:218:19) at <anonymous> (/Users/runner/work/use-m/use-m/use.cjs:712:43) at <anonymous> (/Users/runner/work/use-m/use-m/use.cjs:705:17) at <anonymous> (/Users/runner/work/use-m/use-m/use.cjs:755:10) at use (/Users/runner/work/use-m/use-m/use.cjs:730:20) at <anonymous> (/Users/runner/work/use-m/use-m/tests/builtin-nodejs.test.cjs:121:33) at <anonymous> (/Users/runner/work/use-m/use-m/tests/builtin-nodejs.test.cjs:120:52)
}
},
'child_process': {
Expand Down Expand Up @@ -243,7 +243,7 @@
const result = await moduleFactory();
return result;
} catch (error) {
throw new Error(`Failed to load built-in module '${moduleName}' in ${environment} environment.`, { cause: error });

Check failure on line 246 in use.cjs

View workflow job for this annotation

GitHub Actions / Test on bun

error: Failed to load built-in module 'process' in node environment.

at <anonymous> (/Users/runner/work/use-m/use-m/use.cjs:246:15) at builtin (/Users/runner/work/use-m/use-m/use.cjs:218:19) at <anonymous> (/Users/runner/work/use-m/use-m/use.cjs:712:43) at <anonymous> (/Users/runner/work/use-m/use-m/use.cjs:705:17) at <anonymous> (/Users/runner/work/use-m/use-m/use.cjs:755:10) at use (/Users/runner/work/use-m/use-m/use.cjs:730:20) at <anonymous> (/Users/runner/work/use-m/use-m/tests/builtin-nodejs.test.cjs:121:33) at <anonymous> (/Users/runner/work/use-m/use-m/tests/builtin-nodejs.test.cjs:120:52)
}
}

Expand Down Expand Up @@ -531,7 +531,7 @@
} catch (error) {
// In CI or fresh environments, the global directory might not exist
// Try to get the default Bun install path
const home = process.env.HOME || process.env.USERPROFILE;

Check failure on line 534 in use.cjs

View workflow job for this annotation

GitHub Actions / Test on bun

ReferenceError: process is not defined

at <anonymous> (/home/runner/work/use-m/use-m/use.cjs:534:22) at ensurePackageInstalled (/home/runner/work/use-m/use-m/use.cjs:524:45) at <anonymous> (/home/runner/work/use-m/use-m/use.cjs:560:31)

Check failure on line 534 in use.cjs

View workflow job for this annotation

GitHub Actions / Test on bun

ReferenceError: process is not defined

at <anonymous> (/home/runner/work/use-m/use-m/use.cjs:534:22) at ensurePackageInstalled (/home/runner/work/use-m/use-m/use.cjs:524:45) at <anonymous> (/home/runner/work/use-m/use-m/use.cjs:560:31)

Check failure on line 534 in use.cjs

View workflow job for this annotation

GitHub Actions / Test on bun

ReferenceError: process is not defined

at <anonymous> (/home/runner/work/use-m/use-m/use.cjs:534:22) at ensurePackageInstalled (/home/runner/work/use-m/use-m/use.cjs:524:45) at <anonymous> (/home/runner/work/use-m/use-m/use.cjs:560:31)
if (home) {
binDir = path.join(home, '.bun', 'bin');
} else {
Expand Down Expand Up @@ -761,6 +761,98 @@
return Promise.all(moduleSpecifiers.map(__use));
}

use.cleanup = async () => {
const isNode = typeof process !== 'undefined' && process.versions && process.versions.node;
const isBun = typeof Bun !== 'undefined';

if (!isNode && !isBun) {
// For browser/Deno environments, no cleanup needed as packages are loaded from CDNs
return { cleaned: [], skipped: 'Browser/Deno environment does not require cleanup' };
}

const { exec } = await import('node:child_process');
const { promisify } = await import('node:util');
const path = await import('node:path');
const { readdir } = await import('node:fs/promises');
const execAsync = promisify(exec);

const cleaned = [];
const errors = [];

try {
if (isBun) {
// Cleanup for Bun environment
let binDir = '';
try {
const { stdout } = await execAsync('bun pm bin -g');
binDir = stdout.trim();
} catch (error) {
const home = process.env.HOME || process.env.USERPROFILE;
if (home) {
binDir = path.join(home, '.bun', 'bin');
} else {
throw new Error('Unable to determine Bun global directory.');
}
}

const bunInstallRoot = path.resolve(binDir, '..');
const globalModulesPath = path.join(bunInstallRoot, 'install', 'global', 'node_modules');

try {
const packages = await readdir(globalModulesPath);
const useManagerPackages = packages.filter(pkg => pkg.match(/^.+-v-.+$/));

for (const pkg of useManagerPackages) {
try {
await execAsync(`bun remove -g ${pkg}`, { stdio: 'ignore' });
cleaned.push(pkg);
} catch (error) {
errors.push({ package: pkg, error: error.message });
}
}
} catch (error) {
if (error.code !== 'ENOENT') {
errors.push({ error: `Failed to read global modules directory: ${error.message}` });
}
}
} else {
// Cleanup for Node.js/npm environment
const { stdout: globalModulesPath } = await execAsync('npm root -g');
const globalPath = globalModulesPath.trim();

try {
const packages = await readdir(globalPath);
const useManagerPackages = packages.filter(pkg => pkg.match(/^.+-v-.+$/));

for (const pkg of useManagerPackages) {
try {
await execAsync(`npm uninstall -g ${pkg}`, { stdio: 'ignore' });
cleaned.push(pkg);
} catch (error) {
errors.push({ package: pkg, error: error.message });
}
}
} catch (error) {
if (error.code !== 'ENOENT') {
errors.push({ error: `Failed to read global modules directory: ${error.message}` });
}
}
}

return {
cleaned,
errors: errors.length > 0 ? errors : undefined,
environment: isBun ? 'bun' : 'npm'
};
} catch (error) {
return {
cleaned,
errors: [{ error: error.message }],
environment: isBun ? 'bun' : 'npm'
};
}
}

module.exports = {
parseModuleSpecifier,
resolvers,
Expand Down
92 changes: 92 additions & 0 deletions use.js
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,98 @@ use.all = async (...moduleSpecifiers) => {
return Promise.all(moduleSpecifiers.map(__use));
}

use.cleanup = async () => {
const isNode = typeof process !== 'undefined' && process.versions && process.versions.node;
const isBun = typeof Bun !== 'undefined';

if (!isNode && !isBun) {
// For browser/Deno environments, no cleanup needed as packages are loaded from CDNs
return { cleaned: [], skipped: 'Browser/Deno environment does not require cleanup' };
}

const { exec } = await import('node:child_process');
const { promisify } = await import('node:util');
const path = await import('node:path');
const { readdir } = await import('node:fs/promises');
const execAsync = promisify(exec);

const cleaned = [];
const errors = [];

try {
if (isBun) {
// Cleanup for Bun environment
let binDir = '';
try {
const { stdout } = await execAsync('bun pm bin -g');
binDir = stdout.trim();
} catch (error) {
const home = process.env.HOME || process.env.USERPROFILE;
if (home) {
binDir = path.join(home, '.bun', 'bin');
} else {
throw new Error('Unable to determine Bun global directory.');
}
}

const bunInstallRoot = path.resolve(binDir, '..');
const globalModulesPath = path.join(bunInstallRoot, 'install', 'global', 'node_modules');

try {
const packages = await readdir(globalModulesPath);
const useManagerPackages = packages.filter(pkg => pkg.match(/^.+-v-.+$/));

for (const pkg of useManagerPackages) {
try {
await execAsync(`bun remove -g ${pkg}`, { stdio: 'ignore' });
cleaned.push(pkg);
} catch (error) {
errors.push({ package: pkg, error: error.message });
}
}
} catch (error) {
if (error.code !== 'ENOENT') {
errors.push({ error: `Failed to read global modules directory: ${error.message}` });
}
}
} else {
// Cleanup for Node.js/npm environment
const { stdout: globalModulesPath } = await execAsync('npm root -g');
const globalPath = globalModulesPath.trim();

try {
const packages = await readdir(globalPath);
const useManagerPackages = packages.filter(pkg => pkg.match(/^.+-v-.+$/));

for (const pkg of useManagerPackages) {
try {
await execAsync(`npm uninstall -g ${pkg}`, { stdio: 'ignore' });
cleaned.push(pkg);
} catch (error) {
errors.push({ package: pkg, error: error.message });
}
}
} catch (error) {
if (error.code !== 'ENOENT') {
errors.push({ error: `Failed to read global modules directory: ${error.message}` });
}
}
}

return {
cleaned,
errors: errors.length > 0 ? errors : undefined,
environment: isBun ? 'bun' : 'npm'
};
} catch (error) {
return {
cleaned,
errors: [{ error: error.message }],
environment: isBun ? 'bun' : 'npm'
};
}
}

makeUse.parseModuleSpecifier = parseModuleSpecifier;
makeUse.resolvers = resolvers;
makeUse.makeUse = makeUse;
Expand Down
Loading
Loading