generated from JS-DevTools/template-node-typescript
-
Notifications
You must be signed in to change notification settings - Fork 77
/
Copy pathcall-npm-cli.ts
151 lines (128 loc) · 3.92 KB
/
call-npm-cli.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import childProcess from "node:child_process";
import os from "node:os";
import * as errors from "../errors.js";
import type { Logger } from "../options.js";
import type { NpmCliEnvironment } from "./use-npm-environment.js";
export interface NpmCliOptions {
environment: NpmCliEnvironment;
ignoreScripts: boolean;
logger?: Logger | undefined;
}
export interface NpmCallResult<CommandT extends string> {
successData: SuccessData<CommandT> | undefined;
errorCode: string | undefined;
error: Error | undefined;
}
type SuccessData<T extends string> = T extends typeof VIEW
? NpmViewData
: T extends typeof PUBLISH
? NpmPublishData
: unknown;
export interface NpmViewData {
"dist-tags": Record<string, string>;
versions: string[];
}
export interface NpmPublishData {
id: string;
files: { path: string; size: number }[];
}
export const VIEW = "view";
export const PUBLISH = "publish";
export const E404 = "E404";
export const EPUBLISHCONFLICT = "EPUBLISHCONFLICT";
const IS_WINDOWS = os.platform() === "win32";
const NPM = IS_WINDOWS ? "npm.cmd" : "npm";
const JSON_MATCH_RE = /(\{[\s\S]*\})/mu;
const baseArguments = (options: NpmCliOptions) =>
options.ignoreScripts ? ["--ignore-scripts", "--json"] : ["--json"];
/**
* Call the NPM CLI in JSON mode.
*
* @param command The command of the NPM CLI to call
* @param cliArguments Any arguments to send to the command
* @param options Customize environment variables or add an error handler.
* @returns The parsed JSON, or stdout if unparsable.
*/
export async function callNpmCli<CommandT extends string>(
command: CommandT,
cliArguments: string[],
options: NpmCliOptions
): Promise<NpmCallResult<CommandT>> {
const { stdout, stderr, exitCode } = await execNpm(
[command, ...baseArguments(options), ...cliArguments],
options.environment,
options.logger
);
let successData;
let errorCode;
let error;
if (exitCode === 0) {
successData = parseJson<SuccessData<CommandT>>(stdout);
} else {
const errorPayload = parseJson<{ error?: { code?: unknown } }>(
stdout,
stderr
);
if (errorPayload?.error?.code) {
errorCode = String(errorPayload.error.code).toUpperCase();
}
error = new errors.NpmCallError(command, exitCode, stderr);
}
return { successData, errorCode, error };
}
/**
* Execute the npm CLI.
*
* @param commandArguments Npm subcommand and arguments.
* @param environment Environment variables.
* @param logger Optional logger.
* @returns Stdout, stderr, and the exit code.
*/
async function execNpm(
commandArguments: string[],
environment: Record<string, string>,
logger?: Logger
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
logger?.debug?.(`Running command: ${NPM} ${commandArguments.join(" ")}`);
return new Promise((resolve) => {
let stdout = "";
let stderr = "";
const npm = childProcess.spawn(NPM, commandArguments, {
env: { ...process.env, ...environment },
shell: IS_WINDOWS,
});
npm.stdout.on("data", (data: string) => (stdout += data));
npm.stderr.on("data", (data: string) => (stderr += data));
npm.on("close", (code) => {
logger?.debug?.(`Received stdout: ${stdout}`);
logger?.debug?.(`Received stderr: ${stderr}`);
resolve({
stdout: stdout.trim(),
stderr: stderr.trim(),
exitCode: code ?? 0,
});
});
});
}
/**
* Parse CLI outputs for JSON data.
*
* Certain versions of the npm CLI may intersperse JSON with human-readable
* output, which this function accounts for.
*
* @param values CLI outputs to check
* @returns Parsed JSON, if able to parse.
*/
function parseJson<TParsed>(...values: string[]): TParsed | undefined {
for (const value of values) {
const jsonValue = JSON_MATCH_RE.exec(value)?.[1];
if (jsonValue) {
try {
return JSON.parse(jsonValue) as TParsed;
} catch {
return undefined;
}
}
}
return undefined;
}