beez-rp 0.2.0 → 0.3.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/CHANGELOG.md +22 -0
- package/README.md +30 -4
- package/bin/beez-rp.js +10 -1
- package/package.json +7 -2
- package/src/constants/cli.js +1 -0
- package/src/constants/create-version.js +169 -0
- package/src/constants/guard-publish.js +29 -0
- package/src/constants/index.js +1 -0
- package/src/create-version/artifact.js +324 -0
- package/src/create-version/config.js +16 -0
- package/src/create-version/index.js +23 -2
- package/src/create-version/npm.js +359 -22
- package/src/create-version/plan.js +8 -1
- package/src/create-version/process.js +20 -7
- package/src/create-version/run.js +207 -13
- package/src/create-version/state.js +25 -2
- package/src/guard-publish.js +74 -0
- package/src/index.js +2 -0
- package/types/constants/cli.d.ts +1 -0
- package/types/constants/create-version.d.ts +118 -0
- package/types/constants/guard-publish.d.ts +25 -0
- package/types/constants/index.d.ts +1 -0
- package/types/create-version/artifact.d.ts +113 -0
- package/types/create-version/config.d.ts +4 -0
- package/types/create-version/index.d.ts +3 -2
- package/types/create-version/npm.d.ts +133 -8
- package/types/create-version/process.d.ts +10 -1
- package/types/create-version/run.d.ts +2 -0
- package/types/guard-publish.d.ts +48 -0
- package/types/index.d.ts +2 -0
|
@@ -1,43 +1,135 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* npm adapter of `beez-rp create-version`:
|
|
3
|
-
*
|
|
2
|
+
* npm adapter of `beez-rp create-version`: resolves the registry a package is
|
|
3
|
+
* published to (`publishConfig`, else npm's own config), lists the versions
|
|
4
|
+
* published there (authenticated with `NPM_TOKEN` when available), reads the integrity npm
|
|
5
|
+
* would pack from the release checkout, and publishes the working tree or a
|
|
6
|
+
* prepared archive with `NPM_TOKEN` bound to the publish registry.
|
|
4
7
|
*
|
|
5
8
|
* @module create-version/npm
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
|
-
import { existsSync } from "node:fs";
|
|
11
|
+
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
9
13
|
import path from "node:path";
|
|
10
14
|
|
|
11
15
|
import {
|
|
16
|
+
DEFAULT_NPM_REGISTRY_URL,
|
|
12
17
|
LOCAL_ENVIRONMENT_FILE,
|
|
18
|
+
LOCAL_PATH_PREFIX,
|
|
19
|
+
NPM_AUTH_DIRECTORY_PREFIX,
|
|
20
|
+
NPM_AUTH_TOKEN_REFERENCE,
|
|
21
|
+
NPM_CONFIG_GET_ARGUMENTS,
|
|
13
22
|
NPM_DIST_TAG,
|
|
23
|
+
NPM_INTEGRITY_PATTERN,
|
|
14
24
|
NPM_LOOKUP_STATUS,
|
|
15
25
|
NPM_NOT_FOUND_CODE,
|
|
26
|
+
NPM_PACK_DRY_RUN_ARGUMENTS,
|
|
16
27
|
NPM_PACKAGE_NAME_PATTERN,
|
|
28
|
+
NPM_REGISTRY_OPTION,
|
|
29
|
+
NPM_REGISTRY_PROTOCOLS,
|
|
17
30
|
NPM_TOKEN_VARIABLE,
|
|
31
|
+
NPM_UNSET_CONFIG_VALUE,
|
|
32
|
+
NPM_USER_CONFIG_OPTION,
|
|
33
|
+
NPMJS_PACKAGE_PAGE_URL,
|
|
34
|
+
PACKAGE_SCOPE_PATTERN,
|
|
35
|
+
PUBLISH_CONFIG_FIELD,
|
|
36
|
+
PUBLISH_CONFIG_REGISTRY_KEY,
|
|
37
|
+
SCOPED_REGISTRY_KEY_SUFFIX,
|
|
38
|
+
SHELL_SAFE_REGISTRY_URL_PATTERN,
|
|
39
|
+
UNSAFE_QUOTED_PATH_PATTERN,
|
|
18
40
|
} from "../constants/create-version.js";
|
|
41
|
+
import { PACKAGE_MANAGER_USER_AGENT_VARIABLE } from "../constants/guard-publish.js";
|
|
19
42
|
import { USES_SHELL_FOR_PACKAGE_MANAGERS, runCaptured, runInherited } from "./process.js";
|
|
20
43
|
|
|
21
44
|
/**
|
|
22
45
|
* @typedef {{ status: string, publishedVersions: string[], reason: string | null }} NpmLookup
|
|
46
|
+
* @typedef {{ name: unknown, version: unknown, integrity: string }} NpmPackDescription
|
|
47
|
+
* What `npm pack --dry-run --json` reports for the release checkout: package name, version and `sha512-<base64>` integrity.
|
|
48
|
+
* @typedef {{ pack: NpmPackDescription | null, problem: string | null }} NpmPackResult
|
|
23
49
|
*/
|
|
24
50
|
|
|
25
51
|
/**
|
|
26
|
-
*
|
|
52
|
+
* Runs npm with captured output. On Windows npm only resolves through the shell, so every argument
|
|
53
|
+
* is double-quoted there; callers pass validated names, registries and temporary paths only.
|
|
54
|
+
*
|
|
55
|
+
* @param {string[]} npmArguments - Arguments that follow `npm`.
|
|
56
|
+
* @param {string} repositoryRoot - Working directory, whose `.npmrc` npm reads.
|
|
57
|
+
* @param {NodeJS.ProcessEnv} [environment] - Environment of npm; the current process by default.
|
|
58
|
+
* @returns {ReturnType<typeof runCaptured>} Exit status and output.
|
|
59
|
+
* @throws {Error} When an argument could break out of its quotes on the Windows shell.
|
|
60
|
+
*/
|
|
61
|
+
function runNpmCaptured(npmArguments, repositoryRoot, environment = process.env) {
|
|
62
|
+
const unsafeArgument = npmArguments.find((npmArgument) => UNSAFE_QUOTED_PATH_PATTERN.test(npmArgument));
|
|
63
|
+
if (unsafeArgument !== undefined) {
|
|
64
|
+
throw new Error(`beez-rp create-version: argumento de npm no permitido en la línea de comandos: ${unsafeArgument}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return USES_SHELL_FOR_PACKAGE_MANAGERS
|
|
68
|
+
? runCaptured(`npm ${npmArguments.map((npmArgument) => `"${npmArgument}"`).join(" ")}`, [], { cwd: repositoryRoot, shell: true, env: environment })
|
|
69
|
+
: runCaptured("npm", npmArguments, { cwd: repositoryRoot, env: environment });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Builds the `npm view` arguments that list the published versions of a package on a registry.
|
|
74
|
+
* `npm view` ignores the manifest `publishConfig`, so the registry is always passed explicitly.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} packageName - npm package name, already checked with `NPM_PACKAGE_NAME_PATTERN`.
|
|
77
|
+
* @param {string} registryUrl - Registry resolved by {@link resolvePublishRegistry}.
|
|
78
|
+
* @param {string | null} [userConfigPath] - Temporary npm config from {@link withNpmAuthConfig} that
|
|
79
|
+
* authenticates the query; `null` queries with npm's usual config.
|
|
80
|
+
* @returns {string[]} Arguments that follow `npm`.
|
|
81
|
+
* @throws {Error} When the registry is not a valid http(s) URL or has characters unsafe on the Windows shell.
|
|
82
|
+
*/
|
|
83
|
+
export function buildNpmViewArguments(packageName, registryUrl, userConfigPath = null) {
|
|
84
|
+
const { href } = parseRegistryUrl(registryUrl);
|
|
85
|
+
if (!SHELL_SAFE_REGISTRY_URL_PATTERN.test(href)) {
|
|
86
|
+
throw new Error(`beez-rp create-version: el registry "${registryUrl}" tiene caracteres no permitidos en la línea de comandos de npm view`);
|
|
87
|
+
}
|
|
88
|
+
const userConfig = userConfigPath ? [NPM_USER_CONFIG_OPTION, userConfigPath] : [];
|
|
89
|
+
return ["view", packageName, "versions", "--json", NPM_REGISTRY_OPTION, href, ...userConfig];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Loads `NPM_TOKEN` from the ignored `.env` when the environment does not define it.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} repositoryRoot - Repository root holding the optional `.env`.
|
|
96
|
+
* @returns {boolean} Whether `NPM_TOKEN` is available in the environment.
|
|
97
|
+
*/
|
|
98
|
+
export function loadNpmToken(repositoryRoot) {
|
|
99
|
+
const environmentFilePath = path.join(repositoryRoot, LOCAL_ENVIRONMENT_FILE);
|
|
100
|
+
|
|
101
|
+
if (!process.env[NPM_TOKEN_VARIABLE] && existsSync(environmentFilePath)) {
|
|
102
|
+
process.loadEnvFile(environmentFilePath);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return Boolean(process.env[NPM_TOKEN_VARIABLE]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Lists the versions of a package published on a registry. With `NPM_TOKEN` (environment or `.env`)
|
|
110
|
+
* the query authenticates through the same temporary config the publication uses, so a private
|
|
111
|
+
* package can be diagnosed; without it the registry is queried with npm's usual config.
|
|
27
112
|
*
|
|
28
113
|
* @param {string} packageName - npm package name.
|
|
29
114
|
* @param {string} repositoryRoot - Directory whose `.npmrc` npm reads.
|
|
115
|
+
* @param {string} [registryUrl] - Registry the package is published to, from {@link resolvePublishRegistry};
|
|
116
|
+
* defaults to npm's default registry.
|
|
30
117
|
* @returns {Promise<NpmLookup>} Published versions; a never-published package has none.
|
|
31
118
|
*/
|
|
32
|
-
export async function lookupPublishedVersions(packageName, repositoryRoot) {
|
|
119
|
+
export async function lookupPublishedVersions(packageName, repositoryRoot, registryUrl = DEFAULT_NPM_REGISTRY_URL) {
|
|
33
120
|
if (!NPM_PACKAGE_NAME_PATTERN.test(packageName)) {
|
|
34
121
|
return { status: NPM_LOOKUP_STATUS.failed, publishedVersions: [], reason: `nombre de paquete inválido: ${packageName}` };
|
|
35
122
|
}
|
|
36
123
|
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
124
|
+
/** @param {string | null} userConfigPath - Temporary authenticated config, or `null`. */
|
|
125
|
+
const view = (userConfigPath) => runNpmCaptured(buildNpmViewArguments(packageName, registryUrl, userConfigPath), repositoryRoot);
|
|
126
|
+
|
|
127
|
+
let result;
|
|
128
|
+
try {
|
|
129
|
+
result = loadNpmToken(repositoryRoot) ? await withNpmAuthConfig(buildNpmAuthConfigLine(registryUrl), view) : await view(null);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return { status: NPM_LOOKUP_STATUS.failed, publishedVersions: [], reason: error instanceof Error ? error.message : String(error) };
|
|
132
|
+
}
|
|
41
133
|
|
|
42
134
|
if (result.status !== 0) {
|
|
43
135
|
return `${result.stdout}\n${result.stderr}`.includes(NPM_NOT_FOUND_CODE)
|
|
@@ -58,28 +150,273 @@ export async function lookupPublishedVersions(packageName, repositoryRoot) {
|
|
|
58
150
|
}
|
|
59
151
|
|
|
60
152
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
153
|
+
* Parses the output of `npm pack --dry-run --json`: an array with one package and its `integrity`.
|
|
154
|
+
*
|
|
155
|
+
* @param {string} output - Standard output of npm.
|
|
156
|
+
* @returns {NpmPackResult} Package name, version and integrity, or why the output is unusable.
|
|
157
|
+
*/
|
|
158
|
+
export function parseNpmPackDryRunOutput(output) {
|
|
159
|
+
let parsed;
|
|
160
|
+
try {
|
|
161
|
+
parsed = JSON.parse(output);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
return { pack: null, problem: `la salida de npm pack --dry-run no es JSON válido (${error instanceof Error ? error.message : String(error)})` };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const packages = Array.isArray(parsed) ? parsed : [];
|
|
167
|
+
const described = packages.length === 1 && packages[0] && typeof packages[0] === "object" ? packages[0] : null;
|
|
168
|
+
if (!described || typeof described.integrity !== "string" || !NPM_INTEGRITY_PATTERN.test(described.integrity)) {
|
|
169
|
+
return { pack: null, problem: "la salida de npm pack --dry-run no describe un único paquete con su integrity sha512" };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { pack: { name: described.name, version: described.version, integrity: described.integrity }, problem: null };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Asks npm for the integrity of the archive it would pack from the release checkout, without
|
|
177
|
+
* writing anything nor running lifecycle scripts. The command line is constant, so it is safe
|
|
178
|
+
* on the Windows shell.
|
|
179
|
+
*
|
|
180
|
+
* @param {string} repositoryRoot - Package root, as `prepare` left it.
|
|
181
|
+
* @returns {Promise<NpmPackResult>} npm description, or why npm failed or its output is unusable.
|
|
182
|
+
*/
|
|
183
|
+
export async function readNpmPackIntegrity(repositoryRoot) {
|
|
184
|
+
const result = USES_SHELL_FOR_PACKAGE_MANAGERS
|
|
185
|
+
? await runCaptured(`npm ${NPM_PACK_DRY_RUN_ARGUMENTS.join(" ")}`, [], { cwd: repositoryRoot, shell: true })
|
|
186
|
+
: await runCaptured("npm", [...NPM_PACK_DRY_RUN_ARGUMENTS], { cwd: repositoryRoot });
|
|
187
|
+
|
|
188
|
+
if (result.status !== 0) {
|
|
189
|
+
return { pack: null, problem: `npm pack --dry-run salió con código ${result.status}: ${result.stderr.split("\n")[0] || "sin detalle"}` };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return parseNpmPackDryRunOutput(result.stdout);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Parses a publish registry URL, accepting only plain http(s) URLs.
|
|
197
|
+
*
|
|
198
|
+
* @param {string} registryUrl - Registry URL as written in the manifest.
|
|
199
|
+
* @returns {URL} Parsed URL.
|
|
200
|
+
* @throws {Error} When the registry is not a plain http(s) URL (credentials, query and fragment are rejected).
|
|
201
|
+
*/
|
|
202
|
+
function parseRegistryUrl(registryUrl) {
|
|
203
|
+
/** @type {URL} */
|
|
204
|
+
let registry;
|
|
205
|
+
try {
|
|
206
|
+
registry = new URL(registryUrl);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
throw new Error(`beez-rp create-version: el registry de publicación "${registryUrl}" no es una URL válida`, { cause: error });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!NPM_REGISTRY_PROTOCOLS.includes(registry.protocol) || registry.username || registry.password || registry.search || registry.hash) {
|
|
212
|
+
throw new Error(`beez-rp create-version: el registry de publicación "${registryUrl}" tiene que ser una URL http(s) sin credenciales, query ni fragmento`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return registry;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Returns the scope of a package name (`team` for `@team/pkg`), or `undefined` when it has none.
|
|
63
220
|
*
|
|
221
|
+
* @param {unknown} packageName - `name` of the manifest.
|
|
222
|
+
* @returns {string | undefined} Scope without `@`.
|
|
223
|
+
*/
|
|
224
|
+
function readPackageScope(packageName) {
|
|
225
|
+
return typeof packageName === "string" ? PACKAGE_SCOPE_PATTERN.exec(packageName)?.groups?.scope : undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Returns the registry the manifest `publishConfig` declares: `publishConfig["@scope:registry"]`
|
|
230
|
+
* for a scoped package that declares it, else `publishConfig.registry`.
|
|
231
|
+
*
|
|
232
|
+
* @param {Record<string, unknown>} manifest - `package.json` being published.
|
|
233
|
+
* @returns {string | null} Declared registry, or `null` when `publishConfig` declares none.
|
|
234
|
+
*/
|
|
235
|
+
function readPublishConfigRegistry(manifest) {
|
|
236
|
+
const publishConfig = manifest[PUBLISH_CONFIG_FIELD];
|
|
237
|
+
const registries = publishConfig && typeof publishConfig === "object" ? /** @type {Record<string, unknown>} */ (publishConfig) : {};
|
|
238
|
+
const scope = readPackageScope(manifest.name);
|
|
239
|
+
const candidates = [scope ? registries[`@${scope}${SCOPED_REGISTRY_KEY_SUFFIX}`] : undefined, registries[PUBLISH_CONFIG_REGISTRY_KEY]];
|
|
240
|
+
const declared = candidates.find((candidate) => typeof candidate === "string" && candidate !== "");
|
|
241
|
+
return typeof declared === "string" ? declared : null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Asks npm for the registry it would publish the package to without `publishConfig`:
|
|
246
|
+
* `@scope:registry` for a scoped package when some config sets it, else `registry`. It runs in the
|
|
247
|
+
* repository root with the environment and the kind of temporary user config `npm publish` gets,
|
|
248
|
+
* so it reads the project `.npmrc`, `npm_config_*` variables and the global config, like the
|
|
249
|
+
* publication (which replaces `~/.npmrc` with its temporary config).
|
|
250
|
+
*
|
|
251
|
+
* @param {Record<string, unknown>} manifest - `package.json` being published.
|
|
64
252
|
* @param {string} repositoryRoot - Package root.
|
|
65
|
-
* @returns {Promise<
|
|
253
|
+
* @returns {Promise<string>} Registry URL npm resolves.
|
|
254
|
+
* @throws {Error} When the package name is invalid or `npm config get` fails.
|
|
66
255
|
*/
|
|
67
|
-
|
|
68
|
-
const
|
|
256
|
+
async function readNpmConfigRegistry(manifest, repositoryRoot) {
|
|
257
|
+
const packageName = String(manifest.name);
|
|
258
|
+
if (!NPM_PACKAGE_NAME_PATTERN.test(packageName)) {
|
|
259
|
+
throw new Error(`beez-rp create-version: nombre de paquete inválido: ${packageName}`);
|
|
260
|
+
}
|
|
69
261
|
|
|
70
|
-
|
|
71
|
-
|
|
262
|
+
// The scope comes from a name that matches the npm name pattern, so the key keeps a fixed shape.
|
|
263
|
+
const scope = readPackageScope(packageName);
|
|
264
|
+
const configKeys = [...(scope ? [`@${scope}${SCOPED_REGISTRY_KEY_SUFFIX}`] : []), PUBLISH_CONFIG_REGISTRY_KEY];
|
|
265
|
+
|
|
266
|
+
return withNpmAuthConfig("", async (userConfigPath) => {
|
|
267
|
+
for (const configKey of configKeys) {
|
|
268
|
+
const result = await runNpmCaptured(
|
|
269
|
+
[...NPM_CONFIG_GET_ARGUMENTS, configKey, NPM_USER_CONFIG_OPTION, userConfigPath],
|
|
270
|
+
repositoryRoot,
|
|
271
|
+
buildNpmPublishEnvironment()
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
if (result.status !== 0) {
|
|
275
|
+
throw new Error(`beez-rp create-version: npm config get ${configKey} salió con código ${result.status}: ${result.stderr.split("\n")[0] || "sin detalle"}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const value = result.stdout.trim();
|
|
279
|
+
if (value !== "" && value !== NPM_UNSET_CONFIG_VALUE) {
|
|
280
|
+
return value;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return DEFAULT_NPM_REGISTRY_URL;
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Returns the registry `npm publish` sends the package to: `publishConfig["@scope:registry"]` for a
|
|
290
|
+
* scoped package that declares it, else `publishConfig.registry`, else the registry npm's own config
|
|
291
|
+
* resolves (project `.npmrc`, environment, global config; see {@link readNpmConfigRegistry}).
|
|
292
|
+
*
|
|
293
|
+
* @param {Record<string, unknown>} manifest - `package.json` being published.
|
|
294
|
+
* @param {string} repositoryRoot - Package root, where npm reads its project config.
|
|
295
|
+
* @returns {Promise<string>} Registry URL, already checked to be a plain http(s) URL.
|
|
296
|
+
* @throws {Error} When the resolved registry is not a plain http(s) URL or npm cannot report its config.
|
|
297
|
+
*/
|
|
298
|
+
export async function resolvePublishRegistry(manifest, repositoryRoot) {
|
|
299
|
+
const registryUrl = readPublishConfigRegistry(manifest) ?? (await readNpmConfigRegistry(manifest, repositoryRoot));
|
|
300
|
+
|
|
301
|
+
parseRegistryUrl(registryUrl);
|
|
302
|
+
return registryUrl;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Describes where a published release can be seen: its npmjs.com page when the registry is the
|
|
307
|
+
* public npm registry, else the registry URL with the package and version.
|
|
308
|
+
*
|
|
309
|
+
* @param {{ registryUrl: string, packageName: string, version: string }} release - Published release.
|
|
310
|
+
* @returns {string} Summary text.
|
|
311
|
+
*/
|
|
312
|
+
export function describePublishedRelease({ registryUrl, packageName, version }) {
|
|
313
|
+
return parseRegistryUrl(registryUrl).href === DEFAULT_NPM_REGISTRY_URL
|
|
314
|
+
? `${NPMJS_PACKAGE_PAGE_URL}${packageName}/v/${version}`
|
|
315
|
+
: `Registro: ${registryUrl} · ${packageName}@${version}`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Builds the npm config line that binds `${NPM_TOKEN}` to a registry, in the form npm matches
|
|
320
|
+
* credentials with: `//<host>[:port]<path>/:_authToken=${NPM_TOKEN}` (no protocol, trailing `/`).
|
|
321
|
+
*
|
|
322
|
+
* @param {string} registryUrl - Registry `npm publish` uses.
|
|
323
|
+
* @returns {string} Config line ending with a newline; the token itself is never written.
|
|
324
|
+
* @throws {Error} When the registry is not a plain http(s) URL (credentials, query and fragment are rejected).
|
|
325
|
+
*/
|
|
326
|
+
export function buildNpmAuthConfigLine(registryUrl) {
|
|
327
|
+
const registry = parseRegistryUrl(registryUrl);
|
|
328
|
+
const registryPath = registry.pathname.endsWith("/") ? registry.pathname : `${registry.pathname}/`;
|
|
329
|
+
return `//${registry.host}${registryPath}:_authToken=${NPM_AUTH_TOKEN_REFERENCE}\n`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Runs an operation with a temporary npm user config outside the repository
|
|
334
|
+
* that only references `${NPM_TOKEN}` for the publish registry: npm expands it
|
|
335
|
+
* from the environment, so the token never reaches the disk or a command line,
|
|
336
|
+
* and no repository needs an `.npmrc` (which pnpm refuses to expand and warns
|
|
337
|
+
* about). The config is always removed afterwards.
|
|
338
|
+
*
|
|
339
|
+
* @template T
|
|
340
|
+
* @param {string} authConfigLine - Line built by {@link buildNpmAuthConfigLine}; an empty line
|
|
341
|
+
* gives npm a user config without credentials, which only replaces `~/.npmrc`.
|
|
342
|
+
* @param {(userConfigPath: string) => Promise<T>} operation - Receives the path for `npm --userconfig`.
|
|
343
|
+
* @param {string} [parentDirectory] - Where the temporary directory is created; defaults to the OS temp directory.
|
|
344
|
+
* @returns {Promise<T>} The operation result.
|
|
345
|
+
*/
|
|
346
|
+
export async function withNpmAuthConfig(authConfigLine, operation, parentDirectory = tmpdir()) {
|
|
347
|
+
const directory = mkdtempSync(path.join(parentDirectory, NPM_AUTH_DIRECTORY_PREFIX));
|
|
348
|
+
const userConfigPath = path.join(directory, "npmrc");
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
writeFileSync(userConfigPath, authConfigLine, { mode: 0o600 });
|
|
352
|
+
return await operation(userConfigPath);
|
|
353
|
+
} finally {
|
|
354
|
+
rmSync(directory, { recursive: true, force: true });
|
|
72
355
|
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Builds the `npm publish` arguments, without the user config. A prepared
|
|
360
|
+
* archive is prefixed with `./` because npm parses a bare relative operand
|
|
361
|
+
* such as `releases/1.9.0-abc/pkg-1.9.0.tgz` as a package spec instead of a file.
|
|
362
|
+
*
|
|
363
|
+
* @param {string | null} [artifactPath] - Archive relative to the root, already checked with `isSafeArtifactPath`; `null` publishes the working tree.
|
|
364
|
+
* @returns {string[]} Arguments that follow `npm`.
|
|
365
|
+
*/
|
|
366
|
+
export function buildNpmPublishArguments(artifactPath = null) {
|
|
367
|
+
const publishTarget = artifactPath
|
|
368
|
+
? [artifactPath.startsWith(LOCAL_PATH_PREFIX) ? artifactPath : `${LOCAL_PATH_PREFIX}${artifactPath}`]
|
|
369
|
+
: [];
|
|
370
|
+
return ["publish", ...publishTarget, "--access", "public", "--tag", NPM_DIST_TAG];
|
|
371
|
+
}
|
|
73
372
|
|
|
74
|
-
|
|
373
|
+
/**
|
|
374
|
+
* Builds the environment of `npm publish` without the inherited package
|
|
375
|
+
* manager user agent. npm reads `npm_config_user_agent` as its `user-agent`
|
|
376
|
+
* config, so under `pnpm create-version` its lifecycle scripts would report
|
|
377
|
+
* pnpm and `beez-rp guard-publish` would block beez-rp's own npm publication.
|
|
378
|
+
* Without it npm reports its own user agent (`npm/…`). npm reads its config
|
|
379
|
+
* variables in any case (`NPM_CONFIG_USER_AGENT` too), so every casing is dropped.
|
|
380
|
+
*
|
|
381
|
+
* @param {NodeJS.ProcessEnv} [environment] - Environment to copy; the current process by default.
|
|
382
|
+
* @returns {NodeJS.ProcessEnv} Copy without `npm_config_user_agent` in any casing.
|
|
383
|
+
*/
|
|
384
|
+
export function buildNpmPublishEnvironment(environment = process.env) {
|
|
385
|
+
return Object.fromEntries(
|
|
386
|
+
Object.entries(environment).filter(([variableName]) => variableName.toLowerCase() !== PACKAGE_MANAGER_USER_AGENT_VARIABLE),
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Publishes the working tree, or a prepared archive, to npm. `NPM_TOKEN` comes
|
|
392
|
+
* from the environment or the ignored `.env` and only reaches npm through the
|
|
393
|
+
* environment and a temporary user config. npm inherits the terminal, so its
|
|
394
|
+
* interactive browser or one-time-password (2FA) confirmation works.
|
|
395
|
+
*
|
|
396
|
+
* @param {string} repositoryRoot - Package root.
|
|
397
|
+
* @param {{ authConfigLine: string, artifactPath?: string | null }} publication - Registry credential line from
|
|
398
|
+
* {@link buildNpmAuthConfigLine}, and the archive relative to the root (already checked with `isSafeArtifactPath`);
|
|
399
|
+
* without `artifactPath` the working tree is published.
|
|
400
|
+
* @returns {Promise<{ exitCode: number, missingToken: boolean }>} npm exit code, or a missing-token result without running npm.
|
|
401
|
+
* @throws {Error} When the temporary config path could break out of its shell quotes.
|
|
402
|
+
*/
|
|
403
|
+
export async function publishToNpm(repositoryRoot, { authConfigLine, artifactPath = null }) {
|
|
404
|
+
if (!loadNpmToken(repositoryRoot)) {
|
|
75
405
|
return { exitCode: 1, missingToken: true };
|
|
76
406
|
}
|
|
77
407
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
408
|
+
const exitCode = await withNpmAuthConfig(authConfigLine, async (userConfigPath) => {
|
|
409
|
+
if (UNSAFE_QUOTED_PATH_PATTERN.test(userConfigPath)) {
|
|
410
|
+
throw new Error(`beez-rp create-version: unsafe temporary npm config path ${userConfigPath}`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// The command line is constant apart from validated paths; the token only travels through the environment.
|
|
414
|
+
const publishArguments = buildNpmPublishArguments(artifactPath);
|
|
415
|
+
const env = buildNpmPublishEnvironment();
|
|
416
|
+
return USES_SHELL_FOR_PACKAGE_MANAGERS
|
|
417
|
+
? runInherited(`npm ${publishArguments.join(" ")} --userconfig "${userConfigPath}"`, [], { cwd: repositoryRoot, shell: true, env })
|
|
418
|
+
: runInherited("npm", [...publishArguments, "--userconfig", userConfigPath], { cwd: repositoryRoot, env });
|
|
419
|
+
});
|
|
83
420
|
|
|
84
421
|
return { exitCode, missingToken: false };
|
|
85
422
|
}
|
|
@@ -314,8 +314,15 @@ export function buildReleasePlan(state, capabilities = DEFAULT_CAPABILITIES) {
|
|
|
314
314
|
/** @type {string[]} */
|
|
315
315
|
const warnings = [];
|
|
316
316
|
|
|
317
|
+
// The rest of the plan depends on the code and configuration of the updated main, so the run
|
|
318
|
+
// stops after syncing and the next one diagnoses again.
|
|
317
319
|
if (state.main.behindCount > 0) {
|
|
318
|
-
steps.push({
|
|
320
|
+
steps.push({
|
|
321
|
+
id: RELEASE_STEP.syncMain,
|
|
322
|
+
title: `Actualizar ${MAIN_BRANCH} desde origin`,
|
|
323
|
+
detail: `${state.main.behindCount} commit(s) nuevos. Después hay que volver a correr pnpm create-version, que diagnostica con el código nuevo.`,
|
|
324
|
+
});
|
|
325
|
+
return { mode: RELEASE_MODE.newRelease, steps, blockers: [], warnings, pendingVersion: null };
|
|
319
326
|
}
|
|
320
327
|
|
|
321
328
|
if (state.migrations?.status === MIGRATION_STATUS.pending) {
|
|
@@ -12,7 +12,7 @@ import { FIELD_SEPARATOR, PACKAGE_MANIFEST_FILE, RECORD_SEPARATOR } from "../con
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* @typedef {{ status: number, stdout: string, stderr: string }} CapturedResult
|
|
15
|
-
* @typedef {{ cwd?: string, shell?: boolean }} CommandOptions
|
|
15
|
+
* @typedef {{ cwd?: string, shell?: boolean, env?: NodeJS.ProcessEnv }} CommandOptions
|
|
16
16
|
* @typedef {{ git: (gitArguments: string[]) => Promise<string>, tryGit: (gitArguments: string[]) => Promise<string | null> }} GitReader
|
|
17
17
|
* @typedef {{ sha: string, subject: string, body: string }} CommitRecord
|
|
18
18
|
*/
|
|
@@ -57,7 +57,7 @@ export function runCaptured(command, commandArguments, options = {}) {
|
|
|
57
57
|
*/
|
|
58
58
|
export function runInherited(command, commandArguments, options = {}) {
|
|
59
59
|
return new Promise((resolve) => {
|
|
60
|
-
const child = spawn(command, commandArguments, { cwd: options.cwd, shell: options.shell ?? false, stdio: "inherit" });
|
|
60
|
+
const child = spawn(command, commandArguments, { cwd: options.cwd, env: options.env, shell: options.shell ?? false, stdio: "inherit" });
|
|
61
61
|
child.on("error", () => resolve(1));
|
|
62
62
|
child.on("close", (status) => resolve(status ?? 1));
|
|
63
63
|
});
|
|
@@ -131,18 +131,31 @@ export async function listCommits(reader, range) {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
/**
|
|
134
|
-
* Reads
|
|
134
|
+
* Reads `package.json` at a revision.
|
|
135
135
|
*
|
|
136
136
|
* @param {GitReader} reader - Git reader.
|
|
137
|
-
* @param {string} revision - Revision such as `HEAD` or `origin/main`.
|
|
138
|
-
* @returns {Promise<string | null>}
|
|
137
|
+
* @param {string} revision - Revision such as `HEAD`, `v1.2.0` or `origin/main`.
|
|
138
|
+
* @returns {Promise<Record<string, unknown> | null>} Parsed manifest, or `null` when missing, unreadable or not a JSON object.
|
|
139
139
|
*/
|
|
140
|
-
export async function
|
|
140
|
+
export async function readPackageManifestAt(reader, revision) {
|
|
141
141
|
const manifest = await reader.tryGit(["show", `${revision}:${PACKAGE_MANIFEST_FILE}`]);
|
|
142
142
|
|
|
143
143
|
try {
|
|
144
|
-
|
|
144
|
+
const parsed = manifest ? JSON.parse(manifest) : null;
|
|
145
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
145
146
|
} catch {
|
|
146
147
|
return null;
|
|
147
148
|
}
|
|
148
149
|
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Reads the `version` field of `package.json` at a revision.
|
|
153
|
+
*
|
|
154
|
+
* @param {GitReader} reader - Git reader.
|
|
155
|
+
* @param {string} revision - Revision such as `HEAD` or `origin/main`.
|
|
156
|
+
* @returns {Promise<string | null>} Version, or `null` when unreadable.
|
|
157
|
+
*/
|
|
158
|
+
export async function readPackageVersionAt(reader, revision) {
|
|
159
|
+
const manifest = await readPackageManifestAt(reader, revision);
|
|
160
|
+
return typeof manifest?.version === "string" ? manifest.version : null;
|
|
161
|
+
}
|