@xpack/xpm-lib 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +223 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/chmod-recursive.d.ts +7 -0
- package/dist/lib/chmod-recursive.d.ts.map +1 -0
- package/dist/lib/chmod-recursive.js +81 -0
- package/dist/lib/chmod-recursive.js.map +1 -0
- package/dist/lib/errors.d.ts +11 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +26 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/functions/chmod-recursive.d.ts +7 -0
- package/dist/lib/functions/chmod-recursive.d.ts.map +1 -0
- package/dist/lib/functions/chmod-recursive.js +81 -0
- package/dist/lib/functions/chmod-recursive.js.map +1 -0
- package/dist/lib/functions/perform-substitutions.d.ts +20 -0
- package/dist/lib/functions/perform-substitutions.d.ts.map +1 -0
- package/dist/lib/functions/perform-substitutions.js +85 -0
- package/dist/lib/functions/perform-substitutions.js.map +1 -0
- package/dist/lib/functions/utils.d.ts +30 -0
- package/dist/lib/functions/utils.d.ts.map +1 -0
- package/dist/lib/functions/utils.js +70 -0
- package/dist/lib/functions/utils.js.map +1 -0
- package/dist/lib/init-template-base.d.ts +46 -0
- package/dist/lib/init-template-base.d.ts.map +1 -0
- package/dist/lib/init-template-base.js +275 -0
- package/dist/lib/init-template-base.js.map +1 -0
- package/dist/lib/liquid-actions.d.ts +32 -0
- package/dist/lib/liquid-actions.d.ts.map +1 -0
- package/dist/lib/liquid-actions.js +113 -0
- package/dist/lib/liquid-actions.js.map +1 -0
- package/dist/lib/liquid-build-configurations.d.ts +49 -0
- package/dist/lib/liquid-build-configurations.d.ts.map +1 -0
- package/dist/lib/liquid-build-configurations.js +267 -0
- package/dist/lib/liquid-build-configurations.js.map +1 -0
- package/dist/lib/liquid-drop.d.ts +13 -0
- package/dist/lib/liquid-drop.d.ts.map +1 -0
- package/dist/lib/liquid-drop.js +56 -0
- package/dist/lib/liquid-drop.js.map +1 -0
- package/dist/lib/liquid-engine.d.ts +5 -0
- package/dist/lib/liquid-engine.d.ts.map +1 -0
- package/dist/lib/liquid-engine.js +85 -0
- package/dist/lib/liquid-engine.js.map +1 -0
- package/dist/lib/liquid-package.d.ts +17 -0
- package/dist/lib/liquid-package.d.ts.map +1 -0
- package/dist/lib/liquid-package.js +70 -0
- package/dist/lib/liquid-package.js.map +1 -0
- package/dist/lib/package.d.ts +66 -0
- package/dist/lib/package.d.ts.map +1 -0
- package/dist/lib/package.js +700 -0
- package/dist/lib/package.js.map +1 -0
- package/dist/lib/perform-substitutions.d.ts +20 -0
- package/dist/lib/perform-substitutions.d.ts.map +1 -0
- package/dist/lib/perform-substitutions.js +85 -0
- package/dist/lib/perform-substitutions.js.map +1 -0
- package/dist/lib/policies.d.ts +13 -0
- package/dist/lib/policies.d.ts.map +1 -0
- package/dist/lib/policies.js +31 -0
- package/dist/lib/policies.js.map +1 -0
- package/dist/lib/substitutions-variables.d.ts +117 -0
- package/dist/lib/substitutions-variables.d.ts.map +1 -0
- package/dist/lib/substitutions-variables.js +51 -0
- package/dist/lib/substitutions-variables.js.map +1 -0
- package/dist/lib/types.d.ts +70 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +13 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/lib/utils.d.ts +30 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +70 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +102 -0
- package/src/README.md +10 -0
- package/src/index.ts +35 -0
- package/src/lib/errors.ts +29 -0
- package/src/lib/functions/chmod-recursive.ts +103 -0
- package/src/lib/functions/perform-substitutions.ts +116 -0
- package/src/lib/functions/utils.ts +88 -0
- package/src/lib/init-template-base.ts +401 -0
- package/src/lib/liquid-actions.ts +179 -0
- package/src/lib/liquid-build-configurations.ts +410 -0
- package/src/lib/liquid-drop.ts +99 -0
- package/src/lib/liquid-engine.ts +135 -0
- package/src/lib/liquid-package.ts +108 -0
- package/src/lib/package.ts +946 -0
- package/src/lib/policies.ts +49 -0
- package/src/lib/substitutions-variables.ts +177 -0
- package/src/lib/types.ts +109 -0
- package/src/package.json +3 -0
- package/src/tsconfig.json +10 -0
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the xPack project (http://xpack.github.io).
|
|
3
|
+
* Copyright (c) 2021-2026 Liviu Ionescu. All rights reserved.
|
|
4
|
+
*
|
|
5
|
+
* Permission to use, copy, modify, and/or distribute this software
|
|
6
|
+
* for any purpose is hereby granted, under the terms of the MIT license.
|
|
7
|
+
*
|
|
8
|
+
* If a copy of the license was not distributed with this file, it can
|
|
9
|
+
* be obtained from https://opensource.org/license/mit.
|
|
10
|
+
*/
|
|
11
|
+
var _a;
|
|
12
|
+
/* eslint max-len: [ "error", 80, { "ignoreUrls": true } ] */
|
|
13
|
+
// ----------------------------------------------------------------------------
|
|
14
|
+
import assert from 'node:assert';
|
|
15
|
+
import * as fs from 'node:fs/promises';
|
|
16
|
+
import * as os from 'node:os';
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
import util from 'node:util';
|
|
19
|
+
import stream from 'node:stream';
|
|
20
|
+
// https://www.npmjs.com/package/@npmcli/arborist
|
|
21
|
+
import { Arborist } from '@npmcli/arborist';
|
|
22
|
+
// https://www.npmjs.com/package/pacote
|
|
23
|
+
import pacote from 'pacote';
|
|
24
|
+
// https://www.npmjs.com/package/cacache
|
|
25
|
+
import cacache from 'cacache';
|
|
26
|
+
// https://www.npmjs.com/package/decompress
|
|
27
|
+
import decompress from 'decompress';
|
|
28
|
+
// https://www.npmjs.com/package/semver
|
|
29
|
+
import semver from 'semver';
|
|
30
|
+
// https://www.npmjs.com/package/del
|
|
31
|
+
import { deleteAsync } from 'del';
|
|
32
|
+
// https://www.npmjs.com/package/proxy-from-env
|
|
33
|
+
import { getProxyForUrl } from 'proxy-from-env';
|
|
34
|
+
// https://www.npmjs.com/package/https-proxy-agent
|
|
35
|
+
import { HttpsProxyAgent } from 'https-proxy-agent';
|
|
36
|
+
// https://www.npmjs.com/package/node-fetch
|
|
37
|
+
import fetch from 'node-fetch';
|
|
38
|
+
import { chmodRecursive } from './functions/chmod-recursive.js';
|
|
39
|
+
import { XpmError, XpmInputError, XpmPrerequisitesError } from './errors.js';
|
|
40
|
+
export class XpmPackage {
|
|
41
|
+
// --------------------------------------------------------------------------
|
|
42
|
+
// Members.
|
|
43
|
+
packageFolderPath;
|
|
44
|
+
jsonPackage;
|
|
45
|
+
#log;
|
|
46
|
+
// --------------------------------------------------------------------------
|
|
47
|
+
// Constructor.
|
|
48
|
+
constructor({ log, packageFolderPath, }) {
|
|
49
|
+
this.#log = log;
|
|
50
|
+
this.packageFolderPath = packageFolderPath;
|
|
51
|
+
log.trace(`${_a.name}(${packageFolderPath})`);
|
|
52
|
+
}
|
|
53
|
+
// --------------------------------------------------------------------------
|
|
54
|
+
// Methods.
|
|
55
|
+
async readPackageDotJson({ withThrow = false, } = {}) {
|
|
56
|
+
const jsonFilePath = path.join(this.packageFolderPath, 'package.json');
|
|
57
|
+
let fileContent;
|
|
58
|
+
try {
|
|
59
|
+
fileContent = await fs.readFile(jsonFilePath);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
if (withThrow) {
|
|
63
|
+
if (err instanceof Error) {
|
|
64
|
+
this.#log.trace(err.message);
|
|
65
|
+
}
|
|
66
|
+
throw new XpmInputError(`no package.json in folder ‘${this.packageFolderPath}’`);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
this.jsonPackage = JSON.parse(fileContent.toString());
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
if (withThrow) {
|
|
77
|
+
this.jsonPackage = undefined;
|
|
78
|
+
if (err instanceof Error) {
|
|
79
|
+
this.#log.trace(err.message);
|
|
80
|
+
}
|
|
81
|
+
throw new XpmInputError(`invalid package.json in folder ‘${this.packageFolderPath}’`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return this.jsonPackage;
|
|
88
|
+
}
|
|
89
|
+
// Note: the json is explicitly passed.
|
|
90
|
+
async rewritePackageDotJson(jsonPackage) {
|
|
91
|
+
const log = this.#log;
|
|
92
|
+
assert(jsonPackage);
|
|
93
|
+
const jsonString = JSON.stringify(jsonPackage, null, 2) + '\n';
|
|
94
|
+
const jsonFilePath = path.join(this.packageFolderPath, 'package.json');
|
|
95
|
+
log.trace(`write filePath: '${jsonFilePath}'`);
|
|
96
|
+
await fs.writeFile(jsonFilePath, jsonString);
|
|
97
|
+
}
|
|
98
|
+
isNpmPackage() {
|
|
99
|
+
const jsonPackage = this.jsonPackage;
|
|
100
|
+
if (jsonPackage?.name === undefined || jsonPackage.version === undefined) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
const name = jsonPackage.name.trim();
|
|
104
|
+
if (name.length === 0) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
const version = jsonPackage.version.trim();
|
|
108
|
+
if (version.length === 0) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
isXpmPackage() {
|
|
114
|
+
const jsonPackage = this.jsonPackage;
|
|
115
|
+
if (!this.isNpmPackage()) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
if (jsonPackage?.xpack === undefined) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
// Binary packages must have both executables and binaries, but
|
|
124
|
+
// the presence of one implies the other, so validate.
|
|
125
|
+
isBinaryXpmPackage() {
|
|
126
|
+
const jsonPackage = this.jsonPackage;
|
|
127
|
+
if (!this.isXpmPackage()) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
// Since Nov. 2024, `executables` is preferred to `bin`.
|
|
131
|
+
if (jsonPackage?.xpack.executables ?? jsonPackage?.xpack.bin) {
|
|
132
|
+
if (!jsonPackage.xpack.binaries) {
|
|
133
|
+
throw new XpmInputError("doesn't look like a proper binary xpm package, " +
|
|
134
|
+
'package.json has no "xpack.binaries"');
|
|
135
|
+
}
|
|
136
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
137
|
+
if (!jsonPackage.xpack.binaries.platforms) {
|
|
138
|
+
throw new XpmInputError("doesn't look like a proper binary xpm package, " +
|
|
139
|
+
'package.json has no "xpack.binaries.platforms"');
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
if (jsonPackage?.xpack.binaries) {
|
|
144
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
145
|
+
if (!jsonPackage.xpack.binaries.platforms) {
|
|
146
|
+
throw new XpmInputError("doesn't look like a proper binary xpm package, " +
|
|
147
|
+
'package.json has no "xpack.binaries.platforms"');
|
|
148
|
+
}
|
|
149
|
+
if (!(jsonPackage.xpack.executables ?? jsonPackage.xpack.bin)) {
|
|
150
|
+
throw new XpmInputError("doesn't look like a proper binary xpm package, " +
|
|
151
|
+
'package.json has no "xpack.executables"');
|
|
152
|
+
}
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
isNodeModule() {
|
|
158
|
+
const jsonPackage = this.jsonPackage;
|
|
159
|
+
return !!jsonPackage && !jsonPackage.xpack;
|
|
160
|
+
}
|
|
161
|
+
isBinaryNodeModule() {
|
|
162
|
+
const jsonPackage = this.jsonPackage;
|
|
163
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
164
|
+
return this.isNodeModule() && !!jsonPackage?.bin;
|
|
165
|
+
}
|
|
166
|
+
hasNpmScripts() {
|
|
167
|
+
const jsonPackage = this.jsonPackage;
|
|
168
|
+
if (jsonPackage?.scripts !== undefined &&
|
|
169
|
+
Object.keys(jsonPackage.scripts).length > 0) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
hasXpmActions() {
|
|
175
|
+
const json = this.jsonPackage;
|
|
176
|
+
if (!this.isXpmPackage()) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
if (json?.xpack.actions !== undefined &&
|
|
181
|
+
Object.keys(json.xpack.actions).length > 0) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
if (json?.xpack.buildConfigurations !== undefined &&
|
|
185
|
+
Object.keys(json.xpack.buildConfigurations).length > 0) {
|
|
186
|
+
// Don't use a lambda, to return directly from the loop.
|
|
187
|
+
for (const name of Object.keys(json.xpack.buildConfigurations)) {
|
|
188
|
+
const buildConfiguration = json.xpack.buildConfigurations[name];
|
|
189
|
+
if (buildConfiguration.actions !== undefined &&
|
|
190
|
+
Object.keys(buildConfiguration.actions).length > 0) {
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
// In case xpack is not an option to get its properties.
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
getMinimumXpmRequired() {
|
|
203
|
+
const log = this.#log;
|
|
204
|
+
const jsonPackage = this.jsonPackage;
|
|
205
|
+
log.trace(`${_a.name}.getMinimumXpmRequired()`);
|
|
206
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
207
|
+
const version = jsonPackage?.xpack?.minimumXpmRequired;
|
|
208
|
+
if (version === undefined) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
// Remove the pre-release part.
|
|
212
|
+
return version.replace(/-.*$/, '');
|
|
213
|
+
}
|
|
214
|
+
async checkMinimumXpmRequired({ xpmRootFolderPath, }) {
|
|
215
|
+
const log = this.#log;
|
|
216
|
+
const jsonPackage = this.jsonPackage;
|
|
217
|
+
log.trace(`${_a.name}.checkMinimumXpmRequired()`);
|
|
218
|
+
if (!jsonPackage) {
|
|
219
|
+
// Not in a package.
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
if (!this.isXpmPackage() || !jsonPackage.xpack.minimumXpmRequired) {
|
|
223
|
+
log.trace('minimumXpmRequired not used, no checks');
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
// Remove the pre-release part.
|
|
227
|
+
const cleanedVersion = semver.clean(jsonPackage.xpack.minimumXpmRequired.replace(/-.*$/, ''));
|
|
228
|
+
if (!cleanedVersion) {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
const minimumXpmRequired = cleanedVersion;
|
|
232
|
+
log.trace(`minimumXpmRequired: ${minimumXpmRequired}`);
|
|
233
|
+
let jsonXpmCliPackage;
|
|
234
|
+
try {
|
|
235
|
+
const cliXpmPackage = new _a({
|
|
236
|
+
log,
|
|
237
|
+
packageFolderPath: xpmRootFolderPath,
|
|
238
|
+
});
|
|
239
|
+
jsonXpmCliPackage = await cliXpmPackage.readPackageDotJson({
|
|
240
|
+
withThrow: true,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
if (err instanceof Error) {
|
|
245
|
+
log.trace(err.message);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
log.trace(err);
|
|
249
|
+
}
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
assert(jsonXpmCliPackage);
|
|
253
|
+
log.trace(jsonXpmCliPackage.version);
|
|
254
|
+
if (!jsonXpmCliPackage.version) {
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
// Remove the pre-release part.
|
|
258
|
+
const xpmVersion = semver.clean(jsonXpmCliPackage.version.replace(/-.*$/, ''));
|
|
259
|
+
if (!xpmVersion) {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
if (semver.lt(xpmVersion, minimumXpmRequired)) {
|
|
263
|
+
throw new XpmPrerequisitesError('package ' +
|
|
264
|
+
(jsonPackage.name ? `'${jsonPackage.name}' ` : '') +
|
|
265
|
+
`requires xpm v${minimumXpmRequired} or later, please upgrade`);
|
|
266
|
+
}
|
|
267
|
+
// Check passed.
|
|
268
|
+
return minimumXpmRequired;
|
|
269
|
+
}
|
|
270
|
+
parsePackageSpecifier({ npmPackageSpecifier, }) {
|
|
271
|
+
assert(npmPackageSpecifier);
|
|
272
|
+
const log = this.#log;
|
|
273
|
+
let scope;
|
|
274
|
+
let name;
|
|
275
|
+
let version;
|
|
276
|
+
if (npmPackageSpecifier.startsWith('@')) {
|
|
277
|
+
const arr = npmPackageSpecifier.split('/');
|
|
278
|
+
if (arr.length > 2) {
|
|
279
|
+
throw new XpmInputError(`'${npmPackageSpecifier}' not a package name`);
|
|
280
|
+
}
|
|
281
|
+
scope = arr[0];
|
|
282
|
+
if (arr.length > 1) {
|
|
283
|
+
const arr2 = arr[1].split('@');
|
|
284
|
+
name = arr2[0];
|
|
285
|
+
if (arr2.length > 1) {
|
|
286
|
+
version = arr2[1];
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
const arr2 = npmPackageSpecifier.split('@');
|
|
292
|
+
name = arr2[0];
|
|
293
|
+
if (arr2.length > 1) {
|
|
294
|
+
version = arr2[1];
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
log.trace(`${npmPackageSpecifier} => ` +
|
|
298
|
+
`${scope ?? '?'} ${name ?? '?'} ${version ?? '?'}`);
|
|
299
|
+
return { scope, name, version };
|
|
300
|
+
}
|
|
301
|
+
getPlatformKey({ doForce32bit = false, } = {}) {
|
|
302
|
+
const log = this.#log;
|
|
303
|
+
const platform = process.platform;
|
|
304
|
+
let arch = process.arch;
|
|
305
|
+
if (doForce32bit) {
|
|
306
|
+
if (platform === 'win32' && arch === 'x64') {
|
|
307
|
+
arch = 'ia32';
|
|
308
|
+
}
|
|
309
|
+
else if (platform === 'linux' && arch === 'x64') {
|
|
310
|
+
arch = 'ia32';
|
|
311
|
+
}
|
|
312
|
+
else if (platform === 'linux' && arch === 'arm64') {
|
|
313
|
+
arch = 'arm';
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const key = `${platform}-${arch}`;
|
|
317
|
+
log.trace(`platform key: ${key}`);
|
|
318
|
+
return key;
|
|
319
|
+
}
|
|
320
|
+
async pacoteCreateManifest({ specifier, cacheFolderPath, }) {
|
|
321
|
+
const log = this.#log;
|
|
322
|
+
log.trace(`${_a.name}.pacoteCreateManifest('${specifier}')`);
|
|
323
|
+
const manifest = await pacote.manifest(specifier, {
|
|
324
|
+
cache: cacheFolderPath,
|
|
325
|
+
});
|
|
326
|
+
return manifest;
|
|
327
|
+
}
|
|
328
|
+
async pacoteExtractPackage({ packFullName, specifier, destinationFolderPath, cacheFolderPath, setReadOnly, verboseMessage, config, policies, }) {
|
|
329
|
+
assert(packFullName);
|
|
330
|
+
assert(specifier);
|
|
331
|
+
assert(destinationFolderPath);
|
|
332
|
+
assert(cacheFolderPath);
|
|
333
|
+
assert(verboseMessage);
|
|
334
|
+
assert(config);
|
|
335
|
+
assert(policies);
|
|
336
|
+
const log = this.#log;
|
|
337
|
+
log.trace(`${_a.name}.pacoteExtractContent('${specifier}')`);
|
|
338
|
+
let destinationXpmPackage = new _a({
|
|
339
|
+
log,
|
|
340
|
+
packageFolderPath: destinationFolderPath,
|
|
341
|
+
});
|
|
342
|
+
const jsonDestination = await destinationXpmPackage.readPackageDotJson();
|
|
343
|
+
if (jsonDestination) {
|
|
344
|
+
// The package is already present in the destination folder.
|
|
345
|
+
if (!config.doForce) {
|
|
346
|
+
if (!config.doSkipIfInstalled) {
|
|
347
|
+
log.warn(`package ${packFullName} already installed, ` +
|
|
348
|
+
'use --force to overwrite');
|
|
349
|
+
}
|
|
350
|
+
return; // Not an error, proceed to other packages.
|
|
351
|
+
}
|
|
352
|
+
if (setReadOnly) {
|
|
353
|
+
if (config.isDryRun) {
|
|
354
|
+
log.verbose('Pretend changing permissions to read-write...');
|
|
355
|
+
log.verbose('Pretend removing existing package from ' +
|
|
356
|
+
`'${destinationFolderPath}'...`);
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
log.verbose('Changing permissions to read-write...');
|
|
360
|
+
await chmodRecursive({
|
|
361
|
+
inputPath: destinationFolderPath,
|
|
362
|
+
readOnly: false,
|
|
363
|
+
log,
|
|
364
|
+
});
|
|
365
|
+
log.verbose(`Removing existing package from '${destinationFolderPath}'...`);
|
|
366
|
+
await deleteAsync(destinationFolderPath, { force: true });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const destinationTmpFolderPath = destinationFolderPath + '.tmp';
|
|
371
|
+
log.trace(`del(${destinationTmpFolderPath})`);
|
|
372
|
+
await deleteAsync(destinationTmpFolderPath, { force: true });
|
|
373
|
+
if (log.isVerbose && verboseMessage) {
|
|
374
|
+
log.verbose(verboseMessage);
|
|
375
|
+
}
|
|
376
|
+
if (config.isDryRun) {
|
|
377
|
+
if (!log.isVerbose) {
|
|
378
|
+
log.info(`${packFullName} => '${destinationFolderPath}' (dry run)`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
await this.pacoteExtract({
|
|
383
|
+
specifier: specifier,
|
|
384
|
+
destinationFolderPath: destinationTmpFolderPath,
|
|
385
|
+
cacheFolderPath,
|
|
386
|
+
});
|
|
387
|
+
if (!log.isVerbose) {
|
|
388
|
+
log.info(`${packFullName} => '${destinationFolderPath}'`);
|
|
389
|
+
}
|
|
390
|
+
destinationXpmPackage = new _a({
|
|
391
|
+
log,
|
|
392
|
+
packageFolderPath: destinationTmpFolderPath,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
await destinationXpmPackage.readPackageDotJson();
|
|
396
|
+
if (!destinationXpmPackage.isXpmPackage()) {
|
|
397
|
+
if (!policies.shareNpmDependencies) {
|
|
398
|
+
log.trace(`del(${destinationTmpFolderPath})`);
|
|
399
|
+
await deleteAsync(destinationTmpFolderPath, { force: true });
|
|
400
|
+
throw new XpmInputError(`${packFullName} is not an xpm package, use npm to install it`);
|
|
401
|
+
}
|
|
402
|
+
log.debug(`'${destinationFolderPath}' doesn't look like an ` +
|
|
403
|
+
'xpm package, package.json has no "xpack"');
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (config.isDryRun) {
|
|
407
|
+
if (setReadOnly) {
|
|
408
|
+
log.verbose('Pretend changing permissions to read-only...');
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
await this.#downloadBinaries({
|
|
413
|
+
destinationXpmPackage,
|
|
414
|
+
destinationFolderPath,
|
|
415
|
+
cacheFolderPath,
|
|
416
|
+
config,
|
|
417
|
+
});
|
|
418
|
+
// When everything is ready, rename the folder to the desired name.
|
|
419
|
+
await fs.rename(destinationTmpFolderPath, destinationFolderPath);
|
|
420
|
+
log.trace(`rename(${destinationTmpFolderPath}, ${destinationFolderPath})`);
|
|
421
|
+
log.trace(`in '${destinationFolderPath}'`);
|
|
422
|
+
if (setReadOnly) {
|
|
423
|
+
log.verbose('Changing permissions to read-only...');
|
|
424
|
+
await chmodRecursive({
|
|
425
|
+
inputPath: destinationFolderPath,
|
|
426
|
+
readOnly: true,
|
|
427
|
+
log,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
async pacoteExtract({ specifier, destinationFolderPath, cacheFolderPath, }) {
|
|
433
|
+
assert(specifier);
|
|
434
|
+
assert(destinationFolderPath);
|
|
435
|
+
assert(cacheFolderPath);
|
|
436
|
+
const log = this.#log;
|
|
437
|
+
log.trace(`${_a.name}.pacoteExtract(${specifier})`);
|
|
438
|
+
try {
|
|
439
|
+
log.trace(`pacote.extract(${specifier})`);
|
|
440
|
+
const fetchResult = await pacote.extract(specifier, destinationFolderPath, { cache: cacheFolderPath, Arborist });
|
|
441
|
+
log.trace(`fetchResult: ${util.inspect(fetchResult)}`);
|
|
442
|
+
}
|
|
443
|
+
catch (err) {
|
|
444
|
+
log.trace(util.inspect(err));
|
|
445
|
+
throw new XpmInputError(`Package ${specifier} not found`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
async #downloadBinaries({ destinationXpmPackage, destinationFolderPath, cacheFolderPath, config, }) {
|
|
449
|
+
assert(destinationXpmPackage);
|
|
450
|
+
assert(destinationFolderPath);
|
|
451
|
+
assert(cacheFolderPath);
|
|
452
|
+
assert(config);
|
|
453
|
+
const log = this.#log;
|
|
454
|
+
const packageFolderPath = destinationXpmPackage.packageFolderPath;
|
|
455
|
+
const jsonPackage = destinationXpmPackage.jsonPackage;
|
|
456
|
+
assert(jsonPackage);
|
|
457
|
+
log.trace(`${_a.name}.downloadBinaries(${packageFolderPath})`);
|
|
458
|
+
if (!destinationXpmPackage.isXpmPackage()) {
|
|
459
|
+
log.debug("doesn't look like an xpm package, " + 'package.json has no "xpack"');
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (!destinationXpmPackage.isBinaryXpmPackage()) {
|
|
463
|
+
log.debug("doesn't look like an xpm package, " +
|
|
464
|
+
'package.json has no "xpack.executables" and "xpack.binaries"');
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const platformKey = this.getPlatformKey();
|
|
468
|
+
const platformKeyAliases = new Set();
|
|
469
|
+
if (['linux-x32', 'linux-x86', 'linux-ia32'].includes(platformKey)) {
|
|
470
|
+
platformKeyAliases.add('linux-x32');
|
|
471
|
+
platformKeyAliases.add('linux-x86');
|
|
472
|
+
platformKeyAliases.add('linux-ia32'); // official
|
|
473
|
+
}
|
|
474
|
+
else if (['win32-x32', 'win32-x86', 'win32-ia32'].includes(platformKey)) {
|
|
475
|
+
platformKeyAliases.add('win32-x32');
|
|
476
|
+
platformKeyAliases.add('win32-x86');
|
|
477
|
+
platformKeyAliases.add('win32-ia32'); // official
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
platformKeyAliases.add(platformKey);
|
|
481
|
+
}
|
|
482
|
+
assert(jsonPackage.xpack.binaries);
|
|
483
|
+
const platforms = jsonPackage.xpack.binaries.platforms;
|
|
484
|
+
let platform;
|
|
485
|
+
for (const item of platformKeyAliases) {
|
|
486
|
+
if (Object.prototype.hasOwnProperty.call(platforms, item)) {
|
|
487
|
+
platform = platforms[item];
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (!platform) {
|
|
492
|
+
throw new XpmInputError(`platform ${platformKey} not supported`);
|
|
493
|
+
}
|
|
494
|
+
if (!jsonPackage.xpack.binaries.baseUrl) {
|
|
495
|
+
throw new XpmInputError('missing "xpack.binaries.baseUrl" in package.json');
|
|
496
|
+
}
|
|
497
|
+
if (platform.skip) {
|
|
498
|
+
log.warn('no binaries are available for this platform, command ignored');
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (!platform.fileName) {
|
|
502
|
+
throw new XpmInputError(`missing xpack.binaries.platform[${platformKey}].fileName`);
|
|
503
|
+
}
|
|
504
|
+
// Prefer the platform specific URL, if available, otherwise
|
|
505
|
+
// use the common URL.
|
|
506
|
+
let fileUrl = platform.baseUrl ?? jsonPackage.xpack.binaries.baseUrl;
|
|
507
|
+
if (!fileUrl.endsWith('/')) {
|
|
508
|
+
fileUrl += '/';
|
|
509
|
+
}
|
|
510
|
+
fileUrl += platform.fileName;
|
|
511
|
+
let hashAlgorithm = '?';
|
|
512
|
+
let hexSum = '?';
|
|
513
|
+
if (platform.sha256) {
|
|
514
|
+
hashAlgorithm = 'sha256';
|
|
515
|
+
hexSum = platform.sha256;
|
|
516
|
+
}
|
|
517
|
+
else if (platform.sha512) {
|
|
518
|
+
hashAlgorithm = 'sha512';
|
|
519
|
+
hexSum = platform.sha512;
|
|
520
|
+
}
|
|
521
|
+
let integrityDigest = '?';
|
|
522
|
+
if (hexSum) {
|
|
523
|
+
const buff = Buffer.from(hexSum, 'hex');
|
|
524
|
+
integrityDigest = `${hashAlgorithm}-${buff.toString('base64')}`;
|
|
525
|
+
}
|
|
526
|
+
log.trace(`expected integrity digest ${integrityDigest} for ${hexSum}`);
|
|
527
|
+
if (config.isDryRun) {
|
|
528
|
+
log.info(`Pretend downloading ${fileUrl}...`);
|
|
529
|
+
log.info(`Pretend extracting '${platform.fileName}'...`);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const cacheKey = `xpm:binaries:${platform.fileName}`;
|
|
533
|
+
log.trace(`getting cacache info(${cacheFolderPath}, ${cacheKey})...`);
|
|
534
|
+
// Debug only, to force the downloads.
|
|
535
|
+
// await cacache.rm.entry(cacheFolderPath, cacheKey)
|
|
536
|
+
let cacheInfo = await cacache.get.info(cacheFolderPath, cacheKey);
|
|
537
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
538
|
+
if (!cacheInfo) {
|
|
539
|
+
// If the cache has no idea of the desired file, proceed with
|
|
540
|
+
// the download.
|
|
541
|
+
log.info(`Downloading ${fileUrl}...`);
|
|
542
|
+
const opts = {};
|
|
543
|
+
if (integrityDigest) {
|
|
544
|
+
// Enable hash checking.
|
|
545
|
+
opts.integrity = integrityDigest;
|
|
546
|
+
}
|
|
547
|
+
try {
|
|
548
|
+
await this.cacheArchive({
|
|
549
|
+
url: fileUrl,
|
|
550
|
+
cacheFolderPath,
|
|
551
|
+
key: cacheKey,
|
|
552
|
+
opts,
|
|
553
|
+
});
|
|
554
|
+
log.trace(`cache written for ${fileUrl}`);
|
|
555
|
+
}
|
|
556
|
+
catch (err) {
|
|
557
|
+
log.trace(util.inspect(err));
|
|
558
|
+
// Do not throw yet, only display the error.
|
|
559
|
+
if (err instanceof Error) {
|
|
560
|
+
log.info(err.message);
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
log.info(String(err));
|
|
564
|
+
}
|
|
565
|
+
if (os.platform() === 'win32') {
|
|
566
|
+
log.info('If you have an aggressive antivirus, try to ' +
|
|
567
|
+
'reconfigure it, or temporarily disable it');
|
|
568
|
+
}
|
|
569
|
+
throw new XpmError('download failed, quit');
|
|
570
|
+
}
|
|
571
|
+
// Update the cache info after downloading the file.
|
|
572
|
+
cacheInfo = await cacache.get.info(cacheFolderPath, cacheKey);
|
|
573
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
574
|
+
if (!cacheInfo) {
|
|
575
|
+
throw new XpmError('download failed, quit');
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
log.trace(`cache path ${cacheInfo.path} for ${fileUrl}`);
|
|
579
|
+
// The number of initial folder levels to skip.
|
|
580
|
+
let skip = 0;
|
|
581
|
+
if (jsonPackage.xpack.binaries.skip) {
|
|
582
|
+
try {
|
|
583
|
+
skip = jsonPackage.xpack.binaries.skip;
|
|
584
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
585
|
+
}
|
|
586
|
+
catch (err) {
|
|
587
|
+
// Ignore invalid skip value, use default
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
log.trace(`skip ${skip.toString()} levels`);
|
|
591
|
+
const contentFolderRelativePath = jsonPackage.xpack.binaries.destination || '.content';
|
|
592
|
+
const contentFolderPath = path.join(packageFolderPath, contentFolderRelativePath);
|
|
593
|
+
const destinationContentFolderPath = path.join(destinationFolderPath, contentFolderRelativePath);
|
|
594
|
+
log.trace(`del ${contentFolderPath}`);
|
|
595
|
+
await deleteAsync(contentFolderPath, { force: true });
|
|
596
|
+
const cacheInfoPath = cacheInfo.path;
|
|
597
|
+
log.trace(`cacheInfoPath ${cacheInfoPath}`);
|
|
598
|
+
let res = [];
|
|
599
|
+
// Currently this includes decompressTar(), decompressTarbz2(),
|
|
600
|
+
// decompressTargz(), decompressUnzip().
|
|
601
|
+
log.info(`Extracting '${platform.fileName}'...`);
|
|
602
|
+
res = await decompress(cacheInfoPath, contentFolderPath, {
|
|
603
|
+
strip: skip,
|
|
604
|
+
});
|
|
605
|
+
if (log.isVerbose) {
|
|
606
|
+
// The common value is self relative ./.content; remove the folder.
|
|
607
|
+
const shownFolderRelativePath = contentFolderRelativePath.replace(/^\.\//, '');
|
|
608
|
+
assert(jsonPackage.version);
|
|
609
|
+
log.verbose(`${res.length.toString()} files extracted in ` +
|
|
610
|
+
`'${jsonPackage.version}/${shownFolderRelativePath}'`);
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
log.info(`${res.length.toString()} files => '${destinationContentFolderPath}'`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
// Returns nothing. Used by downloadBinaries().
|
|
617
|
+
async cacheArchive({ url, cacheFolderPath, key, opts, }) {
|
|
618
|
+
assert(url);
|
|
619
|
+
assert(cacheFolderPath);
|
|
620
|
+
assert(key);
|
|
621
|
+
assert(opts);
|
|
622
|
+
const log = this.#log;
|
|
623
|
+
// https://github.com/node-fetch/node-fetch/blob/main/docs/ERROR-HANDLING.md
|
|
624
|
+
// https://github.com/node-fetch/node-fetch/blob/main/test/main.js
|
|
625
|
+
// https://www.scrapingbee.com/blog/proxy-node-fetch/
|
|
626
|
+
// https://iproyal.com/blog/how-do-i-use-a-node-fetch-proxy/
|
|
627
|
+
let response;
|
|
628
|
+
let timeoutMillis = 1000;
|
|
629
|
+
// If no proxy is set, an empty string is returned.
|
|
630
|
+
const proxyUrl = getProxyForUrl(url);
|
|
631
|
+
log.trace(`proxyUrl ${proxyUrl}`);
|
|
632
|
+
const maxRetry = 5;
|
|
633
|
+
for (let retry = 0; retry < maxRetry; ++retry) {
|
|
634
|
+
try {
|
|
635
|
+
if (proxyUrl.length > 0) {
|
|
636
|
+
const proxyAgent = new HttpsProxyAgent(proxyUrl);
|
|
637
|
+
log.trace(`proxyAgent ${util.inspect(proxyAgent)} for ${url}`);
|
|
638
|
+
response = await fetch(url, { agent: proxyAgent });
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
response = await fetch(url);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
catch (err) {
|
|
645
|
+
log.trace(util.inspect(err));
|
|
646
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
647
|
+
throw new XpmError(`${errorMessage} in fetch ${url}`);
|
|
648
|
+
}
|
|
649
|
+
log.debug(`fetch.status ${response.status.toString()} ${url}`);
|
|
650
|
+
log.trace(`fetch.statusText ${response.statusText} ${url}`);
|
|
651
|
+
if (!response.ok) {
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
// the HTTP response status was [200, 300).
|
|
655
|
+
// https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#2xx_success
|
|
656
|
+
const pipelinePromise = util.promisify(stream.pipeline);
|
|
657
|
+
log.trace(`create write stream for ${key}`);
|
|
658
|
+
const cacacheWriteStream = cacache.put.stream(cacheFolderPath, key, opts);
|
|
659
|
+
log.trace(`create pipeline for ${key}`);
|
|
660
|
+
try {
|
|
661
|
+
assert(response.body);
|
|
662
|
+
await pipelinePromise(response.body, cacacheWriteStream);
|
|
663
|
+
// If no exception, everything must be ok.
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
catch (err) {
|
|
667
|
+
log.trace(util.inspect(err));
|
|
668
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
669
|
+
if (retry >= maxRetry) {
|
|
670
|
+
throw new XpmError(`${errorMessage} in pipeline ${url}`);
|
|
671
|
+
}
|
|
672
|
+
// For now retry on all errors during download.
|
|
673
|
+
// TODO: identify non recoverable and quit.
|
|
674
|
+
log.warn(`${errorMessage} while downloading ${url}, retrying...`);
|
|
675
|
+
const tenPercent = timeoutMillis * 0.1;
|
|
676
|
+
// +/- 10%
|
|
677
|
+
// Math.random() * (max - min) + min
|
|
678
|
+
const jitter = Math.floor(Math.random() * (tenPercent - -tenPercent) + -tenPercent);
|
|
679
|
+
timeoutMillis = timeoutMillis + jitter;
|
|
680
|
+
log.debug(`timeoutMillis: ${timeoutMillis.toString()}`);
|
|
681
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
682
|
+
await sleep(timeoutMillis);
|
|
683
|
+
// 1 2 4 8 16... seconds
|
|
684
|
+
timeoutMillis = timeoutMillis * 2;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
// res.status < 200 || res.status >= 300 (4xx, 5xx)
|
|
688
|
+
// 1xx informational
|
|
689
|
+
// 3xx: redirection messages
|
|
690
|
+
// 4xx: client error
|
|
691
|
+
// 5xx: server error
|
|
692
|
+
// TODO: detect cases that can be retried.
|
|
693
|
+
assert(response);
|
|
694
|
+
throw new XpmError(`server returned ${response.status.toString()}: ` +
|
|
695
|
+
`${response.statusText} for ${key}`);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
_a = XpmPackage;
|
|
699
|
+
// ----------------------------------------------------------------------------
|
|
700
|
+
//# sourceMappingURL=package.js.map
|