Skip to content

Commit

Permalink
refactor: replace libnpm with npm cli (#139)
Browse files Browse the repository at this point in the history
* refactor: add npm version resolver

* refactor: replace libnpm with npm cli execution

* refactor: remove libnpm from project

* chore: fix linting issues

* chore: rebuild project
  • Loading branch information
byCedric authored Jan 12, 2022
1 parent 3134e26 commit 4f28493
Show file tree
Hide file tree
Showing 11 changed files with 142 additions and 1,424 deletions.
60 changes: 12 additions & 48 deletions build/setup/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion build/setup/index.map.js

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
"@actions/exec": "^1.1.0",
"@actions/io": "^1.1.1",
"@actions/tool-cache": "^1.7.1",
"libnpm": "^3.0.1",
"semver": "^7.3.5"
},
"devDependencies": {
Expand Down
3 changes: 2 additions & 1 deletion src/actions/setup.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { addPath, getBooleanInput, getInput, group, info } from '@actions/core';

import { install } from '../install';
import { resolveVersion } from '../packager';
import * as tools from '../tools';

// Auto-execute in GitHub actions
Expand Down Expand Up @@ -33,7 +34,7 @@ async function installCli(name: tools.PackageName): Promise<string | void> {
return info(`Skipping installation of ${name}, \`${shortName}-version\` not provided.`);
}

const version = await tools.resolveVersion(name, inputVersion);
const version = await resolveVersion(name, inputVersion);
const cache = getBooleanInput(`${shortName}-cache`);

try {
Expand Down
30 changes: 30 additions & 0 deletions src/packager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { exec } from '@actions/exec';

/**
* Resolve a package with version range to an exact version.
* This is useful to invalidate the cache _and_ using dist-tags or version ranges.
* It executes `npm info` and parses the latest manifest.
*/
export async function resolveVersion(name: string, range: string): Promise<string> {
let stdout = '';

try {
await exec('npm', ['info', `${name}@${range}`, 'version', '--json'], {
silent: true,
listeners: {
stdout(data) {
stdout += data.toString();
},
},
});
} catch (error) {
throw new Error(`Could not resolve version "${range}" of "${name}", reason:\n${error.message || error}`);
}

// thanks npm, for returning a "x.x.x" json value...
if (stdout.startsWith('"')) {
stdout = `[${stdout}]`;
}

return JSON.parse(stdout).at(-1);
}
12 changes: 1 addition & 11 deletions src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import * as core from '@actions/core';
import * as cli from '@actions/exec';
import semver from 'semver';

// eslint-disable-next-line @typescript-eslint/no-var-requires
const registry = require('libnpm');
import { resolveVersion } from './packager';

export type PackageName = 'expo-cli' | 'eas-cli';

Expand All @@ -23,15 +22,6 @@ export function getBinaryName(name: PackageName, forWindows = false): string {
return forWindows ? `${bin}.cmd` : bin;
}

/**
* Resolve the provided semver to exact version of `expo-cli`.
* This uses the npm registry and accepts latest, dist-tags or version ranges.
* It's used to determine the cached version of `expo-cli`.
*/
export async function resolveVersion(name: PackageName, version: string): Promise<string> {
return (await registry.manifest(`${name}@${version}`)).version;
}

/**
* Authenticate with Expo using either the token or username/password method.
* If both of them are set, token has priority.
Expand Down
5 changes: 5 additions & 0 deletions tests/actions/setup.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { getToolsMock, mockInput } from '../utils';

jest.mock('../../src/tools', () => getToolsMock());
jest.mock('../../src/packager', () => ({
async resolveVersion(_: string, version: string) {
return version;
},
}));

import * as core from '@actions/core';

Expand Down
27 changes: 27 additions & 0 deletions tests/packager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { valid as validVersion } from 'semver';
import { resolveVersion } from '../src/packager';

describe(resolveVersion, () => {
it('resolves expo-cli@^2.0.0 to 2.21.2', async () => {
await expect(resolveVersion('expo-cli', '^2.0.0')).resolves.toBe('2.21.2');
});

it('resolves expo-cli@~3.15.0 to 3.15.5', async () => {
await expect(resolveVersion('expo-cli', '~3.15.0')).resolves.toBe('3.15.5');
});

it('resolves eas-cli@~0.33.0 to 0.33.1', async () => {
await expect(resolveVersion('eas-cli', '~0.33.0')).resolves.toBe('0.33.1');
});

it('resolves expo-cli@latest to a valid version', async () => {
const version = await resolveVersion('expo-cli', 'latest');
expect(validVersion(version)).not.toBeNull();
});

it('rejects donotpublishthispackageoryouwillbefired with proper error', async () => {
await expect(resolveVersion('donotpublishthispackageoryouwillbefired', 'latest')).rejects.toThrow(
'Could not resolve version "latest" of "donotpublishthispackageoryouwillbefired"'
);
});
});
38 changes: 15 additions & 23 deletions tests/tools.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
const registry = { manifest: jest.fn() };
jest.mock('libnpm', () => registry);

import * as core from '@actions/core';
import * as cli from '@actions/exec';

import * as tools from '../src/tools';
import * as packager from '../src/packager';
import * as utils from './utils';

describe(tools.getBinaryName, () => {
Expand All @@ -21,20 +19,6 @@ describe(tools.getBinaryName, () => {
});
});

describe(tools.resolveVersion, () => {
it('fetches exact version of expo-cli', async () => {
registry.manifest.mockResolvedValue({ version: '3.0.10' });
expect(await tools.resolveVersion('expo-cli', 'latest')).toBe('3.0.10');
expect(registry.manifest).toBeCalledWith('expo-cli@latest');
});

it('fetches exact version of eas-cli', async () => {
registry.manifest.mockResolvedValue({ version: '4.2.0' });
expect(await tools.resolveVersion('eas-cli', 'latest')).toBe('4.2.0');
expect(registry.manifest).toBeCalledWith('eas-cli@latest');
});
});

describe(tools.maybeAuthenticate, () => {
const token = 'ABC123';
const username = 'bycedric';
Expand Down Expand Up @@ -268,21 +252,25 @@ describe(tools.maybeWarnForUpdate, () => {
let spy: { [key: string]: jest.SpyInstance } = {};

beforeEach(() => {
spy = { warning: jest.spyOn(core, 'warning').mockImplementation() };
spy = {
warning: jest.spyOn(core, 'warning').mockImplementation(),
resolveVersion: jest.spyOn(packager, 'resolveVersion').mockImplementation(),
};
});

afterAll(() => {
spy.warning.mockRestore();
spy.resolveVersion.mockRestore();
});

it('is silent when major version is up to date', async () => {
registry.manifest.mockResolvedValueOnce({ version: '4.1.0' }).mockResolvedValueOnce({ version: '4.0.1' });
spy.resolveVersion.mockResolvedValueOnce('4.1.0').mockResolvedValueOnce('4.0.1');
await tools.maybeWarnForUpdate('eas-cli');
expect(spy.warning).not.toBeCalled();
});

it('warns when major version is outdated', async () => {
registry.manifest.mockResolvedValueOnce({ version: '4.1.0' }).mockResolvedValueOnce({ version: '3.0.1' });
spy.resolveVersion.mockResolvedValueOnce('4.1.0').mockResolvedValueOnce('3.0.1');
await tools.maybeWarnForUpdate('expo-cli');
expect(spy.warning).toBeCalledWith('There is a new major version available of the Expo CLI (4.1.0)');
expect(spy.warning).toBeCalledWith('If you run into issues, try upgrading your workflow to "expo-version: 4.x"');
Expand All @@ -293,23 +281,27 @@ describe(tools.handleError, () => {
let spy: { [key: string]: jest.SpyInstance } = {};

beforeEach(() => {
spy = { setFailed: jest.spyOn(core, 'setFailed').mockImplementation() };
spy = {
setFailed: jest.spyOn(core, 'setFailed').mockImplementation(),
resolveVersion: jest.spyOn(packager, 'resolveVersion').mockImplementation(),
};
});

afterAll(() => {
spy.setFailed.mockRestore();
spy.resolveVersion.mockRestore();
});

it('marks the job as failed with expo-cli', async () => {
const error = new Error('test');
registry.manifest.mockResolvedValue('4.0.0');
spy.resolveVersion.mockResolvedValue('4.0.0');
await tools.handleError('expo-cli', error);
expect(core.setFailed).toBeCalledWith(error);
});

it('fails with original error when update warning failed', async () => {
const error = new Error('test');
registry.manifest.mockRejectedValue(new Error('npm issue'));
spy.resolveVersion.mockRejectedValue(new Error('npm issue'));
await tools.handleError('eas-cli', error);
expect(core.setFailed).toBeCalledWith(error);
});
Expand Down
1 change: 0 additions & 1 deletion tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ export function restoreEnv(): void {
export function getToolsMock() {
return {
getBinaryName: jest.fn(v => v.replace('-cli', '')),
resolveVersion: jest.fn((n, v) => v),
maybeAuthenticate: jest.fn(),
maybePatchWatchers: jest.fn(),
maybeWarnForUpdate: jest.fn(),
Expand Down
Loading

0 comments on commit 4f28493

Please sign in to comment.