Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic implementation of bump and publish methods #1

Merged
merged 13 commits into from
Mar 25, 2020
9 changes: 8 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,12 @@ module.exports = {
env: {
node: true,
},
rules: {},
overrides: [
{
files: ['jest.setup.js', '__tests__/**/*.js'],
env: {
jest: true,
},
},
],
};
93 changes: 93 additions & 0 deletions __tests__/plugin-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
const { createTempDir } = require('broccoli-test-helper');
const { factory, runTasks } = require('release-it/test/util');
const Plugin = require('../index');

const namespace = 'release-it-yarn-workspaces';

class TestPlugin extends Plugin {
constructor() {
super(...arguments);

this.responses = {};

this.commands = [];
this.shell.execFormattedCommand = async (command, options) => {
this.commands.push([command, options]);
if (this.responses[command]) {
return Promise.resolve(this.responses[command]);
}
};
}
}

function buildPlugin(config = {}, _Plugin = TestPlugin) {
const options = { [namespace]: config };
const plugin = factory(_Plugin, { namespace, options });

return plugin;
}

function json(obj) {
return JSON.stringify(obj, null, 2);
}

describe('release-it-yarn-workspaces', () => {
let ROOT = process.cwd();
let dir;

beforeEach(async () => {
dir = await createTempDir();

process.chdir(dir.path());
});

afterEach(async () => {
process.chdir(ROOT);
await dir.dispose();
});

it('it executes commands with defaults', async () => {
let plugin = buildPlugin();

dir.write({
'package.json': json({
name: 'root',
version: '0.0.0',
license: 'MIT',
private: true,
workspaces: ['packages/*'],
}),
packages: {
foo: {
'package.json': json({
name: 'foo',
version: '0.0.0',
license: 'MIT',
}),
},
bar: {
'package.json': json({
name: 'bar',
version: '0.0.0',
license: 'MIT',
}),
},
},
});

await runTasks(plugin);

expect(plugin.commands).toMatchInlineSnapshot(`
Array [
Array [
"npm version 0.0.1 --no-git-tag-version",
Object {},
],
Array [
"npm version 0.0.1 --no-git-tag-version",
Object {},
],
]
`);
});
});
156 changes: 150 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,153 @@
const { Plugin } = require('release-it');
const path = require('path');
const walkSync = require('walk-sync');
const { hasAccess, rejectAfter } = require('release-it/lib/util');
const { npmTimeoutError, npmAuthError } = require('release-it/lib/errors');
const prompts = require('release-it/lib/plugin/npm/prompts');
const UpstreamPlugin = require('release-it/lib/plugin/npm/npm');

module.exports = class YarnWorkspacesPlugin extends Plugin {
// TODO: implement me!
async bump() {}
const options = { write: false };

// TODO: implement me!
async publish() {}
const ROOT_MANIFEST_PATH = './package.json';
const REGISTRY_TIMEOUT = 10000;
const DEFAULT_TAG = 'latest';

const noop = Promise.resolve();

function resolveWorkspaces(workspaces) {
if (Array.isArray(workspaces)) {
return workspaces;
} else if (workspaces !== null && typeof workspaces === 'object') {
return workspaces.workspaces;
}

throw new Error(
"This package doesn't use yarn workspaces. (package.json doesn't contain a `workspaces` property)"
);
}

module.exports = class YarnWorkspacesPlugin extends UpstreamPlugin {
static isEnabled(options) {
return hasAccess(ROOT_MANIFEST_PATH) && options !== false;
}

constructor(...args) {
super(...args);
this.registerPrompts(prompts);
}

async init() {
// intentionally not calling super.init here:
//
// * avoid the `getLatestRegistryVersion` check

const {
name,
version: latestVersion,
private: isPrivate,
publishConfig,
workspaces,
} = require(path.resolve(ROOT_MANIFEST_PATH));

this.setContext({
name,
latestVersion,
private: isPrivate,
publishConfig,
workspaces: resolveWorkspaces(workspaces),
root: process.cwd(),
});

if (this.options.skipChecks) return;

const validations = Promise.all([this.isRegistryUp(), this.isAuthenticated()]);

await Promise.race([validations, rejectAfter(REGISTRY_TIMEOUT)]);

const [isRegistryUp, isAuthenticated] = await validations;

if (!isRegistryUp) {
throw new npmTimeoutError(REGISTRY_TIMEOUT);
}

if (!isAuthenticated) {
throw new npmAuthError();
}
}

async bump(version) {
// intentionally not calling super.bump here

const task = () => {
return this.eachWorkspace(() => {
return this.exec(`npm version ${version} --no-git-tag-version`).catch(err => {
if (/version not changed/i.test(err)) {
this.log.warn(`Did not update version in package.json, etc. (already at ${version}).`);
}
});
});
};

const tag = this.options.tag || (await this.resolveTag(version));
this.setContext({ version, tag });
return this.spinner.show({ task, label: 'npm version' });
}

async publish({ otp = this.options.otp, otpCallback } = {}) {
// intentionally not calling super.publish here

const { publishPath = '.', access } = this.options;
const { name, private: isPrivate, tag = DEFAULT_TAG, isNewPackage } = this.getContext();
const isScopedPkg = name.startsWith('@');
const accessArg =
isScopedPkg && (access || (isNewPackage && !isPrivate))
? `--access ${access || 'public'}`
: '';
const otpArg = otp ? `--otp ${otp}` : '';
const dryRunArg = this.global.isDryRun ? '--dry-run' : '';
if (isPrivate) {
this.log.warn('Skip publish: package is private.');
return noop;
}

return this.eachWorkspace(async () => {
try {
await this.exec(
`npm publish ${publishPath} --tag ${tag} ${accessArg} ${otpArg} ${dryRunArg}`,
{ options }
);

this.isReleased = true;
} catch (err) {
this.debug(err);
if (/one-time pass/.test(err)) {
if (otp != null) {
this.log.warn('The provided OTP is incorrect or has expired.');
}
if (otpCallback) {
return otpCallback(otp => this.publish({ otp, otpCallback }));
}
}
throw err;
}
});
}

eachWorkspace(action) {
return Promise.all(
this.getWorkspaceDirs().map(async workspace => {
try {
process.chdir(path.dirname(workspace));
return await action();
} finally {
process.chdir(this.getContext('root'));
}
})
);
}

getWorkspaceDirs() {
return walkSync('.', {
globs: this.getContext('workspaces').map(glob => `${glob}/package.json`),
}).map(workspace => path.resolve(this.getContext('root'), workspace));
}
};
3 changes: 3 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
setupFilesAfterEnv: ['./jest.setup.js'],
};
1 change: 1 addition & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
jest.setTimeout(30000);
11 changes: 5 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,19 @@
],
"scripts": {
"lint:js": "eslint .",
"test": "ava"
},
"ava": {
"serial": true
"test": "jest"
},
"dependencies": {
"release-it": "^13.0.2"
"release-it": "^13.0.2",
"walk-sync": "^2.0.2"
},
"devDependencies": {
"ava": "^3.5.0",
"broccoli-test-helper": "^2.0.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-node": "^11.0.0",
"eslint-plugin-prettier": "^3.1.2",
"jest": "^25.1.0",
"prettier": "^1.19.1",
"release-it-lerna-changelog": "^2.1.0",
"sinon": "^9.0.1",
Expand Down
41 changes: 0 additions & 41 deletions test.js

This file was deleted.

Loading