@astra-kit/rp 0.0.0-stage → 0.10.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 +196 -0
- package/LICENSE.md +21 -0
- package/README.md +352 -2
- package/bin/astra-rp.js +51 -0
- package/package.json +93 -4
- package/src/build-gate.js +157 -0
- package/src/changelog-blocks.js +53 -0
- package/src/changelog.js +108 -0
- package/src/constants/build-gate.js +30 -0
- package/src/constants/changelog-draft.js +180 -0
- package/src/constants/changelog.js +74 -0
- package/src/constants/ci-release.js +346 -0
- package/src/constants/cli.js +12 -0
- package/src/constants/create-version.js +523 -0
- package/src/constants/guard-publish.js +29 -0
- package/src/constants/index.js +18 -0
- package/src/constants/monorepo.js +57 -0
- package/src/constants/package-manager.js +56 -0
- package/src/constants/registry.js +72 -0
- package/src/constants/terminal-ui.js +93 -0
- package/src/constants/version-files.js +63 -0
- package/src/constants/versions.js +60 -0
- package/src/create-version/artifact.js +343 -0
- package/src/create-version/changelog-draft-workspace.js +124 -0
- package/src/create-version/changelog-draft.js +380 -0
- package/src/create-version/changelog.js +264 -0
- package/src/create-version/ci-config.js +54 -0
- package/src/create-version/ci-publication-history.js +113 -0
- package/src/create-version/ci-release-identity-step.js +58 -0
- package/src/create-version/ci-setup.js +794 -0
- package/src/create-version/ci.js +555 -0
- package/src/create-version/committed-tree.js +75 -0
- package/src/create-version/config.js +489 -0
- package/src/create-version/errors.js +46 -0
- package/src/create-version/github-workflow.js +199 -0
- package/src/create-version/index.js +54 -0
- package/src/create-version/json.js +32 -0
- package/src/create-version/jsonc.js +76 -0
- package/src/create-version/jsr.js +96 -0
- package/src/create-version/local-changes.js +178 -0
- package/src/create-version/npm-auth.js +142 -0
- package/src/create-version/npm.js +812 -0
- package/src/create-version/plan.js +1200 -0
- package/src/create-version/process.js +334 -0
- package/src/create-version/registry-config.js +121 -0
- package/src/create-version/registry-http.js +62 -0
- package/src/create-version/registry.js +220 -0
- package/src/create-version/release-execution.js +45 -0
- package/src/create-version/release-tag-config.js +76 -0
- package/src/create-version/run.js +2087 -0
- package/src/create-version/state.js +461 -0
- package/src/create-version/strict-utf8.js +11 -0
- package/src/create-version/templates/release.yml +52 -0
- package/src/create-version/templates/vercel-ignore-build.mjs +66 -0
- package/src/guard-publish.js +77 -0
- package/src/index.js +42 -0
- package/src/monorepo/plan.js +475 -0
- package/src/monorepo/release-commit.js +46 -0
- package/src/monorepo/run.js +927 -0
- package/src/monorepo/state.js +206 -0
- package/src/monorepo/workspaces.js +327 -0
- package/src/package-manager.js +107 -0
- package/src/terminal-ui.js +548 -0
- package/src/testing.js +70 -0
- package/src/version-files.js +142 -0
- package/src/versions.js +303 -0
- package/types/build-gate.d.ts +43 -0
- package/types/changelog-blocks.d.ts +33 -0
- package/types/changelog.d.ts +61 -0
- package/types/constants/build-gate.d.ts +23 -0
- package/types/constants/changelog-draft.d.ts +138 -0
- package/types/constants/changelog.d.ts +56 -0
- package/types/constants/ci-release.d.ts +284 -0
- package/types/constants/cli.d.ts +11 -0
- package/types/constants/create-version.d.ts +397 -0
- package/types/constants/guard-publish.d.ts +25 -0
- package/types/constants/index.d.ts +17 -0
- package/types/constants/monorepo.d.ts +45 -0
- package/types/constants/package-manager.d.ts +40 -0
- package/types/constants/registry.d.ts +79 -0
- package/types/constants/terminal-ui.d.ts +73 -0
- package/types/constants/version-files.d.ts +51 -0
- package/types/constants/versions.d.ts +47 -0
- package/types/create-version/artifact.d.ts +113 -0
- package/types/create-version/changelog-draft-workspace.d.ts +33 -0
- package/types/create-version/changelog-draft.d.ts +127 -0
- package/types/create-version/changelog.d.ts +141 -0
- package/types/create-version/ci-config.d.ts +33 -0
- package/types/create-version/ci-publication-history.d.ts +51 -0
- package/types/create-version/ci-release-identity-step.d.ts +12 -0
- package/types/create-version/ci-setup.d.ts +148 -0
- package/types/create-version/ci.d.ts +145 -0
- package/types/create-version/committed-tree.d.ts +55 -0
- package/types/create-version/config.d.ts +128 -0
- package/types/create-version/errors.d.ts +39 -0
- package/types/create-version/github-workflow.d.ts +41 -0
- package/types/create-version/index.d.ts +28 -0
- package/types/create-version/json.d.ts +21 -0
- package/types/create-version/jsonc.d.ts +21 -0
- package/types/create-version/jsr.d.ts +52 -0
- package/types/create-version/local-changes.d.ts +52 -0
- package/types/create-version/npm-auth.d.ts +60 -0
- package/types/create-version/npm.d.ts +331 -0
- package/types/create-version/plan.d.ts +369 -0
- package/types/create-version/process.d.ts +151 -0
- package/types/create-version/registry-config.d.ts +56 -0
- package/types/create-version/registry-http.d.ts +22 -0
- package/types/create-version/registry.d.ts +86 -0
- package/types/create-version/release-execution.d.ts +22 -0
- package/types/create-version/release-tag-config.d.ts +18 -0
- package/types/create-version/run.d.ts +407 -0
- package/types/create-version/state.d.ts +204 -0
- package/types/create-version/strict-utf8.d.ts +10 -0
- package/types/guard-publish.d.ts +49 -0
- package/types/index.d.ts +23 -0
- package/types/monorepo/plan.d.ts +68 -0
- package/types/monorepo/release-commit.d.ts +29 -0
- package/types/monorepo/run.d.ts +68 -0
- package/types/monorepo/state.d.ts +96 -0
- package/types/monorepo/workspaces.d.ts +86 -0
- package/types/package-manager.d.ts +58 -0
- package/types/terminal-ui.d.ts +196 -0
- package/types/testing.d.ts +31 -0
- package/types/version-files.d.ts +59 -0
- package/types/versions.d.ts +158 -0
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* npm adapter of `astra-rp create-version`: resolves the registry a package is
|
|
3
|
+
* published to (`publishConfig`, else npm's own config), resolves `NPM_TOKEN`
|
|
4
|
+
* with a single ordered lookup (environment, repository `.env`, shared
|
|
5
|
+
* `~/.config/astra-rp/.env`), checks that the token can publish the package
|
|
6
|
+
* (`npm whoami` + `npm owner ls`), lists the versions published there
|
|
7
|
+
* (authenticated with `NPM_TOKEN` when available), reads the integrity npm
|
|
8
|
+
* would pack from the release checkout, and publishes the working tree or a
|
|
9
|
+
* prepared archive with `NPM_TOKEN` bound to the publish registry.
|
|
10
|
+
*
|
|
11
|
+
* @module create-version/npm
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { homedir, tmpdir } from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { parseEnv } from "node:util";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_NPM_REGISTRY_URL,
|
|
21
|
+
LOCAL_ENVIRONMENT_FILE,
|
|
22
|
+
LOCAL_PATH_PREFIX,
|
|
23
|
+
NPM_CONFIG_ENVIRONMENT_PREFIX,
|
|
24
|
+
NPM_CREDENTIAL_CONFIG_FIELDS,
|
|
25
|
+
NPM_AUTH_DIRECTORY_PREFIX,
|
|
26
|
+
NPM_AUTH_STATUS,
|
|
27
|
+
NPM_AUTH_TOKEN_REFERENCE,
|
|
28
|
+
NPM_CONFIG_GET_ARGUMENTS,
|
|
29
|
+
NPM_DIST_TAG,
|
|
30
|
+
NPM_INTEGRITY_PATTERN,
|
|
31
|
+
NPM_LOOKUP_STATUS,
|
|
32
|
+
NPM_NOT_FOUND_CODE,
|
|
33
|
+
NPM_OWNER_LINE_PATTERN,
|
|
34
|
+
NPM_OWNER_LIST_ARGUMENTS,
|
|
35
|
+
NPM_PACK_DRY_RUN_ARGUMENTS,
|
|
36
|
+
NPM_PACKAGE_NAME_PATTERN,
|
|
37
|
+
NPM_REGISTRY_BOUND_KEY_PREFIX,
|
|
38
|
+
NPM_REGISTRY_OPTION,
|
|
39
|
+
NPM_REJECTED_CREDENTIAL_PATTERN,
|
|
40
|
+
NPM_TOKEN_SOURCE,
|
|
41
|
+
NPM_TOKEN_VARIABLE,
|
|
42
|
+
NPM_UNSET_CONFIG_VALUE,
|
|
43
|
+
NPM_USER_CONFIG_OPTION,
|
|
44
|
+
NPM_WHOAMI_ARGUMENTS,
|
|
45
|
+
NPMJS_PACKAGE_PAGE_URL,
|
|
46
|
+
PACKAGE_SCOPE_PATTERN,
|
|
47
|
+
PROJECT_NPM_CONFIG_FILE,
|
|
48
|
+
PUBLISH_CONFIG_FIELD,
|
|
49
|
+
PUBLISH_CONFIG_REGISTRY_KEY,
|
|
50
|
+
SCOPED_REGISTRY_KEY_SUFFIX,
|
|
51
|
+
SHARED_ENVIRONMENT_FILE_SEGMENTS,
|
|
52
|
+
SHELL_SAFE_REGISTRY_URL_PATTERN,
|
|
53
|
+
UNSAFE_QUOTED_PATH_PATTERN,
|
|
54
|
+
} from "../constants/create-version.js";
|
|
55
|
+
import { PACKAGE_MANAGER_USER_AGENT_VARIABLE } from "../constants/guard-publish.js";
|
|
56
|
+
import { describeError } from "./errors.js";
|
|
57
|
+
import { isRecord } from "./json.js";
|
|
58
|
+
import { isPlainHttpUrl } from "./registry-config.js";
|
|
59
|
+
import { USES_SHELL_FOR_PACKAGE_MANAGERS, quoteShellArguments, runCaptured, runInherited } from "./process.js";
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @typedef {{ status: string, publishedVersions: string[], latestVersion?: string | null, reason: string | null, registryLabel?: string, tag?: string | null }} NpmLookup
|
|
63
|
+
* `latestVersion` is the version the `latest` dist-tag points at, which may be a prerelease.
|
|
64
|
+
* @typedef {{ name: unknown, version: unknown, integrity: string }} NpmPackDescription
|
|
65
|
+
* What `npm pack --dry-run --json` reports for the release checkout: package name, version and `sha512-<base64>` integrity.
|
|
66
|
+
* @typedef {{ pack: NpmPackDescription | null, problem: string | null }} NpmPackResult
|
|
67
|
+
* @typedef {{ token: string | null, source: string | null }} NpmTokenResolution
|
|
68
|
+
* `NPM_TOKEN` and the `NPM_TOKEN_SOURCE` it came from; both `null` when no source defines it.
|
|
69
|
+
* @typedef {{ environment?: NodeJS.ProcessEnv, homeDirectory?: string, tokenVariable?: string, pinScope?: boolean }} NpmTokenLookup
|
|
70
|
+
* Environment and home directory {@link resolveNpmToken} reads; the current process and `os.homedir()` by default.
|
|
71
|
+
* @typedef {{
|
|
72
|
+
* status: string,
|
|
73
|
+
* user: string | null,
|
|
74
|
+
* source: string | null,
|
|
75
|
+
* registryUrl: string,
|
|
76
|
+
* packageName: string,
|
|
77
|
+
* owners: string[],
|
|
78
|
+
* firstPublication: boolean,
|
|
79
|
+
* reason: string | null,
|
|
80
|
+
* tokenVariable?: string,
|
|
81
|
+
* registryLabel?: string,
|
|
82
|
+
* }} NpmAuthCheck
|
|
83
|
+
* Result of {@link checkNpmPublishAccess}: one of `NPM_AUTH_STATUS`, the user the token
|
|
84
|
+
* authenticates as, where the token came from (never the token itself), the owners npm reports
|
|
85
|
+
* and why the check did not pass.
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Runs npm with captured output. On Windows npm only resolves through the shell, so every argument
|
|
90
|
+
* is double-quoted there; callers pass validated names, registries and temporary paths only.
|
|
91
|
+
*
|
|
92
|
+
* @param {string[]} npmArguments - Arguments that follow `npm`.
|
|
93
|
+
* @param {string} repositoryRoot - Working directory, whose `.npmrc` npm reads.
|
|
94
|
+
* @param {NodeJS.ProcessEnv} [environment] - Environment of npm; the current process by default.
|
|
95
|
+
* @returns {ReturnType<typeof runCaptured>} Exit status and output.
|
|
96
|
+
* @throws {Error} When an argument could break out of its quotes on the Windows shell.
|
|
97
|
+
*/
|
|
98
|
+
export function runNpmCaptured(npmArguments, repositoryRoot, environment = process.env) {
|
|
99
|
+
const unsafeArgument = npmArguments.find((npmArgument) => UNSAFE_QUOTED_PATH_PATTERN.test(npmArgument));
|
|
100
|
+
if (unsafeArgument !== undefined) {
|
|
101
|
+
throw new Error(`astra-rp create-version: argumento de npm no permitido en la línea de comandos: ${unsafeArgument}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return USES_SHELL_FOR_PACKAGE_MANAGERS
|
|
105
|
+
? runCaptured(`npm ${quoteShellArguments(npmArguments)}`, [], { cwd: repositoryRoot, shell: true, env: environment })
|
|
106
|
+
: runCaptured("npm", npmArguments, { cwd: repositoryRoot, env: environment });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Builds the `npm view` arguments that list the published versions and dist-tags of a package on a registry.
|
|
111
|
+
* `npm view` ignores the manifest `publishConfig`, so the registry is always passed explicitly.
|
|
112
|
+
*
|
|
113
|
+
* @param {string} packageName - npm package name, already checked with `NPM_PACKAGE_NAME_PATTERN`.
|
|
114
|
+
* @param {string} registryUrl - Registry resolved by {@link resolvePublishRegistry}.
|
|
115
|
+
* @param {string | null} [userConfigPath] - Temporary npm config from {@link withNpmAuthConfig} that
|
|
116
|
+
* authenticates the query; `null` queries with npm's usual config.
|
|
117
|
+
* @param {boolean} [pinScope] - Whether to override inherited scope routing with this operation's registry.
|
|
118
|
+
* @returns {string[]} Arguments that follow `npm`.
|
|
119
|
+
* @throws {Error} When the registry is not a valid http(s) URL or has characters unsafe on the Windows shell.
|
|
120
|
+
*/
|
|
121
|
+
export function buildNpmViewArguments(packageName, registryUrl, userConfigPath = null, pinScope = false) {
|
|
122
|
+
return ["view", packageName, "versions", "dist-tags", "--json", ...buildRegistryOptions(registryUrl, userConfigPath, "npm view", pinScope ? packageName : null)];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Pins a scoped package to the same endpoint as the global registry option.
|
|
127
|
+
*
|
|
128
|
+
* @param {string | null} packageName - Validated publication name, or `null` for an unscoped operation.
|
|
129
|
+
* @param {string} registryUrl - Resolved endpoint.
|
|
130
|
+
* @returns {string[]} Scope-specific CLI override, or no additional routing.
|
|
131
|
+
* @throws {Error} When a package name is unsafe for a CLI configuration key.
|
|
132
|
+
*/
|
|
133
|
+
function buildScopedRegistryOptions(packageName, registryUrl) {
|
|
134
|
+
if (!packageName) return [];
|
|
135
|
+
if (!NPM_PACKAGE_NAME_PATTERN.test(packageName)) throw new Error("astra-rp create-version: nombre de paquete inválido al fijar el registry del scope");
|
|
136
|
+
const scope = readPackageScope(packageName);
|
|
137
|
+
return scope ? [`--${scopedRegistryKey(scope)}=${parseRegistryUrl(registryUrl).href}`] : [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Builds the npm config key that routes a scope to a registry.
|
|
142
|
+
*
|
|
143
|
+
* @param {string} scope - Package scope without `@`.
|
|
144
|
+
* @returns {string} `@<scope>:registry`.
|
|
145
|
+
*/
|
|
146
|
+
function scopedRegistryKey(scope) {
|
|
147
|
+
return `@${scope}${SCOPED_REGISTRY_KEY_SUFFIX}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Builds the options that point an npm command at a registry and, optionally, at the temporary
|
|
152
|
+
* user config of {@link withNpmAuthConfig} and at the same registry for the scope of a package.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} registryUrl - Registry resolved by {@link resolvePublishRegistry}.
|
|
155
|
+
* @param {string | null} userConfigPath - Temporary npm config, or `null` for npm's usual config.
|
|
156
|
+
* @param {string} commandName - npm command named in the error, such as `npm view`.
|
|
157
|
+
* @param {string | null} [scopedPackageName] - Package whose scope is pinned to the registry (see
|
|
158
|
+
* {@link buildScopedRegistryOptions}); `null` keeps the inherited scope routing.
|
|
159
|
+
* @returns {string[]} `--registry <url>`, with a config `--userconfig <path>`, and the scope override.
|
|
160
|
+
* @throws {Error} When the registry is not a valid http(s) URL or has characters unsafe on the Windows shell,
|
|
161
|
+
* or the package name is invalid.
|
|
162
|
+
*/
|
|
163
|
+
function buildRegistryOptions(registryUrl, userConfigPath, commandName, scopedPackageName = null) {
|
|
164
|
+
const { href } = parseRegistryUrl(registryUrl);
|
|
165
|
+
if (!SHELL_SAFE_REGISTRY_URL_PATTERN.test(href)) {
|
|
166
|
+
throw new Error(`astra-rp create-version: el registry "${registryUrl}" tiene caracteres no permitidos en la línea de comandos de ${commandName}`);
|
|
167
|
+
}
|
|
168
|
+
const userConfig = userConfigPath ? [NPM_USER_CONFIG_OPTION, userConfigPath] : [];
|
|
169
|
+
return [NPM_REGISTRY_OPTION, href, ...userConfig, ...buildScopedRegistryOptions(scopedPackageName, registryUrl)];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Builds the `npm whoami` arguments that ask a registry which user the temporary config authenticates as.
|
|
174
|
+
*
|
|
175
|
+
* @param {string} registryUrl - Registry the package is published to.
|
|
176
|
+
* @param {string} userConfigPath - Temporary npm config from {@link withNpmAuthConfig}.
|
|
177
|
+
* @param {string | null} [packageName] - Package whose scope must use this registry.
|
|
178
|
+
* @returns {string[]} Arguments that follow `npm`.
|
|
179
|
+
* @throws {Error} When the registry is invalid or unsafe on the Windows shell.
|
|
180
|
+
*/
|
|
181
|
+
export function buildNpmWhoamiArguments(registryUrl, userConfigPath, packageName = null) {
|
|
182
|
+
return [...NPM_WHOAMI_ARGUMENTS, ...buildRegistryOptions(registryUrl, userConfigPath, "npm whoami", packageName)];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Builds the `npm owner ls` arguments that list the owners of a package on a registry.
|
|
187
|
+
*
|
|
188
|
+
* @param {string} packageName - npm package name, already checked with `NPM_PACKAGE_NAME_PATTERN`.
|
|
189
|
+
* @param {string} registryUrl - Registry the package is published to.
|
|
190
|
+
* @param {string} userConfigPath - Temporary npm config from {@link withNpmAuthConfig}.
|
|
191
|
+
* @returns {string[]} Arguments that follow `npm`.
|
|
192
|
+
* @throws {Error} When the registry is invalid or unsafe on the Windows shell.
|
|
193
|
+
*/
|
|
194
|
+
export function buildNpmOwnerListArguments(packageName, registryUrl, userConfigPath, pinScope = false) {
|
|
195
|
+
return [...NPM_OWNER_LIST_ARGUMENTS, packageName, ...buildRegistryOptions(registryUrl, userConfigPath, "npm owner ls", pinScope ? packageName : null)];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Reads a text file that npm credentials are looked up in, naming the file (never its content)
|
|
200
|
+
* when it exists but cannot be read, such as a directory or a file without read permission.
|
|
201
|
+
*
|
|
202
|
+
* @param {string} filePath - File to read.
|
|
203
|
+
* @param {string} purpose - What the file is read for, as the error explains it.
|
|
204
|
+
* @returns {string | null} Content, or `null` when the file does not exist.
|
|
205
|
+
* @throws {Error} When the file exists but cannot be read; the original error is its `cause`.
|
|
206
|
+
*/
|
|
207
|
+
function readCredentialFile(filePath, purpose) {
|
|
208
|
+
if (!existsSync(filePath)) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
return readFileSync(filePath, "utf8");
|
|
214
|
+
} catch (error) {
|
|
215
|
+
throw new Error(`astra-rp create-version: no se pudo leer ${filePath} para ${purpose}`, { cause: error });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Reads `NPM_TOKEN` from an environment file without loading anything into the process.
|
|
221
|
+
*
|
|
222
|
+
* @param {string} environmentFilePath - `.env` file.
|
|
223
|
+
* @param {string} tokenVariable - Configured credential variable.
|
|
224
|
+
* @returns {string | null} Non-empty token, or `null` when the file is missing or does not define it.
|
|
225
|
+
* @throws {Error} When the file exists but cannot be read.
|
|
226
|
+
*/
|
|
227
|
+
function readTokenFromEnvironmentFile(environmentFilePath, tokenVariable) {
|
|
228
|
+
const content = readCredentialFile(environmentFilePath, `buscar ${tokenVariable}`);
|
|
229
|
+
return content === null ? null : parseEnv(content)[tokenVariable] || null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Resolves `NPM_TOKEN` with the single lookup shared by the diagnosis, `npm view` and the
|
|
234
|
+
* publication: the `NPM_TOKEN` environment variable, else the repository `.env` (ignored by Git),
|
|
235
|
+
* else the file shared by every project, `~/.config/astra-rp/.env`. The token is only returned:
|
|
236
|
+
* it is never written to disk nor loaded into `process.env`.
|
|
237
|
+
*
|
|
238
|
+
* @param {string} repositoryRoot - Repository root holding the optional `.env`.
|
|
239
|
+
* @param {NpmTokenLookup} [lookup] - Environment and home directory to read.
|
|
240
|
+
* @returns {NpmTokenResolution} Token and its source.
|
|
241
|
+
* @throws {Error} When a `.env` that has to be read exists but cannot be read.
|
|
242
|
+
*/
|
|
243
|
+
export function resolveNpmToken(repositoryRoot, { environment = process.env, homeDirectory = homedir(), tokenVariable = NPM_TOKEN_VARIABLE } = {}) {
|
|
244
|
+
const candidates = [
|
|
245
|
+
{ source: NPM_TOKEN_SOURCE.environment, read: () => environment[tokenVariable] || null },
|
|
246
|
+
{ source: NPM_TOKEN_SOURCE.repository, read: () => readTokenFromEnvironmentFile(path.join(repositoryRoot, LOCAL_ENVIRONMENT_FILE), tokenVariable) },
|
|
247
|
+
{ source: NPM_TOKEN_SOURCE.shared, read: () => readTokenFromEnvironmentFile(path.join(homeDirectory, ...SHARED_ENVIRONMENT_FILE_SEGMENTS), tokenVariable) },
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
for (const candidate of candidates) {
|
|
251
|
+
const token = candidate.read();
|
|
252
|
+
if (token) {
|
|
253
|
+
return { token, source: candidate.source };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return { token: null, source: null };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Tells whether an environment variable is an npm config credential (`npm_config_//host/:_authToken`,
|
|
262
|
+
* `NPM_CONFIG__AUTH`, ...): npm reads its config variables in any casing and ranks them above every
|
|
263
|
+
* npmrc file, so an inherited one would authenticate instead of the temporary config.
|
|
264
|
+
*
|
|
265
|
+
* @param {string} variableName - Environment variable name.
|
|
266
|
+
* @returns {boolean} Whether the variable sets one of {@link NPM_CREDENTIAL_CONFIG_FIELDS}.
|
|
267
|
+
*/
|
|
268
|
+
function isNpmCredentialEnvironmentVariable(variableName) {
|
|
269
|
+
const normalizedName = variableName.toLowerCase();
|
|
270
|
+
if (!normalizedName.startsWith(NPM_CONFIG_ENVIRONMENT_PREFIX)) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const configKey = normalizedName.slice(NPM_CONFIG_ENVIRONMENT_PREFIX.length);
|
|
275
|
+
const configField = configKey.startsWith(NPM_REGISTRY_BOUND_KEY_PREFIX) ? configKey.slice(configKey.lastIndexOf(":") + 1) : configKey;
|
|
276
|
+
return NPM_CREDENTIAL_CONFIG_FIELDS.some((credentialField) => credentialField.toLowerCase() === configField);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Builds the environment of an npm command that authenticates with the temporary config: the
|
|
281
|
+
* publish environment ({@link buildNpmPublishEnvironment}) without inherited npm credential
|
|
282
|
+
* variables, plus `NPM_TOKEN`, which npm expands from `${NPM_TOKEN}` in that config. Only the child
|
|
283
|
+
* process receives the token, and it is the only credential npm can use.
|
|
284
|
+
*
|
|
285
|
+
* @param {string} token - Token from {@link resolveNpmToken}.
|
|
286
|
+
* @param {NodeJS.ProcessEnv} [environment] - Environment to copy; the current process by default.
|
|
287
|
+
* @returns {NodeJS.ProcessEnv} Environment for npm.
|
|
288
|
+
*/
|
|
289
|
+
export function buildNpmTokenEnvironment(token, environment = process.env) {
|
|
290
|
+
const publishVariables = Object.entries(buildNpmPublishEnvironment(environment)).filter(
|
|
291
|
+
([variableName]) => !isNpmCredentialEnvironmentVariable(variableName),
|
|
292
|
+
);
|
|
293
|
+
return { ...Object.fromEntries(publishVariables), [NPM_TOKEN_VARIABLE]: token };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Parses the output of `npm owner ls`: one `<user> <<email>>` line per owner.
|
|
298
|
+
*
|
|
299
|
+
* @param {string} output - Standard output of npm.
|
|
300
|
+
* @returns {string[]} npm user names.
|
|
301
|
+
*/
|
|
302
|
+
export function parseNpmOwnerList(output) {
|
|
303
|
+
return output
|
|
304
|
+
.split("\n")
|
|
305
|
+
.map((line) => NPM_OWNER_LINE_PATTERN.exec(line.trim())?.groups?.user)
|
|
306
|
+
.filter((owner) => typeof owner === "string");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Builds the result of a lookup that could not list the published versions.
|
|
311
|
+
*
|
|
312
|
+
* @param {string | null} reason - Why the lookup failed.
|
|
313
|
+
* @returns {NpmLookup} Failed lookup without versions.
|
|
314
|
+
*/
|
|
315
|
+
export function failedLookup(reason) {
|
|
316
|
+
return { status: NPM_LOOKUP_STATUS.failed, publishedVersions: [], reason };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Returns the first line npm wrote to stderr, or a fallback when it wrote nothing.
|
|
321
|
+
*
|
|
322
|
+
* @param {{ stderr: string }} result - Captured npm result.
|
|
323
|
+
* @param {string} fallback - Text used when stderr is empty.
|
|
324
|
+
* @returns {string} First stderr line, or the fallback.
|
|
325
|
+
*/
|
|
326
|
+
function firstStderrLine(result, fallback) {
|
|
327
|
+
return result.stderr.split("\n")[0] || fallback;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Tells whether a failed npm command reported that the registry does not have the package.
|
|
332
|
+
*
|
|
333
|
+
* @param {{ stdout: string, stderr: string }} result - Captured npm result.
|
|
334
|
+
* @returns {boolean} Whether its output mentions {@link NPM_NOT_FOUND_CODE}.
|
|
335
|
+
*/
|
|
336
|
+
function isNpmNotFound(result) {
|
|
337
|
+
return `${result.stdout}\n${result.stderr}`.includes(NPM_NOT_FOUND_CODE);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Returns the first line of a failed npm command, for messages that never include the token.
|
|
342
|
+
*
|
|
343
|
+
* @param {{ status: number, stdout: string, stderr: string }} result - Captured npm result.
|
|
344
|
+
* @param {string} commandName - npm command, such as `npm whoami`.
|
|
345
|
+
* @returns {string} First meaningful output line, or the exit code.
|
|
346
|
+
*/
|
|
347
|
+
function describeNpmFailure(result, commandName) {
|
|
348
|
+
const firstLine = `${result.stderr}\n${result.stdout}`
|
|
349
|
+
.split("\n")
|
|
350
|
+
.map((line) => line.trim())
|
|
351
|
+
.find(Boolean);
|
|
352
|
+
return firstLine ? `${commandName}: ${firstLine}` : `${commandName} salió con código ${result.status}`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Checks, before anything is touched, the credential `npm publish` will use: resolves the token
|
|
357
|
+
* ({@link resolveNpmToken}), asks the registry who it authenticates as (`npm whoami`) and whether
|
|
358
|
+
* that user owns the package (`npm owner ls`). A package the registry does not show (E404) passes as
|
|
359
|
+
* `firstPublication`; registries also hide private packages from users without access, so the
|
|
360
|
+
* snapshot confirms it against the versions `npm view` lists (see `state.js`). Both commands use the same temporary config and registry as
|
|
361
|
+
* `npm publish`. A passing check proves that the token authenticates as an owner, not that it can
|
|
362
|
+
* write: a read-only or granular token without write permission passes too, and npm offers no
|
|
363
|
+
* side-effect-free way to tell before publishing.
|
|
364
|
+
*
|
|
365
|
+
* @param {string} packageName - npm package name.
|
|
366
|
+
* @param {string} repositoryRoot - Repository root, where npm reads its project config.
|
|
367
|
+
* @param {string} registryUrl - Registry from {@link resolvePublishRegistry}.
|
|
368
|
+
* A project `.npmrc` with credentials for the registry blocks as `projectCredentials` without
|
|
369
|
+
* querying it, because npm would authenticate with them instead of `NPM_TOKEN`; an unreadable
|
|
370
|
+
* `.env` or `.npmrc` leaves the check `unknown` with the file it could not read.
|
|
371
|
+
*
|
|
372
|
+
* @param {NpmTokenLookup} [lookup] - Token lookup of {@link resolveNpmToken}.
|
|
373
|
+
* @returns {Promise<NpmAuthCheck>} Check result; never rejects.
|
|
374
|
+
*/
|
|
375
|
+
export async function checkNpmPublishAccess(packageName, repositoryRoot, registryUrl, lookup = {}) {
|
|
376
|
+
/** @type {NpmAuthCheck} */
|
|
377
|
+
let check = { status: NPM_AUTH_STATUS.unknown, user: null, source: null, registryUrl, packageName, owners: [], firstPublication: false, reason: null };
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
const { token, source } = resolveNpmToken(repositoryRoot, lookup);
|
|
381
|
+
check = { ...check, source };
|
|
382
|
+
|
|
383
|
+
if (!token) {
|
|
384
|
+
return { ...check, status: NPM_AUTH_STATUS.missingToken };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (!NPM_PACKAGE_NAME_PATTERN.test(packageName)) {
|
|
388
|
+
return { ...check, reason: `nombre de paquete inválido: ${packageName}` };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const projectCredentialKey = findProjectNpmCredentialKey(repositoryRoot, registryUrl);
|
|
392
|
+
if (projectCredentialKey) {
|
|
393
|
+
return { ...check, status: NPM_AUTH_STATUS.projectCredentials, reason: `${PROJECT_NPM_CONFIG_FILE} del proyecto define ${projectCredentialKey}` };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const environment = buildNpmTokenEnvironment(token, lookup.environment);
|
|
397
|
+
|
|
398
|
+
return await withNpmAuthConfig(buildNpmAuthConfigLine(registryUrl), async (userConfigPath) => {
|
|
399
|
+
const whoami = await runNpmCaptured(buildNpmWhoamiArguments(registryUrl, userConfigPath, lookup.pinScope ? packageName : null), repositoryRoot, environment);
|
|
400
|
+
|
|
401
|
+
if (whoami.status !== 0) {
|
|
402
|
+
const rejected = NPM_REJECTED_CREDENTIAL_PATTERN.test(`${whoami.stderr}\n${whoami.stdout}`);
|
|
403
|
+
return { ...check, status: rejected ? NPM_AUTH_STATUS.invalidToken : NPM_AUTH_STATUS.unknown, reason: describeNpmFailure(whoami, "npm whoami") };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const user = whoami.stdout.trim();
|
|
407
|
+
const ownerList = await runNpmCaptured(buildNpmOwnerListArguments(packageName, registryUrl, userConfigPath, lookup.pinScope), repositoryRoot, environment);
|
|
408
|
+
|
|
409
|
+
if (ownerList.status !== 0) {
|
|
410
|
+
return isNpmNotFound(ownerList)
|
|
411
|
+
? { ...check, status: NPM_AUTH_STATUS.ok, user, firstPublication: true }
|
|
412
|
+
: { ...check, user, reason: describeNpmFailure(ownerList, "npm owner ls") };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const owners = parseNpmOwnerList(ownerList.stdout);
|
|
416
|
+
|
|
417
|
+
if (owners.includes(user)) {
|
|
418
|
+
return { ...check, status: NPM_AUTH_STATUS.ok, user, owners };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// An organization may grant publication through a team without listing the user as owner.
|
|
422
|
+
return readPackageScope(packageName)
|
|
423
|
+
? { ...check, user, owners, reason: `${user} no figura entre los dueños de ${packageName}; puede publicar solo si tiene acceso por un equipo de la organización` }
|
|
424
|
+
: { ...check, status: NPM_AUTH_STATUS.notOwner, user, owners };
|
|
425
|
+
});
|
|
426
|
+
} catch (error) {
|
|
427
|
+
return { ...check, reason: describeError(error) };
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Lists the versions of a package published on a registry. With `NPM_TOKEN` ({@link resolveNpmToken})
|
|
433
|
+
* the query authenticates through the same temporary config the publication uses, so a private
|
|
434
|
+
* package can be diagnosed; without it the registry is queried with npm's usual config.
|
|
435
|
+
*
|
|
436
|
+
* @param {string} packageName - npm package name.
|
|
437
|
+
* @param {string} repositoryRoot - Directory whose `.npmrc` npm reads.
|
|
438
|
+
* @param {string} [registryUrl] - Registry the package is published to, from {@link resolvePublishRegistry};
|
|
439
|
+
* defaults to npm's default registry.
|
|
440
|
+
* @returns {Promise<NpmLookup>} Published versions; a never-published package has none. An unreadable
|
|
441
|
+
* `.env` fails the lookup with the file it could not read instead of rejecting.
|
|
442
|
+
*/
|
|
443
|
+
export async function lookupPublishedVersions(packageName, repositoryRoot, registryUrl = DEFAULT_NPM_REGISTRY_URL, { tokenVariable = NPM_TOKEN_VARIABLE, tag = NPM_DIST_TAG, pinScope = false } = {}) {
|
|
444
|
+
if (!NPM_PACKAGE_NAME_PATTERN.test(packageName)) {
|
|
445
|
+
return failedLookup(`nombre de paquete inválido: ${packageName}`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
let result;
|
|
449
|
+
try {
|
|
450
|
+
const { token } = resolveNpmToken(repositoryRoot, { tokenVariable });
|
|
451
|
+
const environment = token ? buildNpmTokenEnvironment(token) : process.env;
|
|
452
|
+
/** @param {string | null} userConfigPath - Temporary authenticated config, or `null`. */
|
|
453
|
+
const view = (userConfigPath) => runNpmCaptured(buildNpmViewArguments(packageName, registryUrl, userConfigPath, pinScope), repositoryRoot, environment);
|
|
454
|
+
result = token ? await withNpmAuthConfig(buildNpmAuthConfigLine(registryUrl), view) : await view(null);
|
|
455
|
+
} catch (error) {
|
|
456
|
+
return failedLookup(describeError(error));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (result.status !== 0) {
|
|
460
|
+
return isNpmNotFound(result)
|
|
461
|
+
? { status: NPM_LOOKUP_STATUS.ok, publishedVersions: [], reason: null }
|
|
462
|
+
: failedLookup(firstStderrLine(result, `npm view salió con código ${result.status}`));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
try {
|
|
466
|
+
// With two fields `npm view --json` answers `{ versions, "dist-tags" }`; a single version comes as a string.
|
|
467
|
+
const { versions = [], "dist-tags": distTags = {} } = JSON.parse(result.stdout);
|
|
468
|
+
const latestVersion = typeof distTags[tag] === "string" ? distTags[tag] : null;
|
|
469
|
+
return { status: NPM_LOOKUP_STATUS.ok, publishedVersions: Array.isArray(versions) ? versions : [versions], latestVersion, reason: null };
|
|
470
|
+
} catch (error) {
|
|
471
|
+
return failedLookup(`respuesta inválida de npm view (${describeError(error)})`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Parses the output of `npm pack --dry-run --json`: an array with one package and its `integrity`.
|
|
477
|
+
*
|
|
478
|
+
* @param {string} output - Standard output of npm.
|
|
479
|
+
* @returns {NpmPackResult} Package name, version and integrity, or why the output is unusable.
|
|
480
|
+
*/
|
|
481
|
+
export function parseNpmPackDryRunOutput(output) {
|
|
482
|
+
let parsed;
|
|
483
|
+
try {
|
|
484
|
+
parsed = JSON.parse(output);
|
|
485
|
+
} catch (error) {
|
|
486
|
+
return { pack: null, problem: `la salida de npm pack --dry-run no es JSON válido (${describeError(error)})` };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const packages = Array.isArray(parsed) ? parsed : [];
|
|
490
|
+
const described = packages.length === 1 && packages[0] && typeof packages[0] === "object" ? packages[0] : null;
|
|
491
|
+
if (!described || typeof described.integrity !== "string" || !NPM_INTEGRITY_PATTERN.test(described.integrity)) {
|
|
492
|
+
return { pack: null, problem: "la salida de npm pack --dry-run no describe un único paquete con su integrity sha512" };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return { pack: { name: described.name, version: described.version, integrity: described.integrity }, problem: null };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Asks npm for the integrity of the archive it would pack from the release checkout, without
|
|
500
|
+
* writing anything nor running lifecycle scripts. The command line is constant, so it is safe
|
|
501
|
+
* on the Windows shell.
|
|
502
|
+
*
|
|
503
|
+
* @param {string} repositoryRoot - Package root, as `prepare` left it.
|
|
504
|
+
* @returns {Promise<NpmPackResult>} npm description, or why npm failed or its output is unusable.
|
|
505
|
+
*/
|
|
506
|
+
export async function readNpmPackIntegrity(repositoryRoot) {
|
|
507
|
+
const result = await runNpmCaptured([...NPM_PACK_DRY_RUN_ARGUMENTS], repositoryRoot);
|
|
508
|
+
|
|
509
|
+
if (result.status !== 0) {
|
|
510
|
+
return { pack: null, problem: `npm pack --dry-run salió con código ${result.status}: ${firstStderrLine(result, "sin detalle")}` };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return parseNpmPackDryRunOutput(result.stdout);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Parses a publish registry URL, accepting only plain http(s) URLs.
|
|
518
|
+
*
|
|
519
|
+
* @param {string} registryUrl - Registry URL as written in the manifest.
|
|
520
|
+
* @returns {URL} Parsed URL.
|
|
521
|
+
* @throws {Error} When the registry is not a plain http(s) URL (credentials, query and fragment are rejected).
|
|
522
|
+
*/
|
|
523
|
+
function parseRegistryUrl(registryUrl) {
|
|
524
|
+
/** @type {URL} */
|
|
525
|
+
let registry;
|
|
526
|
+
try {
|
|
527
|
+
registry = new URL(registryUrl);
|
|
528
|
+
} catch (error) {
|
|
529
|
+
throw new Error(`astra-rp create-version: el registry de publicación "${registryUrl}" no es una URL válida`, { cause: error });
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (!isPlainHttpUrl(registry)) {
|
|
533
|
+
throw new Error(`astra-rp create-version: el registry de publicación "${registryUrl}" tiene que ser una URL http(s) sin credenciales, query ni fragmento`);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return registry;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Returns the scope of a package name (`team` for `@team/pkg`), or `undefined` when it has none.
|
|
541
|
+
*
|
|
542
|
+
* @param {unknown} packageName - `name` of the manifest.
|
|
543
|
+
* @returns {string | undefined} Scope without `@`.
|
|
544
|
+
*/
|
|
545
|
+
function readPackageScope(packageName) {
|
|
546
|
+
return typeof packageName === "string" ? PACKAGE_SCOPE_PATTERN.exec(packageName)?.groups?.scope : undefined;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Returns the registry the manifest `publishConfig` declares: `publishConfig["@scope:registry"]`
|
|
551
|
+
* for a scoped package that declares it, else `publishConfig.registry`.
|
|
552
|
+
*
|
|
553
|
+
* @param {Record<string, unknown>} manifest - `package.json` being published.
|
|
554
|
+
* @returns {string | null} Declared registry, or `null` when `publishConfig` declares none.
|
|
555
|
+
*/
|
|
556
|
+
function readPublishConfigRegistry(manifest) {
|
|
557
|
+
const publishConfig = manifest[PUBLISH_CONFIG_FIELD];
|
|
558
|
+
const registries = isRecord(publishConfig) ? publishConfig : {};
|
|
559
|
+
const scope = readPackageScope(manifest.name);
|
|
560
|
+
const candidates = [scope ? registries[scopedRegistryKey(scope)] : undefined, registries[PUBLISH_CONFIG_REGISTRY_KEY]];
|
|
561
|
+
const declared = candidates.find((candidate) => typeof candidate === "string" && candidate !== "");
|
|
562
|
+
return typeof declared === "string" ? declared : null;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Asks npm for the registry it would publish the package to without `publishConfig`:
|
|
567
|
+
* `@scope:registry` for a scoped package when some config sets it, else `registry`. It runs in the
|
|
568
|
+
* repository root with the environment and the kind of temporary user config `npm publish` gets,
|
|
569
|
+
* so it reads the project `.npmrc`, `npm_config_*` variables and the global config, like the
|
|
570
|
+
* publication (which replaces `~/.npmrc` with its temporary config).
|
|
571
|
+
*
|
|
572
|
+
* @param {Record<string, unknown>} manifest - `package.json` being published.
|
|
573
|
+
* @param {string} repositoryRoot - Package root.
|
|
574
|
+
* @returns {Promise<string>} Registry URL npm resolves.
|
|
575
|
+
* @throws {Error} When the package name is invalid or `npm config get` fails.
|
|
576
|
+
*/
|
|
577
|
+
async function readNpmConfigRegistry(manifest, repositoryRoot) {
|
|
578
|
+
const packageName = String(manifest.name);
|
|
579
|
+
if (!NPM_PACKAGE_NAME_PATTERN.test(packageName)) {
|
|
580
|
+
throw new Error(`astra-rp create-version: nombre de paquete inválido: ${packageName}`);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// The scope comes from a name that matches the npm name pattern, so the key keeps a fixed shape.
|
|
584
|
+
const scope = readPackageScope(packageName);
|
|
585
|
+
const configKeys = [...(scope ? [scopedRegistryKey(scope)] : []), PUBLISH_CONFIG_REGISTRY_KEY];
|
|
586
|
+
|
|
587
|
+
return withNpmAuthConfig("", async (userConfigPath) => {
|
|
588
|
+
for (const configKey of configKeys) {
|
|
589
|
+
const result = await runNpmCaptured(
|
|
590
|
+
[...NPM_CONFIG_GET_ARGUMENTS, configKey, NPM_USER_CONFIG_OPTION, userConfigPath],
|
|
591
|
+
repositoryRoot,
|
|
592
|
+
buildNpmPublishEnvironment()
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
if (result.status !== 0) {
|
|
596
|
+
throw new Error(`astra-rp create-version: npm config get ${configKey} salió con código ${result.status}: ${firstStderrLine(result, "sin detalle")}`);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const value = result.stdout.trim();
|
|
600
|
+
if (value !== "" && value !== NPM_UNSET_CONFIG_VALUE) {
|
|
601
|
+
return value;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return DEFAULT_NPM_REGISTRY_URL;
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Returns the registry `npm publish` sends the package to: `publishConfig["@scope:registry"]` for a
|
|
611
|
+
* scoped package that declares it, else `publishConfig.registry`, else the registry npm's own config
|
|
612
|
+
* resolves (project `.npmrc`, environment, global config; see {@link readNpmConfigRegistry}).
|
|
613
|
+
*
|
|
614
|
+
* @param {Record<string, unknown>} manifest - `package.json` being published.
|
|
615
|
+
* @param {string} repositoryRoot - Package root, where npm reads its project config.
|
|
616
|
+
* @returns {Promise<string>} Registry URL, already checked to be a plain http(s) URL.
|
|
617
|
+
* @throws {Error} When the resolved registry is not a plain http(s) URL or npm cannot report its config.
|
|
618
|
+
*/
|
|
619
|
+
export async function resolvePublishRegistry(manifest, repositoryRoot) {
|
|
620
|
+
const registryUrl = readPublishConfigRegistry(manifest) ?? (await readNpmConfigRegistry(manifest, repositoryRoot));
|
|
621
|
+
|
|
622
|
+
parseRegistryUrl(registryUrl);
|
|
623
|
+
return registryUrl;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Describes where a published release can be seen: its npmjs.com page when the registry is the
|
|
628
|
+
* public npm registry, else the registry URL with the package and version.
|
|
629
|
+
*
|
|
630
|
+
* @param {{ registryUrl: string, packageName: string, version: string }} release - Published release.
|
|
631
|
+
* @returns {string} Summary text.
|
|
632
|
+
*/
|
|
633
|
+
export function describePublishedRelease({ registryUrl, packageName, version }) {
|
|
634
|
+
return parseRegistryUrl(registryUrl).href === DEFAULT_NPM_REGISTRY_URL
|
|
635
|
+
? `${NPMJS_PACKAGE_PAGE_URL}${packageName}/v/${version}`
|
|
636
|
+
: `Registro: ${registryUrl} · ${packageName}@${version}`;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Builds the npm config line that binds `${NPM_TOKEN}` to a registry, in the form npm matches
|
|
641
|
+
* credentials with: `//<host>[:port]<path>/:_authToken=${NPM_TOKEN}` (no protocol, trailing `/`).
|
|
642
|
+
*
|
|
643
|
+
* @param {string} registryUrl - Registry `npm publish` uses.
|
|
644
|
+
* @returns {string} Config line ending with a newline; the token itself is never written.
|
|
645
|
+
* @throws {Error} When the registry is not a plain http(s) URL (credentials, query and fragment are rejected).
|
|
646
|
+
*/
|
|
647
|
+
export function buildNpmAuthConfigLine(registryUrl) {
|
|
648
|
+
return `${buildNpmRegistryKey(registryUrl)}:_authToken=${NPM_AUTH_TOKEN_REFERENCE}\n`;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Builds the key npm binds credentials of a registry to: `//<host>[:port]<path>/`.
|
|
653
|
+
*
|
|
654
|
+
* @param {string} registryUrl - Registry URL.
|
|
655
|
+
* @returns {string} Registry key, without protocol and with a trailing `/`.
|
|
656
|
+
* @throws {Error} When the registry is not a plain http(s) URL.
|
|
657
|
+
*/
|
|
658
|
+
function buildNpmRegistryKey(registryUrl) {
|
|
659
|
+
const registry = parseRegistryUrl(registryUrl);
|
|
660
|
+
const registryPath = registry.pathname.endsWith("/") ? registry.pathname : `${registry.pathname}/`;
|
|
661
|
+
return `${NPM_REGISTRY_BOUND_KEY_PREFIX}${registry.host}${registryPath}`;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Tells whether an npm config key authenticates a registry: an unbound credential field
|
|
666
|
+
* (`_authToken`) or one bound to the registry or to a parent path of it, as npm matches them
|
|
667
|
+
* (`//host/:_authToken` also covers `//host/team/`).
|
|
668
|
+
*
|
|
669
|
+
* @param {string} configKey - Key of a `key=value` line of the project `.npmrc`.
|
|
670
|
+
* @param {string} registryKey - Key from {@link buildNpmRegistryKey}.
|
|
671
|
+
* @returns {boolean} Whether npm would use it to authenticate against the registry.
|
|
672
|
+
*/
|
|
673
|
+
function isNpmCredentialKeyFor(configKey, registryKey) {
|
|
674
|
+
if (!configKey.startsWith(NPM_REGISTRY_BOUND_KEY_PREFIX)) {
|
|
675
|
+
return NPM_CREDENTIAL_CONFIG_FIELDS.includes(configKey);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const fieldSeparatorIndex = configKey.lastIndexOf(":");
|
|
679
|
+
const boundRegistry = configKey.slice(0, fieldSeparatorIndex);
|
|
680
|
+
const boundRegistryKey = boundRegistry.endsWith("/") ? boundRegistry : `${boundRegistry}/`;
|
|
681
|
+
return NPM_CREDENTIAL_CONFIG_FIELDS.includes(configKey.slice(fieldSeparatorIndex + 1)) && registryKey.startsWith(boundRegistryKey);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Finds a credential for the registry in the project `.npmrc` (repository root). npm prefers the
|
|
686
|
+
* project config over the temporary `--userconfig` that binds `NPM_TOKEN`, so such a credential
|
|
687
|
+
* would authenticate every command instead of the token. Only keys are read, never values.
|
|
688
|
+
*
|
|
689
|
+
* @param {string} repositoryRoot - Repository root holding the optional `.npmrc`.
|
|
690
|
+
* @param {string} registryUrl - Registry the package is published to.
|
|
691
|
+
* @returns {string | null} First credential key for the registry, or `null` when there is none.
|
|
692
|
+
* @throws {Error} When the `.npmrc` exists but cannot be read, or the registry is not a plain http(s) URL.
|
|
693
|
+
*/
|
|
694
|
+
export function findProjectNpmCredentialKey(repositoryRoot, registryUrl) {
|
|
695
|
+
const content = readCredentialFile(path.join(repositoryRoot, PROJECT_NPM_CONFIG_FILE), "buscar credenciales de npm");
|
|
696
|
+
if (content === null) {
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const registryKey = buildNpmRegistryKey(registryUrl);
|
|
701
|
+
return (
|
|
702
|
+
content
|
|
703
|
+
.split(/\r?\n/u)
|
|
704
|
+
.map((line) => line.split("=", 1)[0].trim())
|
|
705
|
+
.find((configKey) => isNpmCredentialKeyFor(configKey, registryKey)) ?? null
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Runs an operation with a temporary npm user config outside the repository
|
|
711
|
+
* that only references `${NPM_TOKEN}` for the publish registry: npm expands it
|
|
712
|
+
* from the environment, so the token never reaches the disk or a command line,
|
|
713
|
+
* and no repository needs an `.npmrc` (which pnpm refuses to expand and warns
|
|
714
|
+
* about). The config is always removed afterwards.
|
|
715
|
+
*
|
|
716
|
+
* @template T
|
|
717
|
+
* @param {string} authConfigLine - Line built by {@link buildNpmAuthConfigLine}; an empty line
|
|
718
|
+
* gives npm a user config without credentials, which only replaces `~/.npmrc`.
|
|
719
|
+
* @param {(userConfigPath: string) => Promise<T>} operation - Receives the path for `npm --userconfig`.
|
|
720
|
+
* @param {string} [parentDirectory] - Where the temporary directory is created; defaults to the OS temp directory.
|
|
721
|
+
* @returns {Promise<T>} The operation result.
|
|
722
|
+
*/
|
|
723
|
+
export async function withNpmAuthConfig(authConfigLine, operation, parentDirectory = tmpdir()) {
|
|
724
|
+
const directory = mkdtempSync(path.join(parentDirectory, NPM_AUTH_DIRECTORY_PREFIX));
|
|
725
|
+
const userConfigPath = path.join(directory, "npmrc");
|
|
726
|
+
|
|
727
|
+
try {
|
|
728
|
+
writeFileSync(userConfigPath, authConfigLine, { mode: 0o600 });
|
|
729
|
+
return await operation(userConfigPath);
|
|
730
|
+
} finally {
|
|
731
|
+
rmSync(directory, { recursive: true, force: true });
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Builds the `npm publish` arguments, without the user config. A prepared
|
|
737
|
+
* archive is prefixed with `./` because npm parses a bare relative operand
|
|
738
|
+
* such as `releases/1.9.0-abc/pkg-1.9.0.tgz` as a package spec instead of a file.
|
|
739
|
+
*
|
|
740
|
+
* @param {string | null} [artifactPath] - Archive relative to the root, already checked with `isSafeArtifactPath`; `null` publishes the working tree.
|
|
741
|
+
* @param {string | null} [registryUrl] - Registry from {@link resolvePublishRegistry} passed as `--registry`, so npm
|
|
742
|
+
* publishes where it was resolved even from a checkout without the project `.npmrc`; `null` leaves it to npm's config.
|
|
743
|
+
* @param {{ access?: "public" | "restricted" | null, tag?: string | null, provenance?: boolean, packageName?: string | null }} [options] - Publication flags; `null` omits a registry-owned setting.
|
|
744
|
+
* @returns {string[]} Arguments that follow `npm`.
|
|
745
|
+
* @throws {Error} When the registry is not a valid http(s) URL or has characters unsafe on the Windows shell.
|
|
746
|
+
*/
|
|
747
|
+
export function buildNpmPublishArguments(artifactPath = null, registryUrl = null, { access = "public", tag = NPM_DIST_TAG, provenance, packageName = null } = {}) {
|
|
748
|
+
const publishTarget = artifactPath
|
|
749
|
+
? [artifactPath.startsWith(LOCAL_PATH_PREFIX) ? artifactPath : `${LOCAL_PATH_PREFIX}${artifactPath}`]
|
|
750
|
+
: [];
|
|
751
|
+
const registryOptions = registryUrl ? buildRegistryOptions(registryUrl, null, "npm publish", packageName) : [];
|
|
752
|
+
return ["publish", ...publishTarget, ...(access ? ["--access", access] : []), ...(tag ? ["--tag", tag] : []), ...(provenance === undefined ? [] : [`--provenance=${provenance}`]), ...registryOptions];
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Builds the environment of `npm publish` without the inherited package
|
|
757
|
+
* manager user agent. npm reads `npm_config_user_agent` as its `user-agent`
|
|
758
|
+
* config, so under `pnpm create-version` its lifecycle scripts would report
|
|
759
|
+
* pnpm and `astra-rp guard-publish` would block its own npm publication.
|
|
760
|
+
* Without it npm reports its own user agent (`npm/…`). npm reads its config
|
|
761
|
+
* variables in any case (`NPM_CONFIG_USER_AGENT` too), so every casing is dropped.
|
|
762
|
+
*
|
|
763
|
+
* @param {NodeJS.ProcessEnv} [environment] - Environment to copy; the current process by default.
|
|
764
|
+
* @returns {NodeJS.ProcessEnv} Copy without `npm_config_user_agent` in any casing.
|
|
765
|
+
*/
|
|
766
|
+
export function buildNpmPublishEnvironment(environment = process.env) {
|
|
767
|
+
return Object.fromEntries(
|
|
768
|
+
Object.entries(environment).filter(([variableName]) => variableName.toLowerCase() !== PACKAGE_MANAGER_USER_AGENT_VARIABLE),
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Publishes the working tree, or a prepared archive, to npm. `NPM_TOKEN` comes
|
|
774
|
+
* from {@link resolveNpmToken} and only reaches npm through the child
|
|
775
|
+
* environment and a temporary user config. npm inherits the terminal, so its
|
|
776
|
+
* interactive browser or one-time-password (2FA) confirmation works; that is
|
|
777
|
+
* also why its output cannot be parsed, and a failure is explained afterwards
|
|
778
|
+
* with {@link checkNpmPublishAccess}.
|
|
779
|
+
*
|
|
780
|
+
* @param {string} repositoryRoot - Repository root, where `NPM_TOKEN` is looked up; also the package root unless `packageRoot` is given.
|
|
781
|
+
* @param {{ authConfigLine: string, artifactPath?: string | null, packageRoot?: string, registryUrl?: string | null, tokenVariable?: string, authentication?: "token" | "oidc", access?: "public" | "restricted" | null, tag?: string | null, packageName?: string | null }} publication - Registry credential
|
|
782
|
+
* line from {@link buildNpmAuthConfigLine}, the archive relative to the package root (already checked with
|
|
783
|
+
* `isSafeArtifactPath`; without it the working tree is published), the directory of a workspace package and the
|
|
784
|
+
* registry to pass explicitly (see {@link buildNpmPublishArguments}).
|
|
785
|
+
* @returns {Promise<{ exitCode: number, missingToken: boolean }>} npm exit code, or a missing-token result without running npm.
|
|
786
|
+
* @throws {Error} When the temporary config path could break out of its shell quotes.
|
|
787
|
+
*/
|
|
788
|
+
export async function publishToNpm(repositoryRoot, { authConfigLine, artifactPath = null, packageRoot = repositoryRoot, registryUrl = null, tokenVariable = NPM_TOKEN_VARIABLE, authentication = "token", access = "public", tag = NPM_DIST_TAG, packageName = null }) {
|
|
789
|
+
const { token } = authentication === "token" ? resolveNpmToken(repositoryRoot, { tokenVariable }) : { token: null };
|
|
790
|
+
|
|
791
|
+
if (!token && authentication === "token") {
|
|
792
|
+
return { exitCode: 1, missingToken: true };
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const exitCode = await withNpmAuthConfig(authConfigLine, async (userConfigPath) => {
|
|
796
|
+
if (UNSAFE_QUOTED_PATH_PATTERN.test(userConfigPath)) {
|
|
797
|
+
throw new Error(`astra-rp create-version: unsafe temporary npm config path ${userConfigPath}`);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// The command line is constant apart from validated paths; the token only travels through the environment.
|
|
801
|
+
const publishArguments = buildNpmPublishArguments(artifactPath, registryUrl, { access, tag, packageName });
|
|
802
|
+
const env = buildNpmTokenEnvironment(token ?? "");
|
|
803
|
+
if (!token) delete env[NPM_TOKEN_VARIABLE];
|
|
804
|
+
// A failed PUT may already have published: reconcile through metadata instead of retrying a mutation.
|
|
805
|
+
env.npm_config_fetch_retries = "0";
|
|
806
|
+
return USES_SHELL_FOR_PACKAGE_MANAGERS
|
|
807
|
+
? runInherited(`npm ${publishArguments.join(" ")} --userconfig "${userConfigPath}"`, [], { cwd: packageRoot, shell: true, env })
|
|
808
|
+
: runInherited("npm", [...publishArguments, "--userconfig", userConfigPath], { cwd: packageRoot, env });
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
return { exitCode, missingToken: false };
|
|
812
|
+
}
|