@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.
Files changed (125) hide show
  1. package/CHANGELOG.md +196 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +352 -2
  4. package/bin/astra-rp.js +51 -0
  5. package/package.json +93 -4
  6. package/src/build-gate.js +157 -0
  7. package/src/changelog-blocks.js +53 -0
  8. package/src/changelog.js +108 -0
  9. package/src/constants/build-gate.js +30 -0
  10. package/src/constants/changelog-draft.js +180 -0
  11. package/src/constants/changelog.js +74 -0
  12. package/src/constants/ci-release.js +346 -0
  13. package/src/constants/cli.js +12 -0
  14. package/src/constants/create-version.js +523 -0
  15. package/src/constants/guard-publish.js +29 -0
  16. package/src/constants/index.js +18 -0
  17. package/src/constants/monorepo.js +57 -0
  18. package/src/constants/package-manager.js +56 -0
  19. package/src/constants/registry.js +72 -0
  20. package/src/constants/terminal-ui.js +93 -0
  21. package/src/constants/version-files.js +63 -0
  22. package/src/constants/versions.js +60 -0
  23. package/src/create-version/artifact.js +343 -0
  24. package/src/create-version/changelog-draft-workspace.js +124 -0
  25. package/src/create-version/changelog-draft.js +380 -0
  26. package/src/create-version/changelog.js +264 -0
  27. package/src/create-version/ci-config.js +54 -0
  28. package/src/create-version/ci-publication-history.js +113 -0
  29. package/src/create-version/ci-release-identity-step.js +58 -0
  30. package/src/create-version/ci-setup.js +794 -0
  31. package/src/create-version/ci.js +555 -0
  32. package/src/create-version/committed-tree.js +75 -0
  33. package/src/create-version/config.js +489 -0
  34. package/src/create-version/errors.js +46 -0
  35. package/src/create-version/github-workflow.js +199 -0
  36. package/src/create-version/index.js +54 -0
  37. package/src/create-version/json.js +32 -0
  38. package/src/create-version/jsonc.js +76 -0
  39. package/src/create-version/jsr.js +96 -0
  40. package/src/create-version/local-changes.js +178 -0
  41. package/src/create-version/npm-auth.js +142 -0
  42. package/src/create-version/npm.js +812 -0
  43. package/src/create-version/plan.js +1200 -0
  44. package/src/create-version/process.js +334 -0
  45. package/src/create-version/registry-config.js +121 -0
  46. package/src/create-version/registry-http.js +62 -0
  47. package/src/create-version/registry.js +220 -0
  48. package/src/create-version/release-execution.js +45 -0
  49. package/src/create-version/release-tag-config.js +76 -0
  50. package/src/create-version/run.js +2087 -0
  51. package/src/create-version/state.js +461 -0
  52. package/src/create-version/strict-utf8.js +11 -0
  53. package/src/create-version/templates/release.yml +52 -0
  54. package/src/create-version/templates/vercel-ignore-build.mjs +66 -0
  55. package/src/guard-publish.js +77 -0
  56. package/src/index.js +42 -0
  57. package/src/monorepo/plan.js +475 -0
  58. package/src/monorepo/release-commit.js +46 -0
  59. package/src/monorepo/run.js +927 -0
  60. package/src/monorepo/state.js +206 -0
  61. package/src/monorepo/workspaces.js +327 -0
  62. package/src/package-manager.js +107 -0
  63. package/src/terminal-ui.js +548 -0
  64. package/src/testing.js +70 -0
  65. package/src/version-files.js +142 -0
  66. package/src/versions.js +303 -0
  67. package/types/build-gate.d.ts +43 -0
  68. package/types/changelog-blocks.d.ts +33 -0
  69. package/types/changelog.d.ts +61 -0
  70. package/types/constants/build-gate.d.ts +23 -0
  71. package/types/constants/changelog-draft.d.ts +138 -0
  72. package/types/constants/changelog.d.ts +56 -0
  73. package/types/constants/ci-release.d.ts +284 -0
  74. package/types/constants/cli.d.ts +11 -0
  75. package/types/constants/create-version.d.ts +397 -0
  76. package/types/constants/guard-publish.d.ts +25 -0
  77. package/types/constants/index.d.ts +17 -0
  78. package/types/constants/monorepo.d.ts +45 -0
  79. package/types/constants/package-manager.d.ts +40 -0
  80. package/types/constants/registry.d.ts +79 -0
  81. package/types/constants/terminal-ui.d.ts +73 -0
  82. package/types/constants/version-files.d.ts +51 -0
  83. package/types/constants/versions.d.ts +47 -0
  84. package/types/create-version/artifact.d.ts +113 -0
  85. package/types/create-version/changelog-draft-workspace.d.ts +33 -0
  86. package/types/create-version/changelog-draft.d.ts +127 -0
  87. package/types/create-version/changelog.d.ts +141 -0
  88. package/types/create-version/ci-config.d.ts +33 -0
  89. package/types/create-version/ci-publication-history.d.ts +51 -0
  90. package/types/create-version/ci-release-identity-step.d.ts +12 -0
  91. package/types/create-version/ci-setup.d.ts +148 -0
  92. package/types/create-version/ci.d.ts +145 -0
  93. package/types/create-version/committed-tree.d.ts +55 -0
  94. package/types/create-version/config.d.ts +128 -0
  95. package/types/create-version/errors.d.ts +39 -0
  96. package/types/create-version/github-workflow.d.ts +41 -0
  97. package/types/create-version/index.d.ts +28 -0
  98. package/types/create-version/json.d.ts +21 -0
  99. package/types/create-version/jsonc.d.ts +21 -0
  100. package/types/create-version/jsr.d.ts +52 -0
  101. package/types/create-version/local-changes.d.ts +52 -0
  102. package/types/create-version/npm-auth.d.ts +60 -0
  103. package/types/create-version/npm.d.ts +331 -0
  104. package/types/create-version/plan.d.ts +369 -0
  105. package/types/create-version/process.d.ts +151 -0
  106. package/types/create-version/registry-config.d.ts +56 -0
  107. package/types/create-version/registry-http.d.ts +22 -0
  108. package/types/create-version/registry.d.ts +86 -0
  109. package/types/create-version/release-execution.d.ts +22 -0
  110. package/types/create-version/release-tag-config.d.ts +18 -0
  111. package/types/create-version/run.d.ts +407 -0
  112. package/types/create-version/state.d.ts +204 -0
  113. package/types/create-version/strict-utf8.d.ts +10 -0
  114. package/types/guard-publish.d.ts +49 -0
  115. package/types/index.d.ts +23 -0
  116. package/types/monorepo/plan.d.ts +68 -0
  117. package/types/monorepo/release-commit.d.ts +29 -0
  118. package/types/monorepo/run.d.ts +68 -0
  119. package/types/monorepo/state.d.ts +96 -0
  120. package/types/monorepo/workspaces.d.ts +86 -0
  121. package/types/package-manager.d.ts +58 -0
  122. package/types/terminal-ui.d.ts +196 -0
  123. package/types/testing.d.ts +31 -0
  124. package/types/version-files.d.ts +59 -0
  125. package/types/versions.d.ts +158 -0
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Snapshot of a monorepo release: the repository part shared with the
3
+ * single-package mode (branch, working tree, `main` versus `origin/main`,
4
+ * pull request) plus, for every released package, its last release, the
5
+ * commits waiting to be released under its paths, its changelog and its
6
+ * published versions.
7
+ *
8
+ * Every reader is read-only; the network operations are `git fetch`,
9
+ * `git ls-remote`, `gh pr view` and the npm lookups.
10
+ *
11
+ * @module monorepo/state
12
+ */
13
+
14
+ import path from "node:path";
15
+
16
+ import { RELEASE_REMOTE, REMOTE_MAIN_REF } from "../constants/create-version.js";
17
+ import { UNRELEASED_PLACEHOLDER_VERSION } from "../constants/versions.js";
18
+ import { checkNpmPublishAccess, lookupPublishedVersions } from "../create-version/npm.js";
19
+ import { readJsonManifest } from "../create-version/json.js";
20
+ import { createGitReader, findVersionChangeCommit, listCommits, readPackageVersionAt } from "../create-version/process.js";
21
+ import {
22
+ checkPackageAccess,
23
+ lookupPackageVersions,
24
+ lookupPullRequest,
25
+ readMigrations,
26
+ readPackageChangelogState,
27
+ readRepositorySnapshot,
28
+ } from "../create-version/state.js";
29
+ import { parseMonorepoReleaseSubject } from "./release-commit.js";
30
+ import { formatPackageTag } from "./workspaces.js";
31
+
32
+ /**
33
+ * @typedef {import("./workspaces.js").ReleaseUnit} ReleaseUnit
34
+ * @typedef {import("../create-version/process.js").GitReader} GitReader
35
+ * @typedef {import("../create-version/process.js").CommitRecord} CommitRecord
36
+ * @typedef {import("../create-version/npm.js").NpmLookup} NpmLookup
37
+ * @typedef {import("../create-version/npm.js").NpmAuthCheck} NpmAuthCheck
38
+ * @typedef {import("../create-version/config.js").MigrationCheck} MigrationCheck
39
+ * @typedef {{
40
+ * sha: string,
41
+ * version: string | null,
42
+ * subject: string | null,
43
+ * tag: string | null,
44
+ * tagged: boolean,
45
+ * tagOnOrigin: boolean,
46
+ * }} PackageLastRelease
47
+ * Newest commit of `origin/main` that changed the package `version`; `tagged` when its tag points
48
+ * at that commit locally, `tagOnOrigin` when `origin` has the tag.
49
+ * @typedef {{
50
+ * unit: ReleaseUnit,
51
+ * manifest: Record<string, unknown>,
52
+ * releasedVersion: string | null,
53
+ * lastRelease: PackageLastRelease | null,
54
+ * unreleasedCommits: CommitRecord[],
55
+ * changelog: { exists: boolean, entryCount: number, unknownSections: string[], updated: boolean, reason: string | null },
56
+ * npm: NpmLookup | null,
57
+ * npmAuth: NpmAuthCheck | null,
58
+ * }} PackageSnapshot
59
+ * @typedef {import("../create-version/state.js").RepositorySnapshot & {
60
+ * headSha: string | null,
61
+ * headSubject: string | null,
62
+ * packages: PackageSnapshot[],
63
+ * migrations: MigrationCheck | null,
64
+ * }} MonorepoSnapshot
65
+ */
66
+
67
+ /**
68
+ * Lists the tags `origin` has, so pending publications only resume releases whose tag reached it.
69
+ *
70
+ * @param {GitReader} reader - Git reader.
71
+ * @returns {Promise<Set<string>>} Tag names.
72
+ * @throws {Error} When `origin` cannot be read: an empty set would hide tags that already reached it.
73
+ */
74
+ async function readRemoteTags(reader) {
75
+ const output = await reader.tryGit(["ls-remote", "--tags", "--refs", RELEASE_REMOTE]);
76
+ if (output === null) {
77
+ throw new Error(
78
+ `No se pudieron listar los tags de ${RELEASE_REMOTE} (git ls-remote --tags ${RELEASE_REMOTE} falló): sin ellos no se sabe qué releases llegaron a ${RELEASE_REMOTE}. Revisá la conexión y el acceso a ${RELEASE_REMOTE} y volvé a correr el comando.`
79
+ );
80
+ }
81
+ return new Set(
82
+ output
83
+ .split("\n")
84
+ .map((line) => line.trim().split(/\s+/u)[1] ?? "")
85
+ .filter((ref) => ref.startsWith("refs/tags/"))
86
+ .map((ref) => ref.slice("refs/tags/".length))
87
+ );
88
+ }
89
+
90
+ /**
91
+ * Finds the last release of a package on a revision: the newest commit that changed its `version`,
92
+ * unless that version is an untagged `0.0.0` placeholder of a package that was never released.
93
+ *
94
+ * @param {GitReader} reader - Git reader.
95
+ * @param {string} revision - Revision such as `origin/main`.
96
+ * @param {ReleaseUnit} unit - Released package.
97
+ * @param {string} tagFormat - Tag format.
98
+ * @param {ReadonlySet<string>} remoteTags - Tags on `origin`.
99
+ * @returns {Promise<PackageLastRelease | null>} Last release, or `null` without history or when the
100
+ * newest version change is the untagged commit that added the package or leaves it at `0.0.0`
101
+ * (neither pointed at by the package's own tag nor listed in a `release: …` commit).
102
+ */
103
+ export async function findLastPackageRelease(reader, revision, unit, tagFormat, remoteTags) {
104
+ const sha = await findVersionChangeCommit(reader, revision, unit.manifestPath);
105
+
106
+ if (!sha) {
107
+ return null;
108
+ }
109
+
110
+ const version = await readPackageVersionAt(reader, sha, unit.manifestPath);
111
+ const subject = await reader.tryGit(["log", "-1", "--format=%s", sha]);
112
+ const tag = version ? formatPackageTag(tagFormat, unit, version) : null;
113
+ const taggedSha = tag ? await reader.tryGit(["rev-parse", "--verify", "--quiet", `refs/tags/${tag}^{commit}`]) : null;
114
+ // Only the package's own tag, or a release commit that lists it (its tag may follow an older
115
+ // tagFormat), makes the commit a release when it adds the package or leaves it at the `0.0.0` placeholder.
116
+ const listedInReleaseCommit = parseMonorepoReleaseSubject(subject)?.some((release) => release.name === unit.name && release.version === version) ?? false;
117
+ const releasedHere = taggedSha === sha || listedInReleaseCommit;
118
+ if (!releasedHere && version === UNRELEASED_PLACEHOLDER_VERSION) {
119
+ return null;
120
+ }
121
+ if (!releasedHere && (await reader.tryGit(["cat-file", "-e", `${sha}^:${unit.manifestPath}`])) === null) {
122
+ return null;
123
+ }
124
+ return { sha, version, subject, tag, tagged: taggedSha === sha, tagOnOrigin: tag !== null && remoteTags.has(tag) };
125
+ }
126
+
127
+ /**
128
+ * Gathers the monorepo release snapshot.
129
+ *
130
+ * @param {{
131
+ * repositoryRoot: string,
132
+ * units: readonly ReleaseUnit[],
133
+ * tagFormat: string,
134
+ * trackNpm?: boolean,
135
+ * checkMigrations?: (() => Promise<MigrationCheck> | MigrationCheck) | null,
136
+ * onProgress?: (label: string) => void,
137
+ * lookupNpm?: typeof lookupPublishedVersions,
138
+ * checkNpmAuthFor?: ((snapshot: MonorepoSnapshot) => string[]) | null,
139
+ * checkNpmAccess?: typeof checkNpmPublishAccess,
140
+ * lookupPullRequestFor?: typeof lookupPullRequest,
141
+ * registrySelection?: import("../create-version/registry.js").RegistrySelection,
142
+ * }} options - Repository, released packages, adapters and progress callback. `checkNpmAuthFor`
143
+ * receives the snapshot without credentials and names the packages whose credentials are checked.
144
+ * @returns {Promise<MonorepoSnapshot>} Snapshot accepted by `buildMonorepoPlan`.
145
+ */
146
+ export async function collectMonorepoState({
147
+ repositoryRoot,
148
+ units,
149
+ tagFormat,
150
+ trackNpm = false,
151
+ checkMigrations = null,
152
+ onProgress = () => {},
153
+ lookupNpm = lookupPublishedVersions,
154
+ checkNpmAuthFor = null,
155
+ checkNpmAccess = checkNpmPublishAccess,
156
+ lookupPullRequestFor = lookupPullRequest,
157
+ registrySelection,
158
+ }) {
159
+ const reader = createGitReader(repositoryRoot);
160
+ const repository = await readRepositorySnapshot({ repositoryRoot, reader, onProgress, lookupPullRequestFor });
161
+ const headSha = await reader.tryGit(["rev-parse", "HEAD"]);
162
+ const headSubject = await reader.tryGit(["log", "-1", "--format=%s", "HEAD"]);
163
+ const remoteTags = repository.remoteMainExists ? await readRemoteTags(reader) : new Set();
164
+
165
+ /** @type {PackageSnapshot[]} */
166
+ const packages = [];
167
+
168
+ for (const unit of units) {
169
+ onProgress(`Revisando ${unit.name}`);
170
+ const manifest = readJsonManifest(repositoryRoot, unit.manifestPath);
171
+ const releasedVersion = repository.remoteMainExists ? await readPackageVersionAt(reader, REMOTE_MAIN_REF, unit.manifestPath) : null;
172
+ const lastRelease = repository.remoteMainExists ? await findLastPackageRelease(reader, REMOTE_MAIN_REF, unit, tagFormat, remoteTags) : null;
173
+ const range = lastRelease ? `${lastRelease.sha}..${REMOTE_MAIN_REF}` : REMOTE_MAIN_REF;
174
+ const unreleasedCommits = repository.remoteMainExists ? await listCommits(reader, range, unit.changePaths) : [];
175
+ const packageRoot = path.join(repositoryRoot, unit.directory);
176
+ const npm = trackNpm ? await lookupPackageVersions(registrySelection, manifest, repositoryRoot, packageRoot, lookupNpm) : null;
177
+
178
+ packages.push({
179
+ unit,
180
+ manifest,
181
+ releasedVersion,
182
+ lastRelease,
183
+ unreleasedCommits,
184
+ changelog: await readPackageChangelogState(reader, repositoryRoot, packageRoot, unit.changelogPath, lastRelease?.sha ?? null),
185
+ npm,
186
+ npmAuth: null,
187
+ });
188
+ }
189
+
190
+ if (checkMigrations) {
191
+ onProgress("Consultando migraciones pendientes");
192
+ }
193
+
194
+ /** @type {MonorepoSnapshot} */
195
+ const snapshot = { ...repository, headSha, headSubject, packages, migrations: await readMigrations(checkMigrations) };
196
+ const packagesToCheck = new Set(checkNpmAuthFor?.(snapshot) ?? []);
197
+
198
+ for (const packageSnapshot of snapshot.packages) {
199
+ if (packagesToCheck.has(packageSnapshot.unit.name)) {
200
+ onProgress(`Verificando las credenciales de npm de ${packageSnapshot.unit.name}`);
201
+ packageSnapshot.npmAuth = await checkPackageAccess(registrySelection, packageSnapshot.manifest, repositoryRoot, path.join(repositoryRoot, packageSnapshot.unit.directory), checkNpmAccess, packageSnapshot.npm);
202
+ }
203
+ }
204
+
205
+ return snapshot;
206
+ }
@@ -0,0 +1,327 @@
1
+ /**
2
+ * Workspace discovery of the monorepo mode: which packages a repository
3
+ * holds, which of them are released (every non-`private` one) and which paths
4
+ * count as changes of each released package.
5
+ *
6
+ * A released package changes when its own directory changes, and also when a
7
+ * `private` workspace package it depends on changes (transitively): internal
8
+ * packages are usually bundled into their consumers (`noExternal`), so a fix
9
+ * there ships only through a new release of each consumer.
10
+ *
11
+ * @module monorepo/workspaces
12
+ */
13
+
14
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
15
+ import path from "node:path";
16
+
17
+ import { CHANGELOG_FILE } from "../constants/changelog.js";
18
+ import { PACKAGE_MANIFEST_FILE, PUBLISHED_DEPENDENCY_FIELDS } from "../constants/create-version.js";
19
+ import {
20
+ DEPENDENCY_FIELDS,
21
+ TAG_FORMAT_PLACEHOLDER,
22
+ WORKSPACE_EXCLUSION_PREFIX,
23
+ WORKSPACE_WILDCARD_SUFFIX,
24
+ WORKSPACES_PACKAGES,
25
+ } from "../constants/monorepo.js";
26
+ import { isPathInsideRoot } from "../create-version/config.js";
27
+ import { describeError } from "../create-version/errors.js";
28
+ import { isRecord } from "../create-version/json.js";
29
+ import { isStableReleaseVersion } from "../versions.js";
30
+
31
+ /**
32
+ * @typedef {{
33
+ * name: string,
34
+ * directory: string,
35
+ * private: boolean,
36
+ * version: string | null,
37
+ * manifest: Record<string, unknown>,
38
+ * }} WorkspacePackage
39
+ * `directory` is relative to the repository root, with `/` separators.
40
+ * @typedef {{
41
+ * name: string,
42
+ * directory: string,
43
+ * component: string,
44
+ * manifestPath: string,
45
+ * changelogPath: string,
46
+ * version: string | null,
47
+ * changePaths: string[],
48
+ * publishedDependencies: string[],
49
+ * }} ReleaseUnit
50
+ * A released package: `component` names its tag, `changePaths` are the Git pathspecs whose commits
51
+ * count as its changes (its own directory plus its private workspace dependencies, excluding the
52
+ * released packages nested inside it), `publishedDependencies`
53
+ * the released packages it installs (they are published first).
54
+ */
55
+
56
+ /**
57
+ * @param {string} relativePath - Path relative to the root.
58
+ * @returns {string} Same path with `/` separators and no trailing slash.
59
+ */
60
+ function toPosixPath(relativePath) {
61
+ return relativePath.split(path.sep).join("/").replace(/\/+$/u, "");
62
+ }
63
+
64
+ /**
65
+ * Reads a JSON manifest without throwing.
66
+ *
67
+ * @param {string} manifestPath - Absolute path.
68
+ * @returns {Record<string, unknown> | null} Manifest, or `null` when missing or invalid.
69
+ */
70
+ function readManifest(manifestPath) {
71
+ try {
72
+ const parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
73
+ return isRecord(parsed) ? parsed : null;
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Reads the manifest of a matched workspace directory.
81
+ *
82
+ * @param {string} repositoryRoot - Repository root.
83
+ * @param {string} directory - Workspace directory relative to the root.
84
+ * @returns {Record<string, unknown>} Manifest.
85
+ * @throws {Error} When the manifest is not valid JSON or not an object.
86
+ */
87
+ function readWorkspaceManifest(repositoryRoot, directory) {
88
+ const manifestPath = `${directory}/${PACKAGE_MANIFEST_FILE}`;
89
+ let parsed;
90
+ try {
91
+ parsed = JSON.parse(readFileSync(path.join(repositoryRoot, manifestPath), "utf8"));
92
+ } catch (error) {
93
+ throw new Error(`astra-rp create-version: no se pudo leer ${manifestPath}: ${describeError(error)}; corregilo o excluí ese workspace`, { cause: error });
94
+ }
95
+ if (!isRecord(parsed)) {
96
+ throw new Error(`astra-rp create-version: ${manifestPath} no es un objeto JSON; corregilo o excluí ese workspace`);
97
+ }
98
+ return parsed;
99
+ }
100
+
101
+ /**
102
+ * Reads the workspace patterns the root `package.json` declares (`workspaces` as a list, or as an
103
+ * object with `packages`, as Yarn and Bun accept).
104
+ *
105
+ * @param {Record<string, unknown> | null} rootManifest - Root `package.json`.
106
+ * @returns {string[]} Patterns.
107
+ */
108
+ export function readDeclaredWorkspaces(rootManifest) {
109
+ const workspaces = rootManifest?.workspaces;
110
+ const patterns = Array.isArray(workspaces) ? workspaces : /** @type {{ packages?: unknown }} */ (workspaces ?? {}).packages;
111
+ return Array.isArray(patterns) ? patterns.filter((pattern) => typeof pattern === "string") : [];
112
+ }
113
+
114
+ /**
115
+ * Expands workspace patterns into package directories. Supported: exact directories
116
+ * (`packages/cli`), direct children (`packages/*`) and exclusions (`!packages/internal`).
117
+ *
118
+ * @param {string} repositoryRoot - Repository root.
119
+ * @param {readonly string[]} patterns - Workspace patterns.
120
+ * @returns {string[]} Directories relative to the root that hold a `package.json`, sorted.
121
+ * @throws {Error} When a pattern escapes the repository or uses a glob beyond a trailing `/*`.
122
+ */
123
+ export function expandWorkspacePatterns(repositoryRoot, patterns) {
124
+ const included = new Set();
125
+ const excluded = new Set();
126
+
127
+ for (const rawPattern of patterns) {
128
+ const isExclusion = rawPattern.startsWith(WORKSPACE_EXCLUSION_PREFIX);
129
+ if (!isPathInsideRoot(isExclusion ? rawPattern.slice(WORKSPACE_EXCLUSION_PREFIX.length) : rawPattern)) {
130
+ throw new Error(`astra-rp create-version: el patrón de workspace "${rawPattern}" sale del repositorio; usá rutas relativas a la raíz, sin ".." ni rutas absolutas`);
131
+ }
132
+ const pattern = toPosixPath(isExclusion ? rawPattern.slice(WORKSPACE_EXCLUSION_PREFIX.length) : rawPattern).replace(/^\.\//u, "");
133
+ const base = pattern.endsWith(WORKSPACE_WILDCARD_SUFFIX) ? pattern.slice(0, -WORKSPACE_WILDCARD_SUFFIX.length) : pattern;
134
+
135
+ if (/[*?{}[\]]/u.test(base)) {
136
+ throw new Error(`astra-rp create-version: workspace pattern "${rawPattern}" is not supported; use exact directories or a trailing /*`);
137
+ }
138
+
139
+ const directories = pattern.endsWith(WORKSPACE_WILDCARD_SUFFIX)
140
+ ? existsSync(path.join(repositoryRoot, base))
141
+ ? readdirSync(path.join(repositoryRoot, base), { withFileTypes: true })
142
+ .filter((entry) => entry.isDirectory())
143
+ .map((entry) => `${base}/${entry.name}`)
144
+ : []
145
+ : [base];
146
+
147
+ for (const directory of directories) {
148
+ (isExclusion ? excluded : included).add(directory);
149
+ }
150
+ }
151
+
152
+ return [...included]
153
+ .filter((directory) => !excluded.has(directory) && existsSync(path.join(repositoryRoot, directory, PACKAGE_MANIFEST_FILE)))
154
+ .toSorted();
155
+ }
156
+
157
+ /**
158
+ * Lists the workspace packages of a repository.
159
+ *
160
+ * @param {string} repositoryRoot - Repository root.
161
+ * @param {"workspaces" | readonly string[]} packages - `packages` of the configuration: the root workspaces, or explicit patterns.
162
+ * @returns {WorkspacePackage[]} Packages with a `name` (private ones without it are skipped), sorted by directory.
163
+ * @throws {Error} When a matched manifest is malformed, non-private without a `name` or without a stable `X.Y.Z` `version`, no package is found or two packages share a name.
164
+ */
165
+ export function discoverWorkspacePackages(repositoryRoot, packages) {
166
+ const patterns = packages === WORKSPACES_PACKAGES ? readDeclaredWorkspaces(readManifest(path.join(repositoryRoot, PACKAGE_MANIFEST_FILE))) : packages;
167
+ const found = expandWorkspacePatterns(repositoryRoot, patterns).flatMap((directory) => {
168
+ const manifest = readWorkspaceManifest(repositoryRoot, directory);
169
+ if (typeof manifest.name !== "string" || manifest.name === "") {
170
+ if (manifest.private === true) {
171
+ return [];
172
+ }
173
+ throw new Error(`astra-rp create-version: ${directory}/${PACKAGE_MANIFEST_FILE} no tiene "name"; agregale un "name" para publicarlo, o "private": true (o excluí ese workspace) para omitirlo`);
174
+ }
175
+ if (manifest.private !== true && !isStableReleaseVersion(manifest.version)) {
176
+ const currentVersion = manifest.version === undefined ? "no tiene \"version\"" : `tiene "version": ${JSON.stringify(manifest.version)}`;
177
+ throw new Error(`astra-rp create-version: ${directory}/${PACKAGE_MANIFEST_FILE} ${currentVersion}; un paquete publicado necesita una versión estable X.Y.Z (por ejemplo "0.1.0"), o "private": true (o excluí ese workspace) para omitirlo`);
178
+ }
179
+ return [
180
+ {
181
+ name: manifest.name,
182
+ directory,
183
+ private: manifest.private === true,
184
+ version: typeof manifest.version === "string" ? manifest.version : null,
185
+ manifest,
186
+ },
187
+ ];
188
+ });
189
+
190
+ if (found.length === 0) {
191
+ throw new Error(`astra-rp create-version: no workspace package found for packages ${JSON.stringify(packages)}`);
192
+ }
193
+
194
+ const seen = new Map();
195
+ for (const workspacePackage of found) {
196
+ if (seen.has(workspacePackage.name)) {
197
+ throw new Error(`astra-rp create-version: ${workspacePackage.name} is declared in both ${seen.get(workspacePackage.name)} and ${workspacePackage.directory}`);
198
+ }
199
+ seen.set(workspacePackage.name, workspacePackage.directory);
200
+ }
201
+
202
+ return found;
203
+ }
204
+
205
+ /**
206
+ * Names of the workspace packages a manifest depends on through the given fields.
207
+ *
208
+ * @param {Record<string, unknown>} manifest - Package manifest.
209
+ * @param {readonly string[]} fields - Dependency fields to read.
210
+ * @param {ReadonlySet<string>} workspaceNames - Names of every workspace package.
211
+ * @returns {string[]} Workspace dependency names.
212
+ */
213
+ function listWorkspaceDependencies(manifest, fields, workspaceNames) {
214
+ return fields
215
+ .flatMap((field) => Object.keys(/** @type {Record<string, unknown>} */ (manifest[field] ?? {})))
216
+ .filter((dependencyName, index, all) => workspaceNames.has(dependencyName) && all.indexOf(dependencyName) === index);
217
+ }
218
+
219
+ /**
220
+ * Turns the workspace packages into release units: one per non-`private` package.
221
+ *
222
+ * @param {readonly WorkspacePackage[]} workspacePackages - Result of {@link discoverWorkspacePackages}.
223
+ * @returns {ReleaseUnit[]} Released packages, sorted by directory.
224
+ */
225
+ export function resolveReleaseUnits(workspacePackages) {
226
+ const byName = new Map(workspacePackages.map((workspacePackage) => [workspacePackage.name, workspacePackage]));
227
+ const workspaceNames = new Set(byName.keys());
228
+
229
+ /**
230
+ * Directories of the private packages `packageName` depends on, transitively.
231
+ *
232
+ * @param {string} packageName - Package whose private dependencies are collected.
233
+ * @param {Set<string>} visited - Packages already walked (cycles).
234
+ * @returns {string[]} Directories.
235
+ */
236
+ const privateDependencyDirectories = (packageName, visited) => {
237
+ const workspacePackage = byName.get(packageName);
238
+ if (!workspacePackage || visited.has(packageName)) {
239
+ return [];
240
+ }
241
+ visited.add(packageName);
242
+ return listWorkspaceDependencies(workspacePackage.manifest, DEPENDENCY_FIELDS, workspaceNames).flatMap((dependencyName) => {
243
+ const dependency = /** @type {WorkspacePackage} */ (byName.get(dependencyName));
244
+ return dependency.private ? [dependency.directory, ...privateDependencyDirectories(dependencyName, visited)] : [];
245
+ });
246
+ };
247
+
248
+ /**
249
+ * Git pathspecs whose commits count as changes of a package: its directory and its private
250
+ * dependencies, minus the released packages nested inside its directory (they release their own commits).
251
+ *
252
+ * @param {WorkspacePackage} workspacePackage - Released package.
253
+ * @param {string[]} dependencyDirectories - Directories of its private dependencies.
254
+ * @returns {string[]} Git pathspecs.
255
+ */
256
+ const listChangePaths = (workspacePackage, dependencyDirectories) => {
257
+ const nestedExclusions = workspacePackages
258
+ .filter((nestedPackage) => !nestedPackage.private && nestedPackage.directory.startsWith(`${workspacePackage.directory}/`))
259
+ .map((nestedPackage) => `:(exclude)${nestedPackage.directory}`);
260
+ return [...new Set([workspacePackage.directory, ...dependencyDirectories]), ...nestedExclusions];
261
+ };
262
+
263
+ return workspacePackages
264
+ .filter((workspacePackage) => !workspacePackage.private)
265
+ .map((workspacePackage) => ({
266
+ name: workspacePackage.name,
267
+ directory: workspacePackage.directory,
268
+ component: workspacePackage.directory.split("/").at(-1) ?? workspacePackage.directory,
269
+ manifestPath: `${workspacePackage.directory}/${PACKAGE_MANIFEST_FILE}`,
270
+ changelogPath: `${workspacePackage.directory}/${CHANGELOG_FILE}`,
271
+ version: workspacePackage.version,
272
+ changePaths: listChangePaths(workspacePackage, privateDependencyDirectories(workspacePackage.name, new Set())),
273
+ publishedDependencies: listWorkspaceDependencies(workspacePackage.manifest, PUBLISHED_DEPENDENCY_FIELDS, workspaceNames).filter(
274
+ (dependencyName) => byName.get(dependencyName)?.private === false
275
+ ),
276
+ }));
277
+ }
278
+
279
+ /**
280
+ * Orders packages so every one comes after the released packages it installs.
281
+ *
282
+ * @template {{ name: string, publishedDependencies: string[] }} T
283
+ * @param {readonly T[]} units - Packages to publish.
284
+ * @returns {T[]} Same packages, dependencies first (stable for independent packages).
285
+ */
286
+ export function sortByPublicationOrder(units) {
287
+ const byName = new Map(units.map((unit) => [unit.name, unit]));
288
+ /** @type {T[]} */
289
+ const ordered = [];
290
+ const placed = new Set();
291
+
292
+ /** @param {T} unit - Package to place after its dependencies. @param {Set<string>} walk - Packages on the current walk (cycles). */
293
+ const place = (unit, walk) => {
294
+ if (placed.has(unit.name) || walk.has(unit.name)) {
295
+ return;
296
+ }
297
+ walk.add(unit.name);
298
+ for (const dependencyName of unit.publishedDependencies) {
299
+ const dependency = byName.get(dependencyName);
300
+ if (dependency) {
301
+ place(dependency, walk);
302
+ }
303
+ }
304
+ placed.add(unit.name);
305
+ ordered.push(unit);
306
+ };
307
+
308
+ for (const unit of units) {
309
+ place(unit, new Set());
310
+ }
311
+ return ordered;
312
+ }
313
+
314
+ /**
315
+ * Builds the Git tag of a package release.
316
+ *
317
+ * @param {string} tagFormat - Format with `{version}` and optionally `{component}` / `{name}`.
318
+ * @param {{ name: string, component: string }} unit - Released package.
319
+ * @param {string} version - Released version.
320
+ * @returns {string} Tag such as `widget-v1.2.0`.
321
+ */
322
+ export function formatPackageTag(tagFormat, unit, version) {
323
+ return tagFormat
324
+ .replaceAll(TAG_FORMAT_PLACEHOLDER.version, version)
325
+ .replaceAll(TAG_FORMAT_PLACEHOLDER.component, unit.component)
326
+ .replaceAll(TAG_FORMAT_PLACEHOLDER.name, unit.name);
327
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Detects the package manager of a project and builds the commands astra-rp
3
+ * prints or runs for it, so a bun, npm or yarn project is told to run its own
4
+ * commands instead of pnpm ones.
5
+ *
6
+ * @module package-manager
7
+ */
8
+
9
+ import { existsSync, readFileSync } from "node:fs";
10
+ import path from "node:path";
11
+
12
+ import {
13
+ CREATE_VERSION_SCRIPT,
14
+ DEFAULT_PACKAGE_MANAGER,
15
+ LOCKFILE_PACKAGE_MANAGERS,
16
+ PACKAGE_MANAGER,
17
+ PACKAGE_MANAGER_FIELD,
18
+ PACKAGE_MANAGER_FIELD_PATTERN,
19
+ SCRIPT_RUNNER,
20
+ } from "./constants/package-manager.js";
21
+ import { CREATE_VERSION_FLAG, PACKAGE_MANIFEST_FILE } from "./constants/create-version.js";
22
+
23
+ /**
24
+ * @typedef {"pnpm" | "npm" | "yarn" | "bun"} PackageManagerName
25
+ * @typedef {{
26
+ * packageManager: PackageManagerName,
27
+ * runScript: (script: string) => string,
28
+ * createVersion: string,
29
+ * }} ProjectCommands
30
+ * `runScript("ci")` is the command line that runs a `package.json` script; `createVersion` is how
31
+ * the project runs `astra-rp create-version` through its script.
32
+ */
33
+
34
+ /**
35
+ * Reads the package manager a `packageManager` value declares.
36
+ *
37
+ * @param {unknown} value - `packageManager` field of `package.json`.
38
+ * @returns {PackageManagerName | null} Package manager, or `null` when the value is missing or unknown.
39
+ */
40
+ export function parsePackageManagerField(value) {
41
+ const match = typeof value === "string" ? PACKAGE_MANAGER_FIELD_PATTERN.exec(value) : null;
42
+ return match ? /** @type {PackageManagerName} */ (match[1]) : null;
43
+ }
44
+
45
+ /**
46
+ * Detects the package manager of a project: the `packageManager` field of its `package.json`,
47
+ * else its lockfile, else pnpm (the historical default).
48
+ *
49
+ * @param {string} repositoryRoot - Project root.
50
+ * @param {Record<string, unknown>} [manifest] - Its `package.json`, when already read.
51
+ * @returns {PackageManagerName} Package manager.
52
+ */
53
+ export function detectPackageManager(repositoryRoot, manifest) {
54
+ const declared = parsePackageManagerField((manifest ?? readManifest(repositoryRoot))?.[PACKAGE_MANAGER_FIELD]);
55
+
56
+ if (declared) {
57
+ return declared;
58
+ }
59
+
60
+ const lockfile = LOCKFILE_PACKAGE_MANAGERS.find(([fileName]) => existsSync(path.join(repositoryRoot, fileName)));
61
+ return /** @type {PackageManagerName} */ (lockfile?.[1] ?? DEFAULT_PACKAGE_MANAGER);
62
+ }
63
+
64
+ /**
65
+ * Builds the commands printed or run for a package manager.
66
+ *
67
+ * @param {PackageManagerName} packageManager - Package manager.
68
+ * @returns {ProjectCommands} Commands.
69
+ */
70
+ export function describeProjectCommands(packageManager) {
71
+ const scriptRunner = SCRIPT_RUNNER[packageManager];
72
+ return {
73
+ packageManager,
74
+ runScript: (script) => `${packageManager} run ${script}`,
75
+ createVersion: `${scriptRunner} ${CREATE_VERSION_SCRIPT}`,
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Builds the command that runs `create-version` with options. npm reads every option before its
81
+ * `--` separator as its own configuration (`npm run create-version --retry-ci v1.2.3` forwards only
82
+ * `v1.2.3`), so npm projects need `npm run create-version -- <opciones>`; pnpm, bun and yarn forward
83
+ * them as is. Every printed or dispatched command that carries options must start with it.
84
+ *
85
+ * @param {ProjectCommands} commands - Project commands.
86
+ * @returns {string} Command to which the options are appended.
87
+ */
88
+ export function describeCreateVersionWithOptions(commands) {
89
+ return commands.packageManager === PACKAGE_MANAGER.npm ? `${commands.createVersion} ${CREATE_VERSION_FLAG.endOfOptions}` : commands.createVersion;
90
+ }
91
+
92
+ /** Commands of a pnpm project: the default when a project cannot be inspected. */
93
+ export const DEFAULT_PROJECT_COMMANDS = Object.freeze(describeProjectCommands(DEFAULT_PACKAGE_MANAGER));
94
+
95
+ /**
96
+ * Reads `package.json` without throwing.
97
+ *
98
+ * @param {string} repositoryRoot - Project root.
99
+ * @returns {Record<string, unknown> | null} Manifest, or `null` when it is missing or invalid.
100
+ */
101
+ function readManifest(repositoryRoot) {
102
+ try {
103
+ return JSON.parse(readFileSync(path.join(repositoryRoot, PACKAGE_MANIFEST_FILE), "utf8"));
104
+ } catch {
105
+ return null;
106
+ }
107
+ }