Skip to content
Open
3 changes: 2 additions & 1 deletion .github/workflows/ui.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ jobs:
secrets: inherit
with:
jest-enabled: true
jest-test-command: yarn run test
jest-test-command: yarn run test:coverage
jest-coverage-report-dir: artifacts/coverage
sonar-sources: ./lib
compile-translations: false
generate-module-descriptor: false
Expand Down
3 changes: 2 additions & 1 deletion lib/cli/global-dirs.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ function isYarnVersion(version) {
logger.log('Yarn version', yarnVersion);
return semver.satisfies(yarnVersion, version);
} catch (err) {
logger.error('Unable to determine Yarn version.', err);
const logError = logger?.error ? logger.error : console.error;
logError('Unable to determine Yarn version.', err?.message ? err.message : err);
return false;
}
}
Expand Down
6 changes: 3 additions & 3 deletions lib/cli/stripes-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ module.exports = class StripesCore {
}

getCoreModulePath(moduleId) {
const coreModulePath = (this.corePath === this.coreAlias)
? path.join(this.corePath, moduleId)
: resolveFrom(this.corePath, `@folio/stripes-webpack/${moduleId}`);
const coreModulePath = (this.corePath.includes('node_modules'))
? resolveFrom(this.corePath, `@folio/stripes-webpack/${moduleId}`)
: path.join(this.corePath, moduleId);
return coreModulePath;
}

Expand Down
79 changes: 79 additions & 0 deletions lib/package-manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
const childProcess = require('node:child_process');
const path = require('node:path');
const fs = require('node:fs');
const logger = require('./cli/logger')('package-manager');

function hasCommand(cmd) {
try {
const v = childProcess.execSync(`${cmd} --version`, { encoding: 'utf8' }).trim();
// simple check that command exists and returns something
return !!v;
} catch (e) {
logger.log(`Unable to determine ${cmd} version.`, e?.message ? e.message : e);
return false;
}
}

function detect(projectDir) {
// Env override
const env = process.env.STRIPES_PKG_MANAGER;
if (env) return env;

// Prefer pnpm if a lockfile exists nearby
try {
const pnpmLock = path.join(projectDir || process.cwd(), 'pnpm-lock.yaml');
if (fs.existsSync(pnpmLock) && hasCommand('pnpm')) return 'pnpm';
} catch (e) {
throw new Error('There was an error in stripes-cli initialization: Unable to detect package manager.', { cause: e });
}

if (hasCommand('pnpm')) return 'pnpm';
if (hasCommand('yarn')) return 'yarn';
if (hasCommand('npm')) return 'npm';

// default to npm if nothing else
return 'npm';
}

function execCmd(cmd, args, options) {
return new Promise((resolve, reject) => {
const full = `${cmd} ${args.join(' ')}`.trim();
const p = childProcess.exec(full, options, (error) => {
if (error) return reject(error);
resolve({ isInstalled: true, appDir: options?.cwd });
return undefined;
});
if (p?.stdout) p.stdout.on('data', d => console.log(` ${d.toString().trim()}`));
if (p?.stderr) p.stderr.on('data', d => console.error(` ${d.toString().trim()}`));
});
}

function install(projectDir) {
const pm = detect(projectDir);
console.log(`Using package manager: ${pm}`);
if (pm === 'pnpm') return execCmd('pnpm', ['install'], { cwd: projectDir });
if (pm === 'yarn') return execCmd('yarn', [], { cwd: projectDir });
return execCmd('npm', ['install'], { cwd: projectDir });
}

function add(projectDir, packageName, isDev) {
const pm = detect(projectDir);
console.log(`Using package manager: ${pm}`);
const pkgs = packageName.flat().join(' ');
if (pm === 'pnpm') {
const flag = isDev ? ['-D'] : [];
return execCmd('pnpm', ['add'].concat(flag).concat([pkgs]), { cwd: projectDir });
}
if (pm === 'yarn') {
const flag = isDev ? '--dev' : '';
return execCmd('yarn', ['add', flag, pkgs].filter(Boolean), { cwd: projectDir });
}
const flag = isDev ? ['--save-dev'] : ['--save'];
return execCmd('npm', ['install'].concat(flag).concat([pkgs]), { cwd: projectDir });
}

module.exports = {
detect,
install,
add,
};
53 changes: 4 additions & 49 deletions lib/yarn.js
Original file line number Diff line number Diff line change
@@ -1,53 +1,8 @@
const childProcess = require('child_process');

function runYarn(options) {
return new Promise((resolve, reject) => {
const installProcess = childProcess.exec('yarn', options, (error) => {
if (error) {
reject(error);
return;
}
resolve({ isInstalled: true, appDir: options.cwd });
});
installProcess.stdout.on('data', data => console.log(` ${data.trim()}`));
});
}

