Skip to content

Commit

Permalink
feat(node): convert npm tools to typescript (#1285)
Browse files Browse the repository at this point in the history
  • Loading branch information
viceice authored Aug 8, 2023
1 parent ddd5338 commit ce37682
Show file tree
Hide file tree
Showing 19 changed files with 299 additions and 294 deletions.
16 changes: 16 additions & 0 deletions src/cli/install-tool/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ import { rootContainer } from '../services';
import { InstallDartService } from '../tools/dart';
import { InstallDockerService } from '../tools/docker';
import { InstallFluxService } from '../tools/flux';
import {
InstallCorepackService,
InstallLernaService,
InstallNpmService,
InstallPnpmService,
InstallRenovateService,
InstallYarnService,
InstallYarnSlimService,
} from '../tools/node/npm';
import { logger } from '../utils';
import { InstallLegacyToolService } from './install-legacy-tool.service';
import { INSTALL_TOOL_TOKEN, InstallToolService } from './install-tool.service';
Expand All @@ -17,9 +26,16 @@ function prepareContainer(): Container {
container.bind(InstallLegacyToolService).toSelf();

// tool services
container.bind(INSTALL_TOOL_TOKEN).to(InstallCorepackService);
container.bind(INSTALL_TOOL_TOKEN).to(InstallDockerService);
container.bind(INSTALL_TOOL_TOKEN).to(InstallDartService);
container.bind(INSTALL_TOOL_TOKEN).to(InstallFluxService);
container.bind(INSTALL_TOOL_TOKEN).to(InstallLernaService);
container.bind(INSTALL_TOOL_TOKEN).to(InstallNpmService);
container.bind(INSTALL_TOOL_TOKEN).to(InstallPnpmService);
container.bind(INSTALL_TOOL_TOKEN).to(InstallRenovateService);
container.bind(INSTALL_TOOL_TOKEN).to(InstallYarnService);
container.bind(INSTALL_TOOL_TOKEN).to(InstallYarnSlimService);

logger.trace('preparing container done');
return container;
Expand Down
11 changes: 8 additions & 3 deletions src/cli/install-tool/install-tool-base.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { chmod, chown, stat, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { injectable } from 'inversify';
import type { EnvService, PathService } from '../services';
import { NoPrepareTools } from '../tools';
import { fileRights, isValid, logger } from '../utils';

export interface ShellWrapperConfig {
Expand Down Expand Up @@ -33,7 +34,11 @@ export abstract class InstallToolBaseService {
abstract link(version: string): Promise<void>;

needsPrepare(): boolean {
return true;
return !NoPrepareTools.includes(this.name);
}

postInstall(_version: string): Promise<void> {
return Promise.resolve();
}

test(_version: string): Promise<void> {
Expand All @@ -44,8 +49,8 @@ export abstract class InstallToolBaseService {
return this.name;
}

validate(version: string): boolean {
return isValid(version);
validate(version: string): Promise<boolean> {
return Promise.resolve(isValid(version));
}

protected async shellwrapper({
Expand Down
15 changes: 10 additions & 5 deletions src/cli/install-tool/install-tool.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ export class InstallToolService {
}

logger.debug({ tool }, 'validate tool');
if (!toolSvc.validate(version)) {
logger.fatal({ tool }, 'tool version not supported');
if (!(await toolSvc.validate(version))) {
logger.fatal({ tool, version }, 'tool version not supported');
return 1;
}

Expand All @@ -80,11 +80,16 @@ export class InstallToolService {
): Promise<void> {
if (version === (await this.versionSvc.find(toolSvc.name))) {
logger.debug({ tool: toolSvc.name }, 'tool already linked');
return;
} else {
logger.debug({ tool: toolSvc.name }, 'link tool');
await toolSvc.link(version);
}
logger.debug({ tool: toolSvc.name }, 'link tool');
await toolSvc.link(version);

await this.versionSvc.update(toolSvc.name, version);

logger.debug({ tool: toolSvc.name }, 'post-install tool');
await toolSvc.postInstall(version);

logger.debug({ tool: toolSvc.name }, 'test tool');
if (!this.envSvc.skipTests) {
await toolSvc.test(version);
Expand Down
5 changes: 5 additions & 0 deletions src/cli/prepare-tool/prepare-tool.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { inject, injectable, multiInject, optional } from 'inversify';
import { EnvService, PathService } from '../services';
import { NoPrepareTools } from '../tools';
import { logger } from '../utils';
import { PrepareLegacyToolsService } from './prepare-legacy-tools.service';
import type { PrepareToolBaseService } from './prepare-tool-base.service';
Expand Down Expand Up @@ -52,6 +53,10 @@ export class PrepareToolService {
logger.info({ tool }, 'tool ignored');
continue;
}
if (NoPrepareTools.includes(tool)) {
logger.info({ tool }, 'tool does not need to be prepared');
continue;
}
const toolSvc = this.toolSvcs.find((t) => t.name === tool);
if (toolSvc) {
if (await this.pathSvc.findToolPath(tool)) {
Expand Down
2 changes: 1 addition & 1 deletion src/cli/services/version.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class VersionService {
async find(tool: string): Promise<string | null> {
const path = join(this.pathSvc.versionPath, tool);
try {
return (await readFile(path, { encoding: 'utf8' })) ?? null;
return (await readFile(path, { encoding: 'utf8' })).trim() ?? null;
} catch (err) {
if (err instanceof Error && err.code === 'ENOENT') {
logger.debug({ tool }, 'tool version not found');
Expand Down
4 changes: 0 additions & 4 deletions src/cli/tools/flux/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,6 @@ export class InstallFluxService extends InstallToolBaseService {
await this.shellwrapper({ srcDir: src });
}

override needsPrepare(): boolean {
return false;
}

override async test(_version: string): Promise<void> {
await execa('flux', ['--version'], { stdio: 'inherit' });
}
Expand Down
10 changes: 10 additions & 0 deletions src/cli/tools/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const NoPrepareTools = [
'corepack',
'flux',
'lerna',
'npm',
'pnpm',
'renovate',
'yarn',
'yarn-slim',
];
73 changes: 73 additions & 0 deletions src/cli/tools/node/npm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { join } from 'node:path';
import { execa } from 'execa';
import { injectable } from 'inversify';
import { InstallNpmBaseService } from './utils';

@injectable()
export class InstallCorepackService extends InstallNpmBaseService {
override name: string = 'corepack';

override async postInstall(version: string): Promise<void> {
await super.postInstall(version);

const src = join(this.pathSvc.versionedToolPath(this.name, version), 'bin');
await this.shellwrapper({ srcDir: src, name: 'pnpm' });
await this.shellwrapper({ srcDir: src, name: 'yarn' });
}
}

@injectable()
export class InstallLernaService extends InstallNpmBaseService {
override readonly name: string = 'lerna';
}

@injectable()
export class InstallNpmService extends InstallNpmBaseService {
override readonly name: string = 'npm';
}

@injectable()
export class InstallPnpmService extends InstallNpmBaseService {
override readonly name: string = 'pnpm';
}

@injectable()
export class InstallRenovateService extends InstallNpmBaseService {
override readonly name: string = 'renovate';

override async postInstall(version: string): Promise<void> {
await super.postInstall(version);

const src = join(this.pathSvc.versionedToolPath(this.name, version), 'bin');
await this.shellwrapper({ srcDir: src, name: 'renovate-config-validator' });
}
}

@injectable()
export class InstallYarnService extends InstallNpmBaseService {
override readonly name: string = 'yarn';
}

@injectable()
export class InstallYarnSlimService extends InstallNpmBaseService {
override readonly name: string = 'yarn-slim';

protected override get tool(): string {
return 'yarn';
}

override async install(version: string): Promise<void> {
await super.install(version);
// TODO: replace with javascript
const prefix = await this.pathSvc.findVersionedToolPath(this.name, version);
await execa(
'sed',
[
'-i',
's/ steps,/ steps.slice(0,1),/',
`${prefix}/node_modules/yarn/lib/cli.js`,
],
{ stdio: 'inherit' }
);
}
}
139 changes: 139 additions & 0 deletions src/cli/tools/node/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import fs from 'node:fs/promises';
import { join } from 'node:path';
import { env as penv } from 'node:process';
import { execa } from 'execa';
import { inject, injectable } from 'inversify';
import { InstallToolBaseService } from '../../install-tool/install-tool-base.service';
import { EnvService, PathService, VersionService } from '../../services';
import { parse } from '../../utils';

const defaultRegistry = 'https://registry.npmjs.org/';

@injectable()
export abstract class InstallNpmBaseService extends InstallToolBaseService {
protected get tool(): string {
return this.name;
}

constructor(
@inject(EnvService) envSvc: EnvService,
@inject(PathService) pathSvc: PathService,
@inject(VersionService) protected versionSvc: VersionService
) {
super(pathSvc, envSvc);
}

override async install(version: string): Promise<void> {
const npm = await this.getNodeNpm();
const tmp = await fs.mkdtemp(
join(this.pathSvc.tmpDir, 'containerbase-npm-')
);
const env: NodeJS.ProcessEnv = {
NO_UPDATE_NOTIFIER: '1',
npm_config_update_notifier: 'false',
npm_config_fund: 'false',
};

if (!penv.npm_config_cache && !penv.NPM_CONFIG_CACHE) {
env.npm_config_cache = tmp;
}

if (!penv.npm_config_registry && !penv.NPM_CONFIG_REGISTRY) {
const registry = this.envSvc.replaceUrl(defaultRegistry);
if (registry !== defaultRegistry) {
env.npm_config_registry = registry;
}
}

// TODO: create recursive
if (!(await this.pathSvc.findToolPath(this.name))) {
await this.pathSvc.createToolPath(this.name);
}

const prefix = await this.pathSvc.createVersionedToolPath(
this.name,
version
);

await execa(
npm,
[
'install',
`${this.tool}@${version}`,
'--save-exact',
'--no-audit',
'--prefix',
prefix,
'--cache',
tmp,
'--silent',
],
{ stdio: ['inherit', 'inherit', 1], env }
);

await fs.symlink(`${prefix}/node_modules/.bin`, `${prefix}/bin`);

const ver = parse(version)!;

if (this.name === 'npm' && ver.major < 7) {
// update to latest node-gyp to fully support python3
await execa(
join(prefix, 'bin/npm'),
[
'explore',
'npm',
'--prefix',
prefix,
'--silent',
'--',
'npm',
'install',
'node-gyp@latest',
'--no-audit',
'--cache',
tmp,
'--silent',
],
{ stdio: ['inherit', 'inherit', 1], env }
);
}

await fs.rm(tmp, { recursive: true, force: true });
await fs.rm(join(this.envSvc.home, '.npm/_logs'), {
recursive: true,
force: true,
});
}

override async link(version: string): Promise<void> {
await this.postInstall(version);
}

override async postInstall(version: string): Promise<void> {
const src = join(this.pathSvc.versionedToolPath(this.name, version), 'bin');

await this.shellwrapper({ srcDir: src, name: this.tool });
}

override async test(_version: string): Promise<void> {
await execa(this.tool, ['--version'], { stdio: 'inherit' });
}

override async validate(version: string): Promise<boolean> {
if (!(await super.validate(version))) {
return false;
}

return (await this.versionSvc.find('node')) !== null;
}

protected async getNodeNpm(): Promise<string> {
const nodeVersion = await this.versionSvc.find('node');

if (!nodeVersion) {
throw new Error('Node not installed');
}

return join(this.pathSvc.versionedToolPath('node', nodeVersion), 'bin/npm');
}
}
6 changes: 5 additions & 1 deletion src/cli/utils/versions.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import semver from 'semver';
import semver, { type SemVer } from 'semver';
import { type StrictValidator, makeValidator } from 'typanion';

export function isValid(version: string): boolean {
return semver.valid(version) !== null;
}

export function parse(version: string): SemVer | null {
return semver.parse(version);
}

export function validateSemver(): StrictValidator<string, string> {
return makeValidator<string, string>({
test: (value, state): value is string => {
Expand Down
Loading

0 comments on commit ce37682

Please sign in to comment.