Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
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 });
Comment thread
zburke marked this conversation as resolved.
Outdated
}

if (hasCommand('pnpm')) return 'pnpm';
if (hasCommand('yarn')) return 'yarn';
if (hasCommand('npm')) return 'npm';
Comment thread
zburke marked this conversation as resolved.
Outdated

// 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}`);
Comment thread
zburke marked this conversation as resolved.
Outdated
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}`);
Comment thread
zburke marked this conversation as resolved.
Outdated
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
Comment thread
zburke marked this conversation as resolved.
Outdated
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