Skip to content

Commit

Permalink
feat(cli/node): install latest node and npm tool versions (#1828)
Browse files Browse the repository at this point in the history
  • Loading branch information
viceice authored Jan 3, 2024
1 parent beacf32 commit 92ed0b5
Show file tree
Hide file tree
Showing 15 changed files with 235 additions and 23 deletions.
3 changes: 3 additions & 0 deletions docs/custom-registries.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,11 @@ https://github.com/containerbase/node-prebuild/releases/download/18.12.0/node-18
https://nodejs.org/dist/v20.0.0/SHASUMS256.txt
https://nodejs.org/dist/v20.0.0/node-v20.0.0-linux-x64.tar.xz
https://nodejs.org/dist/v20.0.0/node-v20.0.0-linux-arm64.tar.xz
https://nodejs.org/dist/index.json
```

The url `https://nodejs.org/dist/index.json` is used to find the latest version if no version was provided.

### npm tools

Npm tools are downloaded from:
Expand Down
5 changes: 3 additions & 2 deletions src/cli/command/install-gem.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@ import { prepareCommands } from '.';

const mocks = vi.hoisted(() => ({
installTool: vi.fn(),
resolveVersion: vi.fn((_, v) => v),
prepareTools: vi.fn(),
}));

vi.mock('../install-tool', () => mocks);
vi.mock('../prepare-tool', () => mocks);

describe('index', () => {
describe('install-gem', () => {
beforeEach(() => {
delete env.RAKE_VERSION;
});

test('install-gem', async () => {
test('works', async () => {
const cli = new Cli({ binaryName: 'install-gem' });
prepareCommands(cli, 'install-gem');

Expand Down
16 changes: 13 additions & 3 deletions src/cli/command/install-npm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,36 @@ import { prepareCommands } from '.';

const mocks = vi.hoisted(() => ({
installTool: vi.fn(),
resolveVersion: vi.fn((_, v) => v),
prepareTools: vi.fn(),
}));

vi.mock('../install-tool', () => mocks);
vi.mock('../prepare-tool', () => mocks);

describe('index', () => {
describe('install-npm', () => {
beforeEach(() => {
delete env.DEL_CLI_VERSION;
});

test('install-npm', async () => {
test('works', async () => {
const cli = new Cli({ binaryName: 'install-npm' });
prepareCommands(cli, 'install-npm');

expect(await cli.run(['del-cli'])).toBe(MissingVersion);

mocks.resolveVersion.mockResolvedValueOnce('4.0.0');
expect(await cli.run(['del-cli'])).toBe(0);

env.DEL_CLI_VERSION = '5.0.0';
expect(await cli.run(['del-cli'])).toBe(0);
expect(mocks.installTool).toHaveBeenCalledTimes(1);
expect(mocks.installTool).toHaveBeenCalledTimes(2);
expect(mocks.installTool).toHaveBeenCalledWith(
'del-cli',
'4.0.0',
false,
'npm',
);
expect(mocks.installTool).toHaveBeenCalledWith(
'del-cli',
'5.0.0',
Expand Down
5 changes: 3 additions & 2 deletions src/cli/command/install-tool.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@ import { prepareCommands } from '.';

const mocks = vi.hoisted(() => ({
installTool: vi.fn(),
resolveVersion: vi.fn((_, v) => v),
prepareTools: vi.fn(),
}));

vi.mock('../install-tool', () => mocks);
vi.mock('../prepare-tool', () => mocks);

describe('index', () => {
describe('install-tool', () => {
beforeEach(() => {
delete env.NODE_VERSION;
});

test('install-tool', async () => {
test('works', async () => {
const cli = new Cli({ binaryName: 'install-tool' });
prepareCommands(cli, 'install-tool');

Expand Down
16 changes: 13 additions & 3 deletions src/cli/command/install-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import is from '@sindresorhus/is';
import { Command, Option } from 'clipanion';
import prettyMilliseconds from 'pretty-ms';
import * as t from 'typanion';
import { type InstallToolType, installTool } from '../install-tool';
import {
type InstallToolType,
installTool,
resolveVersion,
} from '../install-tool';
import { ResolverMap } from '../tools';
import { logger, validateVersion } from '../utils';
import { MissingVersion } from '../utils/codes';
import { getVersion } from './utils';
Expand Down Expand Up @@ -36,20 +41,25 @@ export class InstallToolCommand extends Command {
override async execute(): Promise<number | void> {
let version = this.version;

const type = ResolverMap[this.name] ?? this.type;

if (!is.nonEmptyStringAndNotWhitespace(version)) {
version = getVersion(this.name);
}

logger.debug(`Try resolving version for ${this.name}@${version} ...`);
version = await resolveVersion(this.name, version, type);

if (!is.nonEmptyStringAndNotWhitespace(version)) {
logger.error(`No version found for ${this.name}`);
return MissingVersion;
}

const start = Date.now();
let error = false;
logger.info(`Installing ${this.type ?? 'tool'} ${this.name}@${version}...`);
logger.info(`Installing ${type ?? 'tool'} ${this.name}@${version}...`);
try {
return await installTool(this.name, version, this.dryRun, this.type);
return await installTool(this.name, version, this.dryRun, type);
} catch (err) {
logger.fatal(err);
error = true;
Expand Down
70 changes: 67 additions & 3 deletions src/cli/install-tool/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import {
InstallYarnService,
InstallYarnSlimService,
} from '../tools/node/npm';
import {
NodeVersionResolver,
NpmVersionResolver,
} from '../tools/node/resolver';
import { InstallNodeBaseService } from '../tools/node/utils';
import {
InstallBundlerService,
Expand All @@ -28,11 +32,13 @@ import { InstallRubyBaseService } from '../tools/ruby/utils';
import { logger } from '../utils';
import { InstallLegacyToolService } from './install-legacy-tool.service';
import { INSTALL_TOOL_TOKEN, InstallToolService } from './install-tool.service';
import { TOOL_VERSION_RESOLVER } from './tool-version-resolver';
import { ToolVersionResolverService } from './tool-version-resolver.service';

export type InstallToolType = 'gem' | 'npm';

function prepareContainer(): Container {
logger.trace('preparing container');
function prepareInstallContainer(): Container {
logger.trace('preparing install container');
const container = new Container();
container.parent = rootContainer;

Expand Down Expand Up @@ -61,6 +67,21 @@ function prepareContainer(): Container {
container.bind(INSTALL_TOOL_TOKEN).to(InstallYarnService);
container.bind(INSTALL_TOOL_TOKEN).to(InstallYarnSlimService);

logger.trace('preparing install container done');
return container;
}

function prepareResolveContainer(): Container {
logger.trace('preparing resolve container');
const container = new Container();
container.parent = rootContainer;

// core services
container.bind(ToolVersionResolverService).toSelf();

// tool version resolver
container.bind(TOOL_VERSION_RESOLVER).to(NodeVersionResolver);

logger.trace('preparing container done');
return container;
}
Expand All @@ -71,7 +92,7 @@ export function installTool(
dryRun = false,
type?: InstallToolType,
): Promise<number | void> {
const container = prepareContainer();
const container = prepareInstallContainer();
if (type) {
switch (type) {
case 'gem': {
Expand Down Expand Up @@ -120,3 +141,46 @@ export function installTool(
}
return container.get(InstallToolService).execute(tool, version, dryRun);
}

export async function resolveVersion(
tool: string,
version: string | undefined,
type?: InstallToolType,
): Promise<string | undefined> {
const container = prepareResolveContainer();

if (type) {
switch (type) {
// case 'gem': {
// @injectable()
// class InstallGenericGemService extends InstallRubyBaseService {
// override readonly name: string = tool;

// override needsPrepare(): boolean {
// return false;
// }

// override async test(version: string): Promise<void> {
// try {
// // some npm packages may not have a `--version` flag
// await super.test(version);
// } catch (err) {
// logger.debug(err);
// }
// }
// }
// container.bind(INSTALL_TOOL_TOKEN).to(InstallGenericGemService);
// break;
// }
case 'npm': {
@injectable()
class GenericNpmVersionResolver extends NpmVersionResolver {
override readonly tool: string = tool;
}
container.bind(TOOL_VERSION_RESOLVER).to(GenericNpmVersionResolver);
break;
}
}
}
return await container.get(ToolVersionResolverService).resolve(tool, version);
}
20 changes: 20 additions & 0 deletions src/cli/install-tool/tool-version-resolver.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { injectable, multiInject } from 'inversify';
import {
TOOL_VERSION_RESOLVER,
type ToolVersionResolver,
} from './tool-version-resolver';

@injectable()
export class ToolVersionResolverService {
constructor(
@multiInject(TOOL_VERSION_RESOLVER) private resolver: ToolVersionResolver[],
) {}

async resolve(
tool: string,
version: string | undefined,
): Promise<string | undefined> {
const resolver = this.resolver.find((r) => r.tool === tool);
return (await resolver?.resolve(version)) ?? version;
}
}
16 changes: 16 additions & 0 deletions src/cli/install-tool/tool-version-resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { injectable } from 'inversify';
import type { EnvService, HttpService } from '../services';

export const TOOL_VERSION_RESOLVER = Symbol('TOOL_VERSION_RESOLVER');

@injectable()
export abstract class ToolVersionResolver {
abstract readonly tool: string;

constructor(
protected readonly http: HttpService,
protected readonly env: EnvService,
) {}

abstract resolve(version: string | undefined): Promise<string | undefined>;
}
12 changes: 12 additions & 0 deletions src/cli/tools/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { InstallToolType } from '../install-tool';

export const NoPrepareTools = [
'bazelisk',
'bower',
Expand All @@ -15,3 +17,13 @@ export const NoPrepareTools = [
'yarn',
'yarn-slim',
];

export const ResolverMap: Record<string, InstallToolType | undefined> = {
corepack: 'npm',
lerna: 'npm',
npm: 'npm',
pnpm: 'npm',
renovate: 'npm',
yarn: 'npm',
'yarn-slim': 'npm',
};
4 changes: 2 additions & 2 deletions src/cli/tools/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ export class InstallNodeService extends InstallNodeBaseService {
constructor(
@inject(EnvService) envSvc: EnvService,
@inject(PathService) pathSvc: PathService,
@inject(HttpService) http: HttpService,
@inject(HttpService) private http: HttpService,
@inject(CompressionService) private compress: CompressionService,
@inject(VersionService) versionSvc: VersionService,
) {
super(envSvc, pathSvc, versionSvc, http);
super(envSvc, pathSvc, versionSvc);
}

override async install(version: string): Promise<void> {
Expand Down
2 changes: 2 additions & 0 deletions src/cli/tools/node/npm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { InstallNodeBaseService } from './utils';
@injectable()
export class InstallBowerService extends InstallNodeBaseService {
override readonly name: string = 'bower';
protected override readonly deprecated: boolean = true;
}

@injectable()
Expand All @@ -15,6 +16,7 @@ export class InstallCorepackService extends InstallNodeBaseService {
@injectable()
export class InstallLernaService extends InstallNodeBaseService {
override readonly name: string = 'lerna';
protected override readonly deprecated: boolean = true;
}

@injectable()
Expand Down
58 changes: 58 additions & 0 deletions src/cli/tools/node/resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import is from '@sindresorhus/is';
import { inject, injectable } from 'inversify';
import { ToolVersionResolver } from '../../install-tool/tool-version-resolver';
import { EnvService, HttpService } from '../../services';
import type { NodeVersionMeta, NpmPackageMeta } from './types';

@injectable()
export class NodeVersionResolver extends ToolVersionResolver {
readonly tool = 'node';

constructor(
@inject(HttpService) http: HttpService,
@inject(EnvService) env: EnvService,
) {
super(http, env);
}

async resolve(version: string | undefined): Promise<string | undefined> {
if (!is.nonEmptyStringAndNotWhitespace(version) || version === 'latest') {
const url = this.env.replaceUrl('https://nodejs.org/dist/index.json');
const meta = await this.http.getJson<NodeVersionMeta[]>(url, {
headers: {
accept:
'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*',
},
});
// we know that the latest version is the first entry, so search for first lts
return meta.find((v) => v.lts)?.version.replace(/^v/, '');
}
return version;
}
}

@injectable()
export abstract class NpmVersionResolver extends ToolVersionResolver {
constructor(
@inject(HttpService) http: HttpService,
@inject(EnvService) env: EnvService,
) {
super(http, env);
}

async resolve(version: string | undefined): Promise<string | undefined> {
if (!is.nonEmptyStringAndNotWhitespace(version) || version === 'latest') {
const meta = await this.http.getJson<NpmPackageMeta>(
`https://registry.npmjs.org/${this.tool}`,
{
headers: {
accept:
'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*',
},
},
);
return meta['dist-tags'].latest;
}
return version;
}
}
9 changes: 9 additions & 0 deletions src/cli/tools/node/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface NodeVersionMeta {
version: string;
lts?: boolean;
}

export interface NpmPackageMeta {
'dist-tags': Record<string, string>;
name: string;
}
Loading

0 comments on commit 92ed0b5

Please sign in to comment.