Skip to content

Commit 6b2a7ed

Browse files
committed
feat: add npm api
1 parent 8d8defe commit 6b2a7ed

1 file changed

Lines changed: 243 additions & 0 deletions

File tree

lib/npm.js

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import childProcess from "node:child_process";
2+
import fs from "node:fs";
3+
import env from "#core/env";
4+
import { calculateMode } from "#core/fs";
5+
import GlobPatterns from "#core/glob/patterns";
6+
import stream from "#core/stream";
7+
import * as TarStream from "#core/stream/tar";
8+
import { TmpDir, TmpFile } from "#core/tmp";
9+
import { shellQuote } from "#core/utils";
10+
11+
export default class Npm {
12+
#npm;
13+
#cwd;
14+
#registry;
15+
16+
constructor ( { npm, cwd, registry } = {} ) {
17+
this.#npm = npm || process.platform === "win32"
18+
? "npm.cmd"
19+
: "npm";
20+
21+
this.#cwd = cwd;
22+
this.#registry = registry;
23+
}
24+
25+
// properties
26+
get npm () {
27+
return this.#npm;
28+
}
29+
30+
get cwd () {
31+
return this.#cwd;
32+
}
33+
34+
get registry () {
35+
return this.#registry;
36+
}
37+
38+
// public
39+
async exec ( args, { cwd, registry } = {} ) {
40+
if ( !Array.isArray( args ) ) args = [ args ];
41+
42+
args = [ this.npm, ...args, "--json" ];
43+
44+
registry ||= this.registry;
45+
46+
if ( registry ) {
47+
args.push( "--registry=" + registry );
48+
}
49+
50+
args = shellQuote( args );
51+
52+
if ( this.#cwd ) cwd ??= this.#cwd;
53+
54+
return new Promise( resolve => {
55+
try {
56+
const proc = childProcess.spawn( args, {
57+
cwd,
58+
"shell": true,
59+
"encoding": "buffer",
60+
"stdio": [ "ignore", "pipe", "pipe" ],
61+
} );
62+
63+
const stdout = [],
64+
stderr = [];
65+
66+
proc.stdout.on( "data", data => stdout.push( data ) );
67+
68+
proc.stderr.on( "data", data => stderr.push( data ) );
69+
70+
proc.once( "close", code => {
71+
var res;
72+
73+
if ( code ) {
74+
let data = Buffer.concat( stdout );
75+
data = JSON.parse( data );
76+
77+
res = result( [ 500, data.error.summary ], data );
78+
}
79+
else {
80+
let data = Buffer.concat( stdout );
81+
data = JSON.parse( data );
82+
83+
res = result( 200, data );
84+
}
85+
86+
resolve( res );
87+
} );
88+
}
89+
catch ( e ) {
90+
resolve( result( [ 500, e.message ] ) );
91+
}
92+
} );
93+
}
94+
95+
async getPackageVersions ( packageName, { cwd, registry } = {} ) {
96+
const args = [ "view", packageName, "versions" ];
97+
98+
return this.exec( args, {
99+
cwd,
100+
registry,
101+
} );
102+
}
103+
104+
async getPackageTags ( packageName, { cwd, registry } = {} ) {
105+
const args = [ "view", packageName, "dist-tags" ];
106+
107+
return this.exec( args, {
108+
cwd,
109+
registry,
110+
} );
111+
}
112+
113+
async setPackageTag ( packageName, packageVersion, tag, { cwd, registry } = {} ) {
114+
packageName = `${ packageName }@${ packageVersion }`;
115+
116+
const args = [ "dist-tag", "add", packageName, tag ];
117+
118+
return this.exec( args, {
119+
cwd,
120+
registry,
121+
} );
122+
}
123+
124+
async deletePackageTag ( packageName, tag, { cwd, registry } = {} ) {
125+
const args = [ "dist-tag", "rm", packageName, tag ];
126+
127+
return this.exec( args, {
128+
cwd,
129+
registry,
130+
} );
131+
}
132+
133+
async getPackageAccessStatus ( packageName, { cwd, registry } = {} ) {
134+
const args = [ "access", "get", "status", packageName ];
135+
136+
const res = await this.exec( args, {
137+
cwd,
138+
registry,
139+
} );
140+
141+
if ( res.ok ) {
142+
res.data = res.data[ packageName ];
143+
}
144+
145+
return res;
146+
}
147+
148+
async setPackageAccessStatus ( packageName, privateAccess, { cwd, registry } = {} ) {
149+
const args = [ "access", "set", "status=" + ( privateAccess
150+
? "private"
151+
: "public" ), packageName ];
152+
153+
const res = await this.exec( args, {
154+
cwd,
155+
registry,
156+
} );
157+
158+
if ( res.ok ) {
159+
res.data = res.data[ packageName ];
160+
}
161+
162+
return res;
163+
}
164+
165+
async pack ( { cwd, executablesPatterns } = {} ) {
166+
cwd = env.findPackageRoot( cwd );
167+
if ( !cwd ) return result( [ 500, "Package not found" ] );
168+
169+
const tmpDir = new TmpDir(),
170+
args = [ "pack", "--pack-destination", tmpDir.path ],
171+
res = await this.exec( args, {
172+
cwd,
173+
} );
174+
175+
if ( !res.ok ) return res;
176+
177+
const filename = res.data[ 0 ].filename,
178+
tmpFile = new TmpFile( {
179+
"extname": ".tgz",
180+
} );
181+
182+
executablesPatterns = executablesPatterns
183+
? new GlobPatterns().add( executablesPatterns )
184+
: null;
185+
186+
// fix permissions
187+
await stream.promises.pipeline(
188+
189+
//
190+
fs.createReadStream( tmpDir.path + "/" + filename ),
191+
new TarStream.TarStreamUnpacker(),
192+
new TarStream.TarStreamPacker( {
193+
"gzip": true,
194+
"onWriteEntry": writeEntry => {
195+
if ( executablesPatterns?.test( writeEntry.path.replace( /^package\//, "" ) ) ) {
196+
writeEntry.mode = calculateMode( "rwxr-xr-x" );
197+
}
198+
else {
199+
writeEntry.mode = calculateMode( "rw-r--r--" );
200+
}
201+
},
202+
} ),
203+
fs.createWriteStream( tmpFile.path )
204+
);
205+
206+
return result( 200, {
207+
"pack": tmpFile,
208+
} );
209+
}
210+
211+
async publish ( { executablesPatterns, packPath, "private": privateAccess, tag, cwd, registry } = {} ) {
212+
if ( !packPath ) {
213+
const res = await this.pack( {
214+
executablesPatterns,
215+
cwd,
216+
} );
217+
if ( !res.ok ) return res;
218+
219+
var pack = res.data.pack;
220+
221+
packPath = pack.path;
222+
}
223+
224+
const args = [ "publish" ];
225+
226+
if ( privateAccess != null ) {
227+
args.push( "--access", privateAccess
228+
? "restricted"
229+
: "public" );
230+
}
231+
232+
if ( tag ) {
233+
args.push( "--tag", tag );
234+
}
235+
236+
args.push( packPath );
237+
238+
return this.exec( args, {
239+
cwd,
240+
registry,
241+
} );
242+
}
243+
}

0 commit comments

Comments
 (0)