From 8210ab8fe36e1f74df84d4387cbe6a247a81f619 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Sat, 8 Nov 2025 13:22:49 +0100 Subject: [PATCH 1/4] feat(`dep-manager`): introduce --- utils/src/dep-manager.ts | 41 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 utils/src/dep-manager.ts diff --git a/utils/src/dep-manager.ts b/utils/src/dep-manager.ts new file mode 100644 index 00000000..fed8fd28 --- /dev/null +++ b/utils/src/dep-manager.ts @@ -0,0 +1,41 @@ +/** + * @fileoverview these utilties need codemod capabilities https://docs.codemod.com/jssg/security + */ +import { accessSync } from 'node:fs'; +import { execSync } from 'node:child_process'; + +export const detectPackageManager = (): 'npm' | 'yarn' | 'pnpm' => { + try { + accessSync('yarn.lock'); + return 'yarn'; + } catch {} + + try { + accessSync('pnpm-lock.yaml'); + return 'pnpm'; + } catch {} + + return 'npm'; +}; + +export const installDependency = ( + dependency: string, + isDevDependency = false, +): void => { + const packageManager = detectPackageManager(); + let command = ''; + + switch (packageManager) { + case 'npm': + command = `npm install ${isDevDependency ? '--save-dev' : '--save'} ${dependency}`; + break; + case 'yarn': + command = `yarn add ${isDevDependency ? '--dev' : ''} ${dependency}`; + break; + case 'pnpm': + command = `pnpm add ${isDevDependency ? '--save-dev' : '--save'} ${dependency}`; + break; + } + + execSync(command, { stdio: 'inherit' }); +}; From 49a9bfc9bf27d58281ebf05be62f0bcfeceaa224 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Sat, 8 Nov 2025 14:30:09 +0100 Subject: [PATCH 2/4] wip --- utils/src/dep-manager.test.ts | 151 ++++++++++++++++++++++++++++++++++ utils/src/dep-manager.ts | 26 +++++- 2 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 utils/src/dep-manager.test.ts diff --git a/utils/src/dep-manager.test.ts b/utils/src/dep-manager.test.ts new file mode 100644 index 00000000..e41f3906 --- /dev/null +++ b/utils/src/dep-manager.test.ts @@ -0,0 +1,151 @@ +import { execSync } from 'node:child_process'; +import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import assert from 'node:assert/strict'; +import { describe, it, beforeEach, afterEach } from 'node:test'; +import { + detectPackageManager, + installDependency, + removeDependency, +} from './dep-manager.ts'; + +const PACKAGE_NAME = '@augustinmauroy/vec3'; + +const haveNpmOnMachine = (): boolean => { + try { + execSync('npm --version', { stdio: 'ignore' }); + return true; + } catch { + return false; + } +}; + +const haveYarnOnMachine = (): boolean => { + try { + execSync('yarn --version', { stdio: 'ignore' }); + return true; + } catch { + return false; + } +}; + +const havePnpmOnMachine = (): boolean => { + try { + execSync('pnpm --version', { stdio: 'ignore' }); + return true; + } catch { + return false; + } +}; + +describe('dep-manager utilities', () => { + let tempDir: string; + + beforeEach(() => { + tempDir = mkdtempSync(join(tmpdir(), 'dep-manager-test-')); + process.chdir(tempDir); + }); + + afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }); + }); + + it('should work with npm', { skip: !haveNpmOnMachine() }, () => { + execSync('npm init -y', { stdio: 'ignore', cwd: tempDir }); + + installDependency(PACKAGE_NAME); + + const detectedManager = detectPackageManager(); + assert.strictEqual(detectedManager, 'npm'); + + installDependency(PACKAGE_NAME, false, 'ignore'); + + const packageJsonPath = join(tempDir, 'package.json'); + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + + assert( + packageJson.dependencies[PACKAGE_NAME], + `${PACKAGE_NAME} should be in dependencies`, + ); + + removeDependency(PACKAGE_NAME, 'ignore'); + + const updatedPackageJson = JSON.parse( + readFileSync(packageJsonPath, 'utf-8'), + ); + assert( + !updatedPackageJson.dependencies || + !updatedPackageJson.dependencies[PACKAGE_NAME], + `${PACKAGE_NAME} should not be in dependencies`, + ); + }); + + it('should work with yarn', { skip: !haveYarnOnMachine() }, () => { + execSync('yarn init -y', { stdio: 'ignore', cwd: tempDir }); + + installDependency(PACKAGE_NAME, false, 'ignore'); + + const detectedManager = detectPackageManager(); + assert.strictEqual(detectedManager, 'yarn'); + + const packageJsonPath = join(tempDir, 'package.json'); + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + + assert( + packageJson.dependencies[PACKAGE_NAME], + `${PACKAGE_NAME} should be in dependencies`, + ); + + removeDependency(PACKAGE_NAME, 'ignore'); + + const updatedPackageJson = JSON.parse( + readFileSync(packageJsonPath, 'utf-8'), + ); + assert( + !updatedPackageJson.dependencies || + !updatedPackageJson.dependencies[PACKAGE_NAME], + `${PACKAGE_NAME} should not be in dependencies`, + ); + }); + + it('should work with pnpm', { skip: !havePnpmOnMachine() }, () => { + execSync('pnpm init', { stdio: 'ignore', cwd: tempDir }); + + // Run pnpm install to generate the pnpm-lock.yaml file + execSync('pnpm install', { stdio: 'ignore', cwd: tempDir }); + + installDependency(PACKAGE_NAME, false, 'ignore'); + + const detectedManager = detectPackageManager(); + assert.strictEqual(detectedManager, 'pnpm'); + + const packageJsonPath = join(tempDir, 'package.json'); + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + + assert( + packageJson.dependencies[PACKAGE_NAME], + `${PACKAGE_NAME} should be in dependencies`, + ); + + removeDependency(PACKAGE_NAME, 'ignore'); + + const updatedPackageJson = JSON.parse( + readFileSync(packageJsonPath, 'utf-8'), + ); + assert( + !updatedPackageJson.dependencies || + !updatedPackageJson.dependencies[PACKAGE_NAME], + `${PACKAGE_NAME} should not be in dependencies`, + ); + }); + + it('should default to npm if no lock file is present', () => { + execSync('npm init -y', { stdio: 'ignore', cwd: tempDir }); + + rmSync(join(tempDir, 'package-lock.json'), { force: true }); + + const detectedManager = detectPackageManager(); + assert.strictEqual(detectedManager, 'npm'); + }); +}); diff --git a/utils/src/dep-manager.ts b/utils/src/dep-manager.ts index fed8fd28..887e7a04 100644 --- a/utils/src/dep-manager.ts +++ b/utils/src/dep-manager.ts @@ -3,6 +3,7 @@ */ import { accessSync } from 'node:fs'; import { execSync } from 'node:child_process'; +import type { StdioOptions } from 'node:child_process'; export const detectPackageManager = (): 'npm' | 'yarn' | 'pnpm' => { try { @@ -21,6 +22,7 @@ export const detectPackageManager = (): 'npm' | 'yarn' | 'pnpm' => { export const installDependency = ( dependency: string, isDevDependency = false, + stdio: StdioOptions = 'inherit', ): void => { const packageManager = detectPackageManager(); let command = ''; @@ -37,5 +39,27 @@ export const installDependency = ( break; } - execSync(command, { stdio: 'inherit' }); + execSync(command, { stdio }); +}; + +export const removeDependency = ( + dependency: string, + stdio: StdioOptions = 'inherit', +): void => { + const packageManager = detectPackageManager(); + let command = ''; + + switch (packageManager) { + case 'npm': + command = `npm uninstall ${dependency}`; + break; + case 'yarn': + command = `yarn remove ${dependency}`; + break; + case 'pnpm': + command = `pnpm remove ${dependency}`; + break; + } + + execSync(command, { stdio }); }; From 4b02a6454c8fafe63fd162df48dbf8108d2ce4ca Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Sat, 8 Nov 2025 14:34:31 +0100 Subject: [PATCH 3/4] test make async --- utils/src/dep-manager.test.ts | 57 ++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/utils/src/dep-manager.test.ts b/utils/src/dep-manager.test.ts index e41f3906..d3e05746 100644 --- a/utils/src/dep-manager.test.ts +++ b/utils/src/dep-manager.test.ts @@ -1,9 +1,10 @@ import { execSync } from 'node:child_process'; -import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { mkdtemp, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import assert from 'node:assert/strict'; import { describe, it, beforeEach, afterEach } from 'node:test'; +import { spawnPromisified } from './spawn-promisified.ts'; import { detectPackageManager, installDependency, @@ -42,37 +43,37 @@ const havePnpmOnMachine = (): boolean => { describe('dep-manager utilities', () => { let tempDir: string; - beforeEach(() => { - tempDir = mkdtempSync(join(tmpdir(), 'dep-manager-test-')); + beforeEach(async () => { + tempDir = await mkdtemp(join(tmpdir(), 'dep-manager-test-')); process.chdir(tempDir); }); - afterEach(() => { - rmSync(tempDir, { recursive: true, force: true }); + afterEach(async () => { + await rm(tempDir, { recursive: true, force: true }); }); - it('should work with npm', { skip: !haveNpmOnMachine() }, () => { - execSync('npm init -y', { stdio: 'ignore', cwd: tempDir }); + it('should work with npm', { skip: !haveNpmOnMachine() }, async () => { + await spawnPromisified('npm', ['init', '-y'], { cwd: tempDir }); - installDependency(PACKAGE_NAME); + await installDependency(PACKAGE_NAME); const detectedManager = detectPackageManager(); assert.strictEqual(detectedManager, 'npm'); - installDependency(PACKAGE_NAME, false, 'ignore'); + await installDependency(PACKAGE_NAME, false, 'ignore'); const packageJsonPath = join(tempDir, 'package.json'); - const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8')); assert( packageJson.dependencies[PACKAGE_NAME], `${PACKAGE_NAME} should be in dependencies`, ); - removeDependency(PACKAGE_NAME, 'ignore'); + await removeDependency(PACKAGE_NAME, 'ignore'); const updatedPackageJson = JSON.parse( - readFileSync(packageJsonPath, 'utf-8'), + await readFile(packageJsonPath, 'utf-8'), ); assert( !updatedPackageJson.dependencies || @@ -81,26 +82,26 @@ describe('dep-manager utilities', () => { ); }); - it('should work with yarn', { skip: !haveYarnOnMachine() }, () => { - execSync('yarn init -y', { stdio: 'ignore', cwd: tempDir }); + it('should work with yarn', { skip: !haveYarnOnMachine() }, async () => { + await spawnPromisified('yarn', ['init', '-y'], { cwd: tempDir }); - installDependency(PACKAGE_NAME, false, 'ignore'); + await installDependency(PACKAGE_NAME, false, 'ignore'); const detectedManager = detectPackageManager(); assert.strictEqual(detectedManager, 'yarn'); const packageJsonPath = join(tempDir, 'package.json'); - const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8')); assert( packageJson.dependencies[PACKAGE_NAME], `${PACKAGE_NAME} should be in dependencies`, ); - removeDependency(PACKAGE_NAME, 'ignore'); + await removeDependency(PACKAGE_NAME, 'ignore'); const updatedPackageJson = JSON.parse( - readFileSync(packageJsonPath, 'utf-8'), + await readFile(packageJsonPath, 'utf-8'), ); assert( !updatedPackageJson.dependencies || @@ -109,29 +110,29 @@ describe('dep-manager utilities', () => { ); }); - it('should work with pnpm', { skip: !havePnpmOnMachine() }, () => { - execSync('pnpm init', { stdio: 'ignore', cwd: tempDir }); + it('should work with pnpm', { skip: !havePnpmOnMachine() }, async () => { + await spawnPromisified('pnpm', ['init'], { cwd: tempDir }); // Run pnpm install to generate the pnpm-lock.yaml file - execSync('pnpm install', { stdio: 'ignore', cwd: tempDir }); + await spawnPromisified('pnpm', ['install'], { cwd: tempDir }); - installDependency(PACKAGE_NAME, false, 'ignore'); + await installDependency(PACKAGE_NAME, false, 'ignore'); const detectedManager = detectPackageManager(); assert.strictEqual(detectedManager, 'pnpm'); const packageJsonPath = join(tempDir, 'package.json'); - const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8')); assert( packageJson.dependencies[PACKAGE_NAME], `${PACKAGE_NAME} should be in dependencies`, ); - removeDependency(PACKAGE_NAME, 'ignore'); + await removeDependency(PACKAGE_NAME, 'ignore'); const updatedPackageJson = JSON.parse( - readFileSync(packageJsonPath, 'utf-8'), + await readFile(packageJsonPath, 'utf-8'), ); assert( !updatedPackageJson.dependencies || @@ -140,10 +141,10 @@ describe('dep-manager utilities', () => { ); }); - it('should default to npm if no lock file is present', () => { - execSync('npm init -y', { stdio: 'ignore', cwd: tempDir }); + it('should default to npm if no lock file is present', async () => { + await spawnPromisified('npm', ['init', '-y'], { cwd: tempDir }); - rmSync(join(tempDir, 'package-lock.json'), { force: true }); + await rm(join(tempDir, 'package-lock.json'), { force: true }); const detectedManager = detectPackageManager(); assert.strictEqual(detectedManager, 'npm'); From 2b18f6919f9f53b2c2634e1a6de223a5260ff842 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Sat, 8 Nov 2025 14:45:58 +0100 Subject: [PATCH 4/4] fix --- utils/src/dep-manager.test.ts | 20 ++++++++++---------- utils/src/dep-manager.ts | 25 ++----------------------- 2 files changed, 12 insertions(+), 33 deletions(-) diff --git a/utils/src/dep-manager.test.ts b/utils/src/dep-manager.test.ts index d3e05746..d7b842fa 100644 --- a/utils/src/dep-manager.test.ts +++ b/utils/src/dep-manager.test.ts @@ -5,11 +5,7 @@ import { join } from 'node:path'; import assert from 'node:assert/strict'; import { describe, it, beforeEach, afterEach } from 'node:test'; import { spawnPromisified } from './spawn-promisified.ts'; -import { - detectPackageManager, - installDependency, - removeDependency, -} from './dep-manager.ts'; +import { detectPackageManager, removeDependency } from './dep-manager.ts'; const PACKAGE_NAME = '@augustinmauroy/vec3'; @@ -55,13 +51,11 @@ describe('dep-manager utilities', () => { it('should work with npm', { skip: !haveNpmOnMachine() }, async () => { await spawnPromisified('npm', ['init', '-y'], { cwd: tempDir }); - await installDependency(PACKAGE_NAME); + await spawnPromisified('npm', ['install', PACKAGE_NAME], { cwd: tempDir }); const detectedManager = detectPackageManager(); assert.strictEqual(detectedManager, 'npm'); - await installDependency(PACKAGE_NAME, false, 'ignore'); - const packageJsonPath = join(tempDir, 'package.json'); const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8')); @@ -85,7 +79,13 @@ describe('dep-manager utilities', () => { it('should work with yarn', { skip: !haveYarnOnMachine() }, async () => { await spawnPromisified('yarn', ['init', '-y'], { cwd: tempDir }); - await installDependency(PACKAGE_NAME, false, 'ignore'); + await spawnPromisified('yarn', ['add', PACKAGE_NAME], { cwd: tempDir }); + + // list all file in the tempDir for debugging + const files = await import('node:fs').then((fs) => + fs.promises.readdir(tempDir), + ); + console.log('Files in tempDir:', files); const detectedManager = detectPackageManager(); assert.strictEqual(detectedManager, 'yarn'); @@ -116,7 +116,7 @@ describe('dep-manager utilities', () => { // Run pnpm install to generate the pnpm-lock.yaml file await spawnPromisified('pnpm', ['install'], { cwd: tempDir }); - await installDependency(PACKAGE_NAME, false, 'ignore'); + await spawnPromisified('pnpm', ['add', PACKAGE_NAME], { cwd: tempDir }); const detectedManager = detectPackageManager(); assert.strictEqual(detectedManager, 'pnpm'); diff --git a/utils/src/dep-manager.ts b/utils/src/dep-manager.ts index 887e7a04..a591b515 100644 --- a/utils/src/dep-manager.ts +++ b/utils/src/dep-manager.ts @@ -7,7 +7,9 @@ import type { StdioOptions } from 'node:child_process'; export const detectPackageManager = (): 'npm' | 'yarn' | 'pnpm' => { try { + console.log('Checking for yarn.lock file'); accessSync('yarn.lock'); + console.log('Detected yarn.lock file'); return 'yarn'; } catch {} @@ -19,29 +21,6 @@ export const detectPackageManager = (): 'npm' | 'yarn' | 'pnpm' => { return 'npm'; }; -export const installDependency = ( - dependency: string, - isDevDependency = false, - stdio: StdioOptions = 'inherit', -): void => { - const packageManager = detectPackageManager(); - let command = ''; - - switch (packageManager) { - case 'npm': - command = `npm install ${isDevDependency ? '--save-dev' : '--save'} ${dependency}`; - break; - case 'yarn': - command = `yarn add ${isDevDependency ? '--dev' : ''} ${dependency}`; - break; - case 'pnpm': - command = `pnpm add ${isDevDependency ? '--save-dev' : '--save'} ${dependency}`; - break; - } - - execSync(command, { stdio }); -}; - export const removeDependency = ( dependency: string, stdio: StdioOptions = 'inherit',