@williamthorsen/release-kit 12.0.0 → 13.1.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/.meta/changelog.json +229 -0
- package/.readyup/kits/default.js +18 -17
- package/.readyup/kits/npm-auto-publish.js +24 -23
- package/.readyup/manifest.json +16 -11
- package/CHANGELOG.md +64 -0
- package/README.md +27 -51
- package/bin/release-kit.js +1 -1
- package/dist/esm/assertCleanWorkingTree.d.ts +1 -5
- package/dist/esm/assertTaggedBaseline.d.ts +29 -0
- package/dist/esm/assertTaggedBaseline.js +41 -0
- package/dist/esm/attachChangelogDiagnostics.d.ts +1 -1
- package/dist/esm/attachChangelogDiagnostics.js +3 -0
- package/dist/esm/bin/release-kit.js +85 -22
- package/dist/esm/buildChangelogEntries.d.ts +23 -13
- package/dist/esm/buildChangelogEntries.js +36 -17
- package/dist/esm/buildDependencyGraph.d.ts +6 -6
- package/dist/esm/buildEmptyReleaseEntry.d.ts +1 -8
- package/dist/esm/buildReleaseSummary.d.ts +2 -8
- package/dist/esm/buildSyntheticChangelogEntry.d.ts +3 -5
- package/dist/esm/buildSyntheticChangelogEntry.js +2 -2
- package/dist/esm/bumpVersion.d.ts +1 -8
- package/dist/esm/changelogJsonFile.d.ts +5 -7
- package/dist/esm/changelogJsonFile.js +2 -17
- package/dist/esm/changelogJsonUtils.d.ts +7 -10
- package/dist/esm/changelogOverrides.d.ts +59 -98
- package/dist/esm/classifyChangelogCommit.d.ts +7 -9
- package/dist/esm/classifyChangelogCommit.js +3 -2
- package/dist/esm/commitCommand.d.ts +2 -4
- package/dist/esm/compareVersions.d.ts +10 -4
- package/dist/esm/compareVersions.js +16 -0
- package/dist/esm/configFlagSchema.d.ts +1 -1
- package/dist/esm/createGithubRelease.d.ts +7 -19
- package/dist/esm/createGithubReleaseCommand.d.ts +5 -5
- package/dist/esm/createGithubReleaseCommand.js +9 -9
- package/dist/esm/createTags.d.ts +2 -3
- package/dist/esm/decideRelease.d.ts +2 -6
- package/dist/esm/defaults.d.ts +5 -24
- package/dist/esm/defaults.js +4 -5
- package/dist/esm/defineConfig.d.ts +1 -1
- package/dist/esm/deleteFileIfExists.d.ts +1 -1
- package/dist/esm/deriveReleaseNotesConfig.d.ts +20 -0
- package/dist/esm/deriveReleaseNotesConfig.js +21 -0
- package/dist/esm/deriveWorkspaceConfig.d.ts +3 -5
- package/dist/esm/detectPackageManager.d.ts +2 -4
- package/dist/esm/detectUndeclaredTagPrefixes.d.ts +4 -6
- package/dist/esm/detectUndeclaredTagPrefixes.js +1 -1
- package/dist/esm/determineBumpType.d.ts +1 -1
- package/dist/esm/discoverWorkspaces.d.ts +14 -15
- package/dist/esm/discoverWorkspaces.js +12 -12
- package/dist/esm/enterRepoRoot.d.ts +12 -0
- package/dist/esm/enterRepoRoot.js +16 -0
- package/dist/esm/enumerateReleaseWindows.d.ts +8 -8
- package/dist/esm/extractMigration.d.ts +6 -8
- package/dist/esm/findUnroutedEntryScopes.d.ts +16 -0
- package/dist/esm/findUnroutedEntryScopes.js +57 -0
- package/dist/esm/formatExcludedSkip.d.ts +3 -0
- package/dist/esm/formatExcludedSkip.js +3 -0
- package/dist/esm/formatPrivateSkip.d.ts +1 -6
- package/dist/esm/generateChangelogs.d.ts +10 -5
- package/dist/esm/glyphs.d.ts +1 -1
- package/dist/esm/help/prepareHelp.d.ts +3 -4
- package/dist/esm/help/prepareHelp.js +4 -3
- package/dist/esm/init/checks.d.ts +3 -3
- package/dist/esm/init/checks.js +4 -1
- package/dist/esm/init/detectRepoType.d.ts +2 -3
- package/dist/esm/init/hasProvenance.d.ts +2 -0
- package/dist/esm/init/hasProvenance.js +3 -0
- package/dist/esm/init/initCommand.d.ts +3 -6
- package/dist/esm/init/initCommand.js +10 -10
- package/dist/esm/init/parseJsonRecord.d.ts +1 -4
- package/dist/esm/init/scaffold.d.ts +1 -1
- package/dist/esm/init/templates.d.ts +9 -11
- package/dist/esm/init/templates.js +12 -11
- package/dist/esm/init/updateTemplatesCommand.d.ts +13 -0
- package/dist/esm/init/updateTemplatesCommand.js +64 -0
- package/dist/esm/injectReleaseNotesIntoReadme.d.ts +12 -17
- package/dist/esm/injectSection.d.ts +3 -3
- package/dist/esm/loadConfig.d.ts +16 -14
- package/dist/esm/loadConfig.js +2 -2
- package/dist/esm/loadValidatedConfig.d.ts +16 -19
- package/dist/esm/loadValidatedConfig.js +6 -1
- package/dist/esm/packsPath.d.ts +9 -9
- package/dist/esm/parseChangeRecordBlock.d.ts +2 -2
- package/dist/esm/parseCommitMessage.d.ts +17 -32
- package/dist/esm/parseCommitMessage.js +28 -37
- package/dist/esm/parseRequestedTags.d.ts +3 -3
- package/dist/esm/planReleaseNotesPreviews.d.ts +6 -6
- package/dist/esm/planVersionBump.d.ts +5 -3
- package/dist/esm/planVersionBump.js +3 -0
- package/dist/esm/prepareCommand.d.ts +4 -7
- package/dist/esm/prepareCommand.js +13 -6
- package/dist/esm/previewTagPrefixes.d.ts +8 -10
- package/dist/esm/propagateBumps.d.ts +6 -7
- package/dist/esm/publish.d.ts +1 -1
- package/dist/esm/publishCommand.d.ts +4 -3
- package/dist/esm/publishCommand.js +8 -8
- package/dist/esm/pushCommand.d.ts +4 -3
- package/dist/esm/pushCommand.js +8 -2
- package/dist/esm/pushRelease.d.ts +2 -2
- package/dist/esm/readChangelogSections.d.ts +33 -0
- package/dist/esm/readChangelogSections.js +80 -0
- package/dist/esm/releaseFiles.d.ts +5 -7
- package/dist/esm/releasePlan.d.ts +11 -13
- package/dist/esm/releasePrepare.d.ts +23 -22
- package/dist/esm/releasePrepare.js +31 -11
- package/dist/esm/releasePrepareMono.d.ts +9 -7
- package/dist/esm/releasePrepareMono.js +85 -33
- package/dist/esm/releasePrepareProject.d.ts +19 -29
- package/dist/esm/releasePrepareProject.js +31 -12
- package/dist/esm/renderChangelogMarkdown.d.ts +10 -4
- package/dist/esm/renderChangelogMarkdown.js +27 -2
- package/dist/esm/renderReleaseNotes.d.ts +3 -11
- package/dist/esm/renderReleaseNotes.js +2 -6
- package/dist/esm/reportPrepare.d.ts +4 -5
- package/dist/esm/reportPrepare.js +33 -19
- package/dist/esm/resolveCommandTags.d.ts +15 -11
- package/dist/esm/resolveCommandTags.js +35 -5
- package/dist/esm/resolveConfigFlag.d.ts +7 -0
- package/dist/esm/resolveConfigFlag.js +4 -0
- package/dist/esm/resolveReleaseTags.d.ts +6 -8
- package/dist/esm/resolveReleaseTags.js +1 -1
- package/dist/esm/resolveStylesOrExit.d.ts +2 -2
- package/dist/esm/showTagPrefixesCommand.d.ts +7 -10
- package/dist/esm/showTagPrefixesCommand.js +4 -4
- package/dist/esm/stripEmojiPrefix.d.ts +1 -0
- package/dist/esm/stripScope.d.ts +2 -2
- package/dist/esm/stripScope.js +3 -3
- package/dist/esm/sync-labels/generateCommand.d.ts +11 -10
- package/dist/esm/sync-labels/generateCommand.js +17 -9
- package/dist/esm/sync-labels/initCommand.d.ts +6 -7
- package/dist/esm/sync-labels/initCommand.js +2 -2
- package/dist/esm/sync-labels/presets.d.ts +5 -4
- package/dist/esm/sync-labels/presets.js +9 -9
- package/dist/esm/sync-labels/resolveLabels.d.ts +7 -7
- package/dist/esm/sync-labels/resolveLabels.js +2 -1
- package/dist/esm/sync-labels/retiredConfig.d.ts +4 -5
- package/dist/esm/sync-labels/syncCommand.d.ts +1 -4
- package/dist/esm/sync-labels/templates.d.ts +3 -3
- package/dist/esm/sync-labels/templates.js +3 -3
- package/dist/esm/sync-labels/typeLabels.d.ts +14 -0
- package/dist/esm/sync-labels/typeLabels.js +38 -0
- package/dist/esm/sync-labels/types.d.ts +4 -2
- package/dist/esm/tagCommand.d.ts +1 -1
- package/dist/esm/typeGuards.d.ts +2 -2
- package/dist/esm/types.d.ts +122 -93
- package/dist/esm/types.js +2 -1
- package/dist/esm/validateConfig.d.ts +2 -11
- package/dist/esm/validateConfig.js +2 -2
- package/dist/esm/validateOnlyExcludesStrandedDependents.d.ts +14 -12
- package/dist/esm/validateOverridesCommand.d.ts +17 -20
- package/dist/esm/validateOverridesCommand.js +1 -1
- package/docs/api.md +6 -6
- package/docs/changelogs.md +63 -40
- package/docs/configuration.md +29 -27
- package/docs/editorial-overrides.md +23 -23
- package/docs/labels.md +37 -19
- package/docs/project-releases.md +13 -13
- package/docs/publishing.md +5 -5
- package/docs/readiness-checks.md +3 -3
- package/docs/releasing.md +50 -25
- package/docs/work-types.md +41 -57
- package/package.json +7 -6
- package/presets/labels/common.yaml +1 -41
- package/dist/esm/resolveReleaseNotesConfig.d.ts +0 -26
- package/dist/esm/resolveReleaseNotesConfig.js +0 -29
- package/dist/esm/workTypesData.d.ts +0 -50
- package/dist/esm/workTypesData.js +0 -145
- package/schemas/label-map.json +0 -24
|
@@ -1,45 +1,44 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { compileTemplate, parse, TEMPLATE_CATALOGUE, } from '@williamthorsen/change-grammar';
|
|
2
|
+
const SUBJECT_TEMPLATES = [
|
|
3
|
+
compileTemplate(TEMPLATE_CATALOGUE.pipedScope),
|
|
4
|
+
compileTemplate(TEMPLATE_CATALOGUE.conventionalCommits),
|
|
5
|
+
];
|
|
4
6
|
export function parseCommitMessage(message, hash, workTypes, scopeAliases, options) {
|
|
5
7
|
const firstLine = message.split('\n', 1)[0] ?? '';
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
if (!match) {
|
|
9
|
-
return undefined;
|
|
10
|
-
}
|
|
11
|
-
const pipeScope = match[1];
|
|
12
|
-
const rawType = match[2];
|
|
13
|
-
const parenthesizedScope = match[3];
|
|
14
|
-
const breakingMarker = match[4];
|
|
15
|
-
const description = match[5];
|
|
16
|
-
if (rawType === undefined || description === undefined) {
|
|
17
|
-
return undefined;
|
|
18
|
-
}
|
|
19
|
-
const resolvedType = resolveType(rawType, workTypes);
|
|
20
|
-
if (resolvedType === undefined) {
|
|
8
|
+
const record = parseSubject(firstLine, workTypes);
|
|
9
|
+
if (record?.type === undefined || record.title === undefined) {
|
|
21
10
|
return undefined;
|
|
22
11
|
}
|
|
23
12
|
const commit = { message, subject: firstLine, hash };
|
|
24
13
|
const breaking = evaluateBreakingPolicy({
|
|
25
14
|
commit,
|
|
26
|
-
resolvedType,
|
|
27
|
-
hasPrefixBreaking:
|
|
15
|
+
resolvedType: record.type,
|
|
16
|
+
hasPrefixBreaking: record.breaking === true,
|
|
28
17
|
hasFooterBreaking: message.includes('BREAKING CHANGE:'),
|
|
29
|
-
policy: options?.breakingPolicies?.[
|
|
18
|
+
policy: options?.breakingPolicies?.[record.type] ?? 'optional',
|
|
30
19
|
onPolicyViolation: options?.onPolicyViolation,
|
|
31
20
|
});
|
|
32
|
-
const rawScope =
|
|
21
|
+
const rawScope = record.scope;
|
|
33
22
|
const resolvedScope = rawScope !== undefined && scopeAliases !== undefined ? (scopeAliases[rawScope] ?? rawScope) : rawScope;
|
|
34
23
|
return {
|
|
35
24
|
message,
|
|
36
25
|
hash,
|
|
37
|
-
type:
|
|
38
|
-
description,
|
|
26
|
+
type: record.type,
|
|
27
|
+
description: record.title,
|
|
39
28
|
breaking,
|
|
40
29
|
...(resolvedScope !== undefined && { scope: resolvedScope }),
|
|
41
30
|
};
|
|
42
31
|
}
|
|
32
|
+
export function parseSubject(subject, workTypes) {
|
|
33
|
+
const taxonomy = toTaxonomy(workTypes);
|
|
34
|
+
for (const nodes of SUBJECT_TEMPLATES) {
|
|
35
|
+
const record = parse(nodes, subject, taxonomy);
|
|
36
|
+
if (record !== undefined) {
|
|
37
|
+
return record;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
43
42
|
export function evaluateBreakingPolicy(inputs) {
|
|
44
43
|
const { commit, resolvedType, hasPrefixBreaking, hasFooterBreaking, policy, onPolicyViolation } = inputs;
|
|
45
44
|
const prefixSurface = inputs.prefixSurface ?? 'prefix';
|
|
@@ -52,13 +51,6 @@ export function evaluateBreakingPolicy(inputs) {
|
|
|
52
51
|
}
|
|
53
52
|
return false;
|
|
54
53
|
}
|
|
55
|
-
if (policy === 'required') {
|
|
56
|
-
if (!hasPrefixBreaking) {
|
|
57
|
-
onPolicyViolation?.(commit, resolvedType, prefixSurface);
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
54
|
return hasPrefixBreaking || hasFooterBreaking;
|
|
63
55
|
}
|
|
64
56
|
export function resolveType(rawType, workTypes) {
|
|
@@ -77,10 +69,9 @@ export function resolveType(rawType, workTypes) {
|
|
|
77
69
|
}
|
|
78
70
|
return undefined;
|
|
79
71
|
}
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
return result;
|
|
72
|
+
function toTaxonomy(workTypes) {
|
|
73
|
+
return {
|
|
74
|
+
tiers: [],
|
|
75
|
+
types: Object.entries(workTypes).map(([key, config]) => ({ aliases: config.aliases ?? [], key, tier: '' })),
|
|
76
|
+
};
|
|
86
77
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Parses the comma-separated `--tags` flag value into a list of requested tag names.
|
|
3
3
|
*
|
|
4
4
|
* Empty segments (from `--tags=`, leading/trailing commas, or `--tags=,,`) are dropped. When the
|
|
5
|
-
* resulting list is empty, returns `undefined
|
|
6
|
-
*
|
|
5
|
+
* resulting list is empty, returns `undefined`, and the caller does not filter the tags, as when
|
|
6
|
+
* `--tags` is omitted.
|
|
7
7
|
*/
|
|
8
8
|
export declare function parseRequestedTags(flagValue: string | undefined): string[] | undefined;
|
|
@@ -11,7 +11,7 @@ export interface PlanReleaseNotesPreviewsOptions {
|
|
|
11
11
|
/** Section titles in priority order, typically derived from `resolveWorkTypes(config.workTypes)`. */
|
|
12
12
|
sectionOrder: string[];
|
|
13
13
|
}
|
|
14
|
-
/** Preview files a release will write,
|
|
14
|
+
/** Preview files that a release will write, with the reasons for any that were left out. */
|
|
15
15
|
export interface ReleaseNotesPreviewsPlan {
|
|
16
16
|
writes: PlannedWrite[];
|
|
17
17
|
warnings: string[];
|
|
@@ -20,14 +20,14 @@ export interface ReleaseNotesPreviewsPlan {
|
|
|
20
20
|
* Computes the per-workspace release-notes previews under `{workspacePath}/docs/` without
|
|
21
21
|
* writing them:
|
|
22
22
|
*
|
|
23
|
-
* - `docs/README.v{version}.md
|
|
24
|
-
* - `docs/RELEASE_NOTES.v{version}.md
|
|
23
|
+
* - `docs/README.v{version}.md`: the workspace README with release notes injected at the marker.
|
|
24
|
+
* - `docs/RELEASE_NOTES.v{version}.md`: the standalone release notes for this version.
|
|
25
25
|
*
|
|
26
|
-
* Renders from the entries the release will write rather than from a saved `changelog.json`, so
|
|
26
|
+
* Renders from the entries that the release will write rather than from a saved `changelog.json`, so
|
|
27
27
|
* a preview describes the release being prepared rather than the one already on disk.
|
|
28
28
|
*
|
|
29
29
|
* The injected-README preview is omitted (the standalone file is still planned) when the
|
|
30
|
-
* workspace
|
|
31
|
-
* for this version, with the reason recorded in `warnings`.
|
|
30
|
+
* workspace does not have a readable `README.md`. Both are omitted when the renderer does not
|
|
31
|
+
* report any content for this version, with the reason recorded in `warnings`.
|
|
32
32
|
*/
|
|
33
33
|
export declare function planReleaseNotesPreviews(options: PlanReleaseNotesPreviewsOptions): ReleaseNotesPreviewsPlan;
|
|
@@ -10,13 +10,15 @@ export interface VersionBumpPlan {
|
|
|
10
10
|
* Computes the version bump for a workspace's package files without writing them.
|
|
11
11
|
*
|
|
12
12
|
* The current version comes from the first package file, and the new one from the release type.
|
|
13
|
-
* Every file's post-bump content is rendered in full, preserving
|
|
14
|
-
*
|
|
13
|
+
* Every file's post-bump content is rendered in full, preserving every field that the file already
|
|
14
|
+
* contains, with two-space indentation and a trailing newline.
|
|
15
15
|
*/
|
|
16
16
|
export declare function planVersionBump(packageFiles: readonly string[], releaseType: ReleaseType): VersionBumpPlan;
|
|
17
17
|
/**
|
|
18
18
|
* Computes an explicit version assignment for a workspace's package files without writing them,
|
|
19
|
-
* bypassing commit-derived bump logic.
|
|
19
|
+
* bypassing commit-derived bump logic. Implements the `--set-version` flag, and so rejects a
|
|
20
20
|
* `newVersion` that is not greater than the current version.
|
|
21
21
|
*/
|
|
22
22
|
export declare function planVersionSet(packageFiles: readonly string[], newVersion: string): VersionBumpPlan;
|
|
23
|
+
/** Reads a workspace's current version from its first package file, as the version planners do. */
|
|
24
|
+
export declare function readCurrentVersion(packageFiles: readonly string[]): string;
|
|
@@ -19,6 +19,9 @@ export function planVersionSet(packageFiles, newVersion) {
|
|
|
19
19
|
}
|
|
20
20
|
return { currentVersion, newVersion, writes: renderVersionWrites(packageFiles, firstFile, firstPkg, newVersion) };
|
|
21
21
|
}
|
|
22
|
+
export function readCurrentVersion(packageFiles) {
|
|
23
|
+
return readPrimaryPackage(packageFiles).firstPkg.version;
|
|
24
|
+
}
|
|
22
25
|
function readPrimaryPackage(packageFiles) {
|
|
23
26
|
const firstFile = packageFiles[0];
|
|
24
27
|
if (firstFile === undefined) {
|
|
@@ -47,12 +47,9 @@ export declare function parseArgs(argv: string[]): {
|
|
|
47
47
|
withReleaseNotes: boolean;
|
|
48
48
|
};
|
|
49
49
|
/**
|
|
50
|
-
* Orchestrates the CLI `prepare` command
|
|
50
|
+
* Orchestrates the CLI `prepare` command: checks the working tree, loads and validates the config, discovers the
|
|
51
|
+
* workspaces, and prepares the release in single-package or monorepo mode.
|
|
51
52
|
*
|
|
52
|
-
*
|
|
53
|
-
* 2. Loads and validates `.config/release-kit.config.ts` (if present).
|
|
54
|
-
* 3. Merges discovered defaults with user config.
|
|
55
|
-
* 4. Delegates to `releasePrepare` or `releasePrepareMono` to compute the release plan.
|
|
56
|
-
* 5. Applies the plan, runs the format command, and prints the result via `reportPrepare`.
|
|
53
|
+
* A relative `--config` resolves against `invocationDir`.
|
|
57
54
|
*/
|
|
58
|
-
export declare function prepareCommand(argv: string[], styles: StreamStyles): Promise<void>;
|
|
55
|
+
export declare function prepareCommand(argv: string[], styles: StreamStyles, invocationDir: string): Promise<void>;
|
|
@@ -16,6 +16,7 @@ import { applyReleasePlan } from "./releasePlan.js";
|
|
|
16
16
|
import { releasePrepare } from "./releasePrepare.js";
|
|
17
17
|
import { releasePrepareMono } from "./releasePrepareMono.js";
|
|
18
18
|
import { reportPrepare } from "./reportPrepare.js";
|
|
19
|
+
import { resolveConfigFlag } from "./resolveConfigFlag.js";
|
|
19
20
|
import { validateOnlyExcludesStrandedDependents } from "./validateOnlyExcludesStrandedDependents.js";
|
|
20
21
|
const VALID_BUMP_TYPES = ['major', 'minor', 'patch'];
|
|
21
22
|
const CANONICAL_SEMVER_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
@@ -77,8 +78,8 @@ export function parseArgs(argv) {
|
|
|
77
78
|
withReleaseNotes: flags.withReleaseNotes,
|
|
78
79
|
};
|
|
79
80
|
}
|
|
80
|
-
export async function prepareCommand(argv, styles) {
|
|
81
|
-
const { configPath, dryRun, force, noGitChecks, bumpOverride, only, setVersion, withReleaseNotes } = parseArgs(argv);
|
|
81
|
+
export async function prepareCommand(argv, styles, invocationDir) {
|
|
82
|
+
const { configPath: configFlag, dryRun, force, noGitChecks, bumpOverride, only, setVersion, withReleaseNotes, } = parseArgs(argv);
|
|
82
83
|
const options = {
|
|
83
84
|
force,
|
|
84
85
|
...(bumpOverride !== undefined && { bumpOverride }),
|
|
@@ -86,7 +87,7 @@ export async function prepareCommand(argv, styles) {
|
|
|
86
87
|
...(withReleaseNotes && { withReleaseNotes: true }),
|
|
87
88
|
};
|
|
88
89
|
if (dryRun) {
|
|
89
|
-
console.info(`\n${formatGlyphLine(RELEASE_GLYPHS, styles.stdout, 'dryRun', 'DRY RUN
|
|
90
|
+
console.info(`\n${formatGlyphLine(RELEASE_GLYPHS, styles.stdout, 'dryRun', 'DRY RUN: release-kit will not modify any files')}\n`);
|
|
90
91
|
}
|
|
91
92
|
if (!dryRun && !noGitChecks) {
|
|
92
93
|
try {
|
|
@@ -97,7 +98,7 @@ export async function prepareCommand(argv, styles) {
|
|
|
97
98
|
process.exit(1);
|
|
98
99
|
}
|
|
99
100
|
}
|
|
100
|
-
const configResult = await loadValidatedConfig(
|
|
101
|
+
const configResult = await loadValidatedConfig(resolveConfigFlag(configFlag, invocationDir));
|
|
101
102
|
if (configResult.status === 'invalid') {
|
|
102
103
|
reportConfigProblem(configResult.problem, styles.stderr);
|
|
103
104
|
process.exit(1);
|
|
@@ -150,6 +151,7 @@ function runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion,
|
|
|
150
151
|
'contributing workspace. To use --set-version, run on a config without a `project` block.');
|
|
151
152
|
process.exit(1);
|
|
152
153
|
}
|
|
154
|
+
let configuredWorkspaceDirs;
|
|
153
155
|
if (only !== undefined) {
|
|
154
156
|
const knownNames = config.workspaces.map((w) => w.dir);
|
|
155
157
|
for (const name of only) {
|
|
@@ -174,6 +176,7 @@ function runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion,
|
|
|
174
176
|
process.stderr.write('Alternatively, run `release-kit prepare` without --only to release everything.\n');
|
|
175
177
|
process.exit(1);
|
|
176
178
|
}
|
|
179
|
+
configuredWorkspaceDirs = knownNames;
|
|
177
180
|
config.workspaces = config.workspaces.filter((w) => only.includes(w.dir));
|
|
178
181
|
}
|
|
179
182
|
if (setVersion !== undefined) {
|
|
@@ -186,7 +189,11 @@ function runMonorepoMode(discoveredPaths, userConfig, options, only, setVersion,
|
|
|
186
189
|
process.exit(1);
|
|
187
190
|
}
|
|
188
191
|
}
|
|
189
|
-
runAndReport(() => releasePrepareMono(config, {
|
|
192
|
+
runAndReport(() => releasePrepareMono(config, {
|
|
193
|
+
...options,
|
|
194
|
+
...(only !== undefined && { only }),
|
|
195
|
+
...(configuredWorkspaceDirs !== undefined && { configuredWorkspaceDirs }),
|
|
196
|
+
}), dryRun, style);
|
|
190
197
|
}
|
|
191
198
|
function runAndReport(computePlan, dryRun, style) {
|
|
192
199
|
let plan;
|
|
@@ -195,7 +202,7 @@ function runAndReport(computePlan, dryRun, style) {
|
|
|
195
202
|
}
|
|
196
203
|
catch (error) {
|
|
197
204
|
reportError(describeError(error));
|
|
198
|
-
process.stderr.write('
|
|
205
|
+
process.stderr.write('The command did not write any files; the working tree is unchanged.\n');
|
|
199
206
|
process.exit(1);
|
|
200
207
|
}
|
|
201
208
|
if (dryRun) {
|
|
@@ -2,7 +2,7 @@ import { type UndeclaredTagPrefix } from './detectUndeclaredTagPrefixes.js';
|
|
|
2
2
|
import type { ReleaseKitConfig } from './types.js';
|
|
3
3
|
/** One workspace's preview row in the tag-prefix preview. */
|
|
4
4
|
export interface TagPrefixPreviewRow {
|
|
5
|
-
/**
|
|
5
|
+
/** Directory path relative to the repo root (e.g., `packages/core`). */
|
|
6
6
|
workspacePath: string;
|
|
7
7
|
/** Directory basename (e.g., `core`). */
|
|
8
8
|
dir: string;
|
|
@@ -10,7 +10,7 @@ export interface TagPrefixPreviewRow {
|
|
|
10
10
|
derivedPrefix: string | null;
|
|
11
11
|
/** Human-readable reason derivation failed, or `null` when it succeeded. */
|
|
12
12
|
derivationError: string | null;
|
|
13
|
-
/** Count of tags matching the derived prefix; `0` when derivation failed or
|
|
13
|
+
/** Count of tags matching the derived prefix; `0` when derivation failed or the repo lacks matching tags. */
|
|
14
14
|
derivedTagCount: number;
|
|
15
15
|
/** One entry per declared legacy prefix, with its current tag count. */
|
|
16
16
|
legacyEntries: LegacyTagPrefixEntry[];
|
|
@@ -40,14 +40,12 @@ export interface TagPrefixPreview {
|
|
|
40
40
|
retiredPackages: RetiredPackagePreviewEntry[];
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
43
|
+
* Builds a structured preview of tag-prefix state for every discovered workspace.
|
|
44
44
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* git tags for undeclared candidate prefixes via `detectUndeclaredTagPrefixes`, and reports collisions
|
|
51
|
-
* across successfully-derived prefixes.
|
|
45
|
+
* Records a workspace's derivation error rather than aborting. For a repo releasing as a single package, the
|
|
46
|
+
* preview doesn't contain any rows; for one declaring patterns that don't resolve to any package, the function
|
|
47
|
+
* throws, because the manifest then needs repairing and a clean table would hide that. Also reports the legacy
|
|
48
|
+
* prefixes that the validated `config` declares per workspace, undeclared candidate prefixes among the local git
|
|
49
|
+
* tags, and collisions across the successfully derived prefixes.
|
|
52
50
|
*/
|
|
53
51
|
export declare function previewTagPrefixes(config?: ReleaseKitConfig): TagPrefixPreview;
|
|
@@ -6,17 +6,16 @@ export interface ReleaseEntry {
|
|
|
6
6
|
/** Present when this workspace was bumped (wholly or partly) due to a dependency update. */
|
|
7
7
|
propagatedFrom?: PropagationSource[];
|
|
8
8
|
/**
|
|
9
|
-
* Explicit new version
|
|
10
|
-
*
|
|
11
|
-
* `releaseType`. Used by the `--set-version` CLI path so propagation reflects the overridden version.
|
|
9
|
+
* Explicit new version, which dependents record in their `propagatedFrom.newVersion` entry in place of a version
|
|
10
|
+
* computed from `releaseType`.
|
|
12
11
|
*/
|
|
13
12
|
newVersionOverride?: string;
|
|
14
13
|
}
|
|
15
14
|
/**
|
|
16
|
-
*
|
|
17
|
-
* not already in the release set
|
|
15
|
+
* Walks upward through the dependency graph breadth-first, adding a `patch` bump for each dependent
|
|
16
|
+
* not already in the release set.
|
|
18
17
|
*
|
|
19
|
-
* Returns the full release set (direct + propagated).
|
|
20
|
-
* propagated dependency
|
|
18
|
+
* Returns the full release set (direct + propagated). An entry already in the set that also has a
|
|
19
|
+
* propagated dependency gains `propagatedFrom` metadata without a change to its bump type.
|
|
21
20
|
*/
|
|
22
21
|
export declare function propagateBumps(directBumps: Map<string, ReleaseEntry>, graph: DependencyGraph): Map<string, ReleaseEntry>;
|
package/dist/esm/publish.d.ts
CHANGED
|
@@ -4,5 +4,5 @@ export interface PublishOptions {
|
|
|
4
4
|
dryRun: boolean;
|
|
5
5
|
provenance: boolean;
|
|
6
6
|
}
|
|
7
|
-
/**
|
|
7
|
+
/** Publishes a single package by running `{pm} publish` from its workspace directory. */
|
|
8
8
|
export declare function publishPackage(resolvedTag: ResolvedTag, packageManager: PackageManager, options: PublishOptions): void;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type StreamStyles } from '@williamthorsen/nmr-core';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Orchestrates the CLI `publish` command: checks for a clean working tree, resolves the tags on HEAD, and publishes
|
|
4
|
+
* each publishable one, injecting release notes into its README for the duration of the publish.
|
|
5
|
+
* A relative `--config` resolves against `invocationDir`.
|
|
5
6
|
*/
|
|
6
|
-
export declare function publishCommand(argv: string[], styles: StreamStyles): Promise<void>;
|
|
7
|
+
export declare function publishCommand(argv: string[], styles: StreamStyles, invocationDir: string): Promise<void>;
|
|
@@ -4,14 +4,15 @@ import { parseArgsOrExit, reportError } from '@williamthorsen/nmr-core';
|
|
|
4
4
|
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
5
5
|
import { assertCleanWorkingTree } from "./assertCleanWorkingTree.js";
|
|
6
6
|
import { configFlagSchema } from "./configFlagSchema.js";
|
|
7
|
+
import { deriveReleaseNotesConfig } from "./deriveReleaseNotesConfig.js";
|
|
7
8
|
import { detectPackageManager } from "./detectPackageManager.js";
|
|
8
9
|
import { formatPrivateSkip } from "./formatPrivateSkip.js";
|
|
9
10
|
import { injectReleaseNotesIntoReadme, resolveReadmePath } from "./injectReleaseNotesIntoReadme.js";
|
|
10
|
-
import {
|
|
11
|
+
import { loadUsableConfig } from "./loadValidatedConfig.js";
|
|
11
12
|
import { parseRequestedTags } from "./parseRequestedTags.js";
|
|
12
13
|
import { publishPackage } from "./publish.js";
|
|
13
14
|
import { resolveCommandTags } from "./resolveCommandTags.js";
|
|
14
|
-
import {
|
|
15
|
+
import { resolveConfigFlag } from "./resolveConfigFlag.js";
|
|
15
16
|
const publishFlagSchema = {
|
|
16
17
|
...configFlagSchema,
|
|
17
18
|
dryRun: { long: '--dry-run', type: 'boolean' },
|
|
@@ -19,10 +20,11 @@ const publishFlagSchema = {
|
|
|
19
20
|
provenance: { long: '--provenance', type: 'boolean' },
|
|
20
21
|
tags: { long: '--tags', type: 'string' },
|
|
21
22
|
};
|
|
22
|
-
export async function publishCommand(argv, styles) {
|
|
23
|
+
export async function publishCommand(argv, styles, invocationDir) {
|
|
23
24
|
const parsed = parseArgsOrExit(argv, publishFlagSchema);
|
|
25
|
+
const configPath = resolveConfigFlag(parsed.flags.config, invocationDir);
|
|
24
26
|
const { dryRun, noGitChecks, provenance } = parsed.flags;
|
|
25
|
-
await
|
|
27
|
+
const userConfig = await loadUsableConfig(styles.stderr, configPath);
|
|
26
28
|
if (!dryRun && !noGitChecks) {
|
|
27
29
|
try {
|
|
28
30
|
assertCleanWorkingTree();
|
|
@@ -33,7 +35,7 @@ export async function publishCommand(argv, styles) {
|
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
const requestedTags = parseRequestedTags(parsed.flags.tags);
|
|
36
|
-
const resolvedTags = resolveCommandTags(requestedTags);
|
|
38
|
+
const resolvedTags = resolveCommandTags(requestedTags, userConfig);
|
|
37
39
|
if (resolvedTags.length === 0) {
|
|
38
40
|
return;
|
|
39
41
|
}
|
|
@@ -43,9 +45,7 @@ export async function publishCommand(argv, styles) {
|
|
|
43
45
|
return;
|
|
44
46
|
}
|
|
45
47
|
const packageManager = detectPackageManager();
|
|
46
|
-
const { releaseNotes, changelogJsonOutputPath, sectionOrder } =
|
|
47
|
-
...(parsed.flags.config !== undefined && { configPath: parsed.flags.config }),
|
|
48
|
-
});
|
|
48
|
+
const { releaseNotes, changelogJsonOutputPath, sectionOrder } = deriveReleaseNotesConfig(userConfig);
|
|
49
49
|
const shouldInject = releaseNotes.shouldInjectIntoReadme;
|
|
50
50
|
console.info(dryRun ? '[dry-run] Would publish:' : 'Publishing:');
|
|
51
51
|
for (const { tag, workspacePath } of publishableTags) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { type StreamStyles } from '@williamthorsen/nmr-core';
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
-
* the release commit and each tag individually.
|
|
3
|
+
* Orchestrates the CLI `push` command: parses flags, loads the config, resolves tags from HEAD, and pushes
|
|
4
|
+
* the release commit and each tag individually. A relative `--config` resolves against `invocationDir`.
|
|
4
5
|
*/
|
|
5
|
-
export declare function pushCommand(argv: string[]): void
|
|
6
|
+
export declare function pushCommand(argv: string[], styles: StreamStyles, invocationDir: string): Promise<void>;
|
package/dist/esm/pushCommand.js
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
import { parseArgsOrExit, reportError } from '@williamthorsen/nmr-core';
|
|
2
2
|
import { describeError } from '@williamthorsen/toolbelt.errors';
|
|
3
|
+
import { configFlagSchema } from "./configFlagSchema.js";
|
|
4
|
+
import { loadUsableConfig } from "./loadValidatedConfig.js";
|
|
3
5
|
import { parseRequestedTags } from "./parseRequestedTags.js";
|
|
4
6
|
import { pushRelease } from "./pushRelease.js";
|
|
5
7
|
import { resolveCommandTags } from "./resolveCommandTags.js";
|
|
8
|
+
import { resolveConfigFlag } from "./resolveConfigFlag.js";
|
|
6
9
|
const pushFlagSchema = {
|
|
10
|
+
...configFlagSchema,
|
|
7
11
|
dryRun: { long: '--dry-run', type: 'boolean' },
|
|
8
12
|
tags: { long: '--tags', type: 'string' },
|
|
9
13
|
tagsOnly: { long: '--tags-only', type: 'boolean' },
|
|
10
14
|
};
|
|
11
|
-
export function pushCommand(argv) {
|
|
15
|
+
export async function pushCommand(argv, styles, invocationDir) {
|
|
12
16
|
const parsed = parseArgsOrExit(argv, pushFlagSchema);
|
|
17
|
+
const configPath = resolveConfigFlag(parsed.flags.config, invocationDir);
|
|
13
18
|
const { dryRun, tagsOnly } = parsed.flags;
|
|
19
|
+
const userConfig = await loadUsableConfig(styles.stderr, configPath);
|
|
14
20
|
const requestedTags = parseRequestedTags(parsed.flags.tags);
|
|
15
|
-
const resolvedTags = resolveCommandTags(requestedTags);
|
|
21
|
+
const resolvedTags = resolveCommandTags(requestedTags, userConfig);
|
|
16
22
|
if (resolvedTags.length === 0) {
|
|
17
23
|
return;
|
|
18
24
|
}
|
|
@@ -9,9 +9,9 @@ export interface PushStep {
|
|
|
9
9
|
command: readonly [string, ...string[]];
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
12
|
+
* Pushes the release commit and each tag individually so that GitHub Actions fires
|
|
13
13
|
* a separate workflow run per tag.
|
|
14
14
|
*
|
|
15
|
-
* Returns the
|
|
15
|
+
* Returns the push steps, which it runs only outside dry-run mode.
|
|
16
16
|
*/
|
|
17
17
|
export declare function pushRelease(resolvedTags: ResolvedTag[], options?: PushReleaseOptions): PushStep[];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ChangelogEntry, ChangelogPreservation } from './types.js';
|
|
2
|
+
/** A `##` section of a `CHANGELOG.md` whose heading names a version. */
|
|
3
|
+
export interface VersionedMarkdownSection {
|
|
4
|
+
version: string;
|
|
5
|
+
/** The section as written, heading included, with trailing blank lines trimmed. */
|
|
6
|
+
text: string;
|
|
7
|
+
}
|
|
8
|
+
/** The `##` sections of a `CHANGELOG.md`, split by whether their heading names a version. */
|
|
9
|
+
export interface ChangelogMarkdownSections {
|
|
10
|
+
versioned: VersionedMarkdownSection[];
|
|
11
|
+
/** Headings of the `##` sections that do not name a version, as written. */
|
|
12
|
+
unversionedHeadings: string[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Plans which sections of the existing `CHANGELOG.md` at `changelogFile` to keep when the file is regenerated from
|
|
16
|
+
* `entries`: every versioned section whose version matches none of the entries. Also returns what to report about the
|
|
17
|
+
* file, or undefined when nothing was kept and nothing was dropped.
|
|
18
|
+
*/
|
|
19
|
+
export declare function planPreservedSections(changelogFile: string, entries: readonly ChangelogEntry[]): {
|
|
20
|
+
sections: VersionedMarkdownSection[];
|
|
21
|
+
preservation: ChangelogPreservation | undefined;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Reads the `##` sections of the `CHANGELOG.md` at `filePath`, excluding the header above the first section and the
|
|
25
|
+
* release-kit footer comment. For a missing file, returns empty section lists.
|
|
26
|
+
*/
|
|
27
|
+
export declare function readChangelogSections(filePath: string): ChangelogMarkdownSections;
|
|
28
|
+
/**
|
|
29
|
+
* Splits `CHANGELOG.md` content at its `##` headings. A section runs to the next `##` heading or the release-kit
|
|
30
|
+
* footer comment; `###` and deeper headings, and any heading inside a fenced code block, belong to the enclosing
|
|
31
|
+
* section.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseChangelogSections(content: string): ChangelogMarkdownSections;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import semver from 'semver';
|
|
3
|
+
export function planPreservedSections(changelogFile, entries) {
|
|
4
|
+
const { versioned, unversionedHeadings } = readChangelogSections(changelogFile);
|
|
5
|
+
const renderedVersions = new Set(entries.map((entry) => entry.version));
|
|
6
|
+
const sections = versioned.filter((section) => !renderedVersions.has(section.version));
|
|
7
|
+
if (sections.length === 0 && unversionedHeadings.length === 0) {
|
|
8
|
+
return { sections, preservation: undefined };
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
sections,
|
|
12
|
+
preservation: {
|
|
13
|
+
file: changelogFile,
|
|
14
|
+
preservedVersions: sections.map((section) => section.version),
|
|
15
|
+
droppedUnversionedHeadings: unversionedHeadings,
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function readChangelogSections(filePath) {
|
|
20
|
+
if (!existsSync(filePath)) {
|
|
21
|
+
return { versioned: [], unversionedHeadings: [] };
|
|
22
|
+
}
|
|
23
|
+
return parseChangelogSections(readFileSync(filePath, 'utf8'));
|
|
24
|
+
}
|
|
25
|
+
export function parseChangelogSections(content) {
|
|
26
|
+
const sections = [];
|
|
27
|
+
let current;
|
|
28
|
+
let fence;
|
|
29
|
+
for (const line of content.split(/\r?\n/)) {
|
|
30
|
+
if (fence === undefined) {
|
|
31
|
+
if (FOOTER_PATTERN.test(line)) {
|
|
32
|
+
current = undefined;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (SECTION_HEADING_PATTERN.test(line)) {
|
|
36
|
+
current = { heading: line.trimEnd(), lines: [] };
|
|
37
|
+
sections.push(current);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
fence = updateFence(fence, line);
|
|
41
|
+
current?.lines.push(line);
|
|
42
|
+
}
|
|
43
|
+
const versioned = [];
|
|
44
|
+
const unversionedHeadings = [];
|
|
45
|
+
for (const { heading, lines } of sections) {
|
|
46
|
+
const version = extractHeadingVersion(heading);
|
|
47
|
+
if (version === undefined) {
|
|
48
|
+
unversionedHeadings.push(heading);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
versioned.push({ version, text: trimTrailingBlankLines(lines).join('\n') });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { versioned, unversionedHeadings };
|
|
55
|
+
}
|
|
56
|
+
const FOOTER_PATTERN = /^<!-- Generated by release-kit\b/;
|
|
57
|
+
const SECTION_HEADING_PATTERN = /^## /;
|
|
58
|
+
const FENCE_PATTERN = /^ {0,3}(`{3,}|~{3,})/;
|
|
59
|
+
const HEADING_VERSION_PATTERN = /(?<![\w.])v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?)(?!\.?\d)/;
|
|
60
|
+
function extractHeadingVersion(heading) {
|
|
61
|
+
const token = HEADING_VERSION_PATTERN.exec(heading)?.[1];
|
|
62
|
+
if (token === undefined)
|
|
63
|
+
return undefined;
|
|
64
|
+
return semver.valid(token) ?? undefined;
|
|
65
|
+
}
|
|
66
|
+
function trimTrailingBlankLines(lines) {
|
|
67
|
+
let end = lines.length;
|
|
68
|
+
while (end > 0 && lines[end - 1]?.trim() === '') {
|
|
69
|
+
end--;
|
|
70
|
+
}
|
|
71
|
+
return lines.slice(0, end);
|
|
72
|
+
}
|
|
73
|
+
function updateFence(fence, line) {
|
|
74
|
+
const marker = FENCE_PATTERN.exec(line)?.[1];
|
|
75
|
+
if (marker === undefined)
|
|
76
|
+
return fence;
|
|
77
|
+
if (fence === undefined)
|
|
78
|
+
return marker;
|
|
79
|
+
return marker[0] === fence[0] && marker.length >= fence.length ? undefined : fence;
|
|
80
|
+
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* File written by the release preparation step, containing one tag per line.
|
|
3
3
|
*
|
|
4
|
-
* Relative to the project root so it works identically in CI and local runs. The reusable
|
|
4
|
+
* Relative to the project root so that it works identically in CI and local runs. The reusable
|
|
5
5
|
* release workflow reads it at this path, so the path and the line-delimited format are a
|
|
6
|
-
* published contract
|
|
6
|
+
* published contract.
|
|
7
7
|
*/
|
|
8
8
|
export declare const RELEASE_TAGS_FILE = "tmp/.release-tags";
|
|
9
9
|
/**
|
|
10
10
|
* File written by the release preparation step, containing the commit body summary.
|
|
11
|
-
* Relative to the project root so it works identically in CI and local runs.
|
|
11
|
+
* Relative to the project root so that it works identically in CI and local runs.
|
|
12
12
|
*/
|
|
13
13
|
export declare const RELEASE_SUMMARY_FILE = "tmp/.release-summary";
|
|
14
14
|
/** Resolves the tags file against the current working directory, for use in diagnostics. */
|
|
@@ -16,10 +16,8 @@ export declare function resolveReleaseTagsPath(): string;
|
|
|
16
16
|
/**
|
|
17
17
|
* Reads the tag names written by `prepare`, one per line, discarding blank lines.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* a genuinely absent file from one the command looked for under a different worktree, and
|
|
22
|
-
* they preserve the underlying errno instead of reporting every failure as a missing file.
|
|
19
|
+
* A failure message names the path as resolved against `process.cwd()`, which distinguishes an
|
|
20
|
+
* absent file from one that the command looked for under a different worktree.
|
|
23
21
|
*
|
|
24
22
|
* An empty or blank file yields an empty array; callers decide whether that is an error.
|
|
25
23
|
*/
|