function yarnAdd(packageName, isDevDep, options) {
const pkgs = [].concat(packageName).join(' ');
const flag = isDevDep ? '--dev' : '';

return new Promise((resolve, reject) => {
const installProcess = childProcess.exec(`yarn add ${flag} ${pkgs}`, options, (error) => {
if (error) {
reject(error);
return;
}
resolve({ isInstalled: true, appDir: options.cwd });
});
installProcess.stdout.on('data', data => console.log(` ${data.trim()}`));
});
}

// Above method does not capture full yarn output, but is a lot quieter
// TODO: Offer option to toggle between the two

// function runYarn(options) {
// options.stdio = 'inherit';
// return new Promise((resolve, reject) => {
// childProcess.spawn('yarn', [], options)
// .on('exit', (error) => {
// if (error) {
// reject(error);
// }
// resolve({ isInstalled: true, appDir: options.cwd });
// });
// });
// }
const packageManager = require('./package-manager');

function install(projectDir) {
console.log(` Directory "${projectDir}"`);
return runYarn({ cwd: projectDir })
return packageManager.install(projectDir)
.catch((err) => {
console.error('Something went wrong while attempting to use Yarn.');
console.info(err);
Expand All @@ -56,9 +11,9 @@ function install(projectDir) {

function add(projectDir, packageName, isDev) {
console.log(` Directory "${projectDir}"`);
return yarnAdd(packageName, isDev, { cwd: projectDir })
return packageManager.add(projectDir, packageName, isDev)
.catch((err) => {
console.error('Something went wrong while attempting to use Yarn.');
console.error('Something went wrong while attempting to add package via package manager.');
console.info(err);
});
}
Expand Down
203 changes: 203 additions & 0 deletions test/lib/package-manager.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
const expect = require('chai').expect;
const childProcess = require('node:child_process');
const fs = require('node:fs');
const { EventEmitter } = require('node:events');

const packageManager = require('../../lib/package-manager');

// Stubs childProcess.exec to simulate a successful or failed command,
// emitting stdout/stderr asynchronously so listeners attached by execCmd
// are in place before data arrives.
function stubExec(sandbox, { error, stdout, stderr } = {}) {
return sandbox.stub(childProcess, 'exec').callsFake((command, options, callback) => {
const proc = new EventEmitter();
proc.stdout = new EventEmitter();
proc.stderr = new EventEmitter();
setImmediate(() => {
if (stdout) proc.stdout.emit('data', stdout);
if (stderr) proc.stderr.emit('data', stderr);
callback(error || null);
});
return proc;
});
}

describe('The package-manager service', function () {
let originalEnv;

beforeEach(function () {
originalEnv = process.env.STRIPES_PKG_MANAGER;
delete process.env.STRIPES_PKG_MANAGER;
this.sandbox.stub(console, 'log');
this.sandbox.stub(console, 'error');
});

afterEach(function () {
if (originalEnv === undefined) {
delete process.env.STRIPES_PKG_MANAGER;
} else {
process.env.STRIPES_PKG_MANAGER = originalEnv;
}
});

describe('detect', function () {
it('honors the STRIPES_PKG_MANAGER environment variable', function () {
process.env.STRIPES_PKG_MANAGER = 'npm';
this.sandbox.stub(fs, 'existsSync');
this.sandbox.stub(childProcess, 'execSync');

expect(packageManager.detect('/project')).to.equal('npm');
expect(fs.existsSync).not.to.have.been.called;
expect(childProcess.execSync).not.to.have.been.called;
});

it('prefers pnpm when a pnpm-lock.yaml is present and pnpm is available', function () {
this.sandbox.stub(fs, 'existsSync').returns(true);
this.sandbox.stub(childProcess, 'execSync').returns('8.0.0');

expect(packageManager.detect('/project')).to.equal('pnpm');
expect(childProcess.execSync).to.have.been.calledWithMatch('pnpm --version');
});

it('falls back to yarn when pnpm is not installed', function () {
this.sandbox.stub(fs, 'existsSync').returns(false);
this.sandbox.stub(childProcess, 'execSync').callsFake((cmd) => {
if (cmd.startsWith('pnpm')) throw new Error('command not found: pnpm');
if (cmd.startsWith('yarn')) return '1.22.0';
return '';
});

expect(packageManager.detect('/project')).to.equal('yarn');
});

it('falls back to npm when neither pnpm nor yarn is installed', function () {
this.sandbox.stub(fs, 'existsSync').returns(false);
this.sandbox.stub(childProcess, 'execSync').callsFake((cmd) => {
if (cmd.startsWith('npm')) return '8.0.0';
throw new Error('command not found');
});

expect(packageManager.detect('/project')).to.equal('npm');
});

it('defaults to npm when no other package manager is detected', function () {
this.sandbox.stub(fs, 'existsSync').returns(false);
this.sandbox.stub(childProcess, 'execSync').returns('');

expect(packageManager.detect('/project')).to.equal('npm');
});

it('wraps errors encountered while probing for a pnpm lockfile', function () {
this.sandbox.stub(fs, 'existsSync').throws(new Error('disk error'));

expect(() => packageManager.detect('/project')).to.throw('Unable to detect package manager');
});
});

describe('install', function () {
it('installs with pnpm when pnpm is detected', function () {
process.env.STRIPES_PKG_MANAGER = 'pnpm';
const exec = stubExec(this.sandbox);

return packageManager.install('/project').then((result) => {
expect(exec).to.have.been.calledWithMatch('pnpm install', { cwd: '/project' });
expect(result).to.eql({ isInstalled: true, appDir: '/project' });
});
});

it('installs with yarn when yarn is detected', function () {
process.env.STRIPES_PKG_MANAGER = 'yarn';
const exec = stubExec(this.sandbox);

return packageManager.install('/project').then(() => {
expect(exec).to.have.been.calledWithMatch('yarn', { cwd: '/project' });
});
});

it('installs with npm by default', function () {
process.env.STRIPES_PKG_MANAGER = 'npm';
const exec = stubExec(this.sandbox);

return packageManager.install('/project').then(() => {
expect(exec).to.have.been.calledWithMatch('npm install', { cwd: '/project' });
});
});

it('logs stdout and stderr from the child process', function () {
process.env.STRIPES_PKG_MANAGER = 'npm';
stubExec(this.sandbox, { stdout: 'installing...', stderr: 'a warning' });

return packageManager.install('/project').then(() => {
expect(console.log).to.have.been.calledWithMatch('installing...');
expect(console.error).to.have.been.calledWithMatch('a warning');
});
});

it('rejects when the child process fails', function () {
process.env.STRIPES_PKG_MANAGER = 'npm';
const failure = new Error('command failed');
stubExec(this.sandbox, { error: failure });

return packageManager.install('/project').then(
() => { throw new Error('expected install to reject'); },
(err) => { expect(err).to.equal(failure); }
);
});
});

describe('add', function () {
it('adds a dependency with pnpm, using the -D flag for dev dependencies', function () {
process.env.STRIPES_PKG_MANAGER = 'pnpm';
const exec = stubExec(this.sandbox);

return packageManager.add('/project', ['lodash'], true).then(() => {
expect(exec).to.have.been.calledWithMatch('pnpm add -D lodash', { cwd: '/project' });
});
});

it('adds a dependency with yarn, using the --dev flag for dev dependencies', function () {
process.env.STRIPES_PKG_MANAGER = 'yarn';
const exec = stubExec(this.sandbox);

return packageManager.add('/project', ['lodash'], true).then(() => {
expect(exec).to.have.been.calledWithMatch('yarn add --dev lodash', { cwd: '/project' });
});
});

it('adds a dependency with yarn, omitting the flag for non-dev dependencies', function () {
process.env.STRIPES_PKG_MANAGER = 'yarn';
const exec = stubExec(this.sandbox);

return packageManager.add('/project', ['lodash'], false).then(() => {
expect(exec).to.have.been.calledWithMatch('yarn add lodash', { cwd: '/project' });
});
});

it('adds a dependency with npm, using --save-dev for dev dependencies', function () {
process.env.STRIPES_PKG_MANAGER = 'npm';
const exec = stubExec(this.sandbox);

return packageManager.add('/project', ['lodash'], true).then(() => {
expect(exec).to.have.been.calledWithMatch('npm install --save-dev lodash', { cwd: '/project' });
});
});

it('adds a dependency with npm, using --save by default', function () {
process.env.STRIPES_PKG_MANAGER = 'npm';
const exec = stubExec(this.sandbox);

return packageManager.add('/project', ['lodash'], false).then(() => {
expect(exec).to.have.been.calledWithMatch('npm install --save lodash', { cwd: '/project' });
});
});

it('flattens nested package name arrays', function () {
process.env.STRIPES_PKG_MANAGER = 'npm';
const exec = stubExec(this.sandbox);

return packageManager.add('/project', [['lodash', 'chalk']], false).then(() => {
expect(exec).to.have.been.calledWithMatch('npm install --save lodash chalk', { cwd: '/project' });
});
});
});
});
Loading
Loading