@ykdz/template 0.0.33 → 0.0.36
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/dist/application.d.ts +2 -1
- package/dist/application.js +64 -75
- package/dist/main.js +6 -2
- package/node_modules/@ykdz/template-builtin-presets/dist/src/foundation.d.ts +50 -36
- package/node_modules/@ykdz/template-builtin-presets/dist/src/foundation.js +970 -365
- package/node_modules/@ykdz/template-builtin-presets/dist/src/generation-record.d.ts +24 -0
- package/node_modules/@ykdz/template-builtin-presets/dist/src/generation-record.js +154 -0
- package/node_modules/@ykdz/template-builtin-presets/dist/src/index.d.ts +2 -2
- package/node_modules/@ykdz/template-builtin-presets/dist/src/index.js +1 -1
- package/node_modules/@ykdz/template-builtin-presets/dist/src/rust-bin/definition.d.ts +31 -2
- package/node_modules/@ykdz/template-builtin-presets/dist/src/rust-bin/definition.js +29 -33
- package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/development-container.d.ts +1 -0
- package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/development-container.js +17 -0
- package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/typescript.d.ts +4 -3
- package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/typescript.js +9 -5
- package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/vue.d.ts +1 -1
- package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/vue.js +1 -2
- package/node_modules/@ykdz/template-builtin-presets/dist/src/ts-cli/definition.d.ts +31 -2
- package/node_modules/@ykdz/template-builtin-presets/dist/src/ts-cli/definition.js +79 -58
- package/node_modules/@ykdz/template-builtin-presets/dist/src/ts-lib/definition.d.ts +31 -2
- package/node_modules/@ykdz/template-builtin-presets/dist/src/ts-lib/definition.js +31 -23
- package/node_modules/@ykdz/template-builtin-presets/dist/src/vike-app/definition.d.ts +22 -2
- package/node_modules/@ykdz/template-builtin-presets/dist/src/vike-app/definition.js +61 -35
- package/node_modules/@ykdz/template-builtin-presets/dist/src/vue-app/definition.d.ts +26 -2
- package/node_modules/@ykdz/template-builtin-presets/dist/src/vue-app/definition.js +19 -8
- package/node_modules/@ykdz/template-builtin-presets/dist/src/vue-hono-app/definition.d.ts +22 -2
- package/node_modules/@ykdz/template-builtin-presets/dist/src/vue-hono-app/definition.js +31 -21
- package/node_modules/@ykdz/template-builtin-presets/package.json +12 -11
- package/node_modules/@ykdz/template-builtin-presets/templates/foundation/.github/workflows/check-deployment-diagnostics.yml +1 -1
- package/node_modules/@ykdz/template-builtin-presets/templates/foundation/.github/workflows/check-deployment.yml +1 -1
- package/node_modules/@ykdz/template-builtin-presets/templates/foundation/.pnpmfile.mjs +19 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/foundation/gitignore +1 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/foundation/tsconfig.packing-hook.json +8 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/shared/devcontainer/github-cli.Dockerfile +9 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/CHANGELOG.md.template +17 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/RELEASING.md +17 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/artifact.ts +1271 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/changelog.ts +283 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/check-artifact.ts +99 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/check-readiness.ts +66 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/cli-command-identity.ts +4 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/handoff.ts +450 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/publish.ts +483 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/readiness.ts +1125 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/release.ts +819 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/release.yml +119 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/root-tsconfig.json +13 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/tsconfig.json +4 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/turbo.json +6 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication-setup/README.md +48 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication-setup/assets/CHANGELOG.md.template +17 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication-setup/assets/LICENSE-APACHE-2.0.txt +202 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication-setup/assets/LICENSE-MIT.txt +21 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication-setup/assets/README.md.template +5 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication-setup/bridge.ts +3336 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication-setup/setup.sh +221 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/src/cli-command-identity.ts +133 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/src/cli.ts +4 -3
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/src/main.ts +5 -4
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/test/integration/command.test.ts +14 -13
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/test/unit/cli-command-identity.test.ts +76 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/tsconfig.build.json +2 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db/src/readiness.ts +1 -1
- package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db/test/global-setup.ts +21 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db/tsconfig.json +6 -1
- package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db/vitest.config.ts +7 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db-migrations/drizzle.config.ts +5 -1
- package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/playwright.config.ts +102 -22
- package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/test/playwright-teardown.ts +12 -0
- package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/vite.config.ts +39 -1
- package/node_modules/@ykdz/template-builtin-presets/templates/vue-app/playwright.config.ts +48 -7
- package/node_modules/@ykdz/template-builtin-presets/templates/vue-hono-app/web/playwright.config.ts +59 -8
- package/node_modules/@ykdz/template-core/dist/module-graph.d.ts +15 -53
- package/node_modules/@ykdz/template-core/dist/module-graph.js +134 -101
- package/node_modules/@ykdz/template-core/dist/package-contribution.d.ts +13 -1
- package/node_modules/@ykdz/template-core/dist/package-contribution.js +92 -0
- package/node_modules/@ykdz/template-core/dist/pnpm-workspace.yaml +14 -10
- package/node_modules/@ykdz/template-core/dist/preset-definition.d.ts +71 -13
- package/node_modules/@ykdz/template-core/dist/preset-definition.js +16 -1
- package/node_modules/@ykdz/template-core/dist/project-blueprint.d.ts +56 -0
- package/node_modules/@ykdz/template-core/dist/project-blueprint.js +271 -0
- package/node_modules/@ykdz/template-core/dist/project-linking-v2.d.ts +2 -2
- package/node_modules/@ykdz/template-core/dist/project-linking-v2.js +2 -2
- package/node_modules/@ykdz/template-core/dist/project-projection.d.ts +2 -1
- package/node_modules/@ykdz/template-core/dist/project-projection.js +107 -20
- package/node_modules/@ykdz/template-core/dist/renderer.d.ts +0 -2
- package/node_modules/@ykdz/template-core/dist/renderer.js +17 -38
- package/node_modules/@ykdz/template-core/dist/template-boundary-check.d.ts +2 -2
- package/node_modules/@ykdz/template-core/dist/template-boundary-check.js +6 -0
- package/node_modules/@ykdz/template-core/dist/toolchain-resolution.d.ts +2 -0
- package/node_modules/@ykdz/template-core/dist/toolchain-resolution.js +9 -3
- package/package.json +3 -3
- package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db-migrations/scripts/prepare-database.ts +0 -45
- package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/scripts/run-playwright.ts +0 -151
- package/node_modules/@ykdz/template-builtin-presets/templates/vue-app/scripts/run-playwright.ts +0 -62
- package/node_modules/@ykdz/template-builtin-presets/templates/vue-hono-app/web/scripts/run-playwright.ts +0 -103
- package/node_modules/@ykdz/template-core/dist/project-blueprint-v2.d.ts +0 -33
- package/node_modules/@ykdz/template-core/dist/project-blueprint-v2.js +0 -182
|
@@ -0,0 +1,1125 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { isBuiltin } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { parse as parseSemver, valid as validSemver } from "semver";
|
|
6
|
+
import parseSpdxExpression from "spdx-expression-parse";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
inspectPackageReleaseChangelog,
|
|
10
|
+
type ChangelogBlockerCode,
|
|
11
|
+
} from "./changelog.ts";
|
|
12
|
+
import { cliCommandIdentity } from "./cli-command-identity.ts";
|
|
13
|
+
|
|
14
|
+
export type NpmPublicationReadinessBlockerCode =
|
|
15
|
+
| "package-name"
|
|
16
|
+
| "cli-bin"
|
|
17
|
+
| "description"
|
|
18
|
+
| "version"
|
|
19
|
+
| "private"
|
|
20
|
+
| "manifest-contract"
|
|
21
|
+
| "license-id"
|
|
22
|
+
| "license-content"
|
|
23
|
+
| "repository"
|
|
24
|
+
| "homepage"
|
|
25
|
+
| "bugs"
|
|
26
|
+
| "readme-missing"
|
|
27
|
+
| "readme-content"
|
|
28
|
+
| "license-missing"
|
|
29
|
+
| "license-mismatch"
|
|
30
|
+
| "changelog-missing"
|
|
31
|
+
| ChangelogBlockerCode
|
|
32
|
+
| "local-template-metadata-missing"
|
|
33
|
+
| "local-template-metadata-invalid"
|
|
34
|
+
| "local-template-metadata-mismatch"
|
|
35
|
+
| "private-runtime-dependency";
|
|
36
|
+
|
|
37
|
+
export type NpmPublicationReadinessBlocker = {
|
|
38
|
+
readonly code: NpmPublicationReadinessBlockerCode;
|
|
39
|
+
readonly owner: {
|
|
40
|
+
readonly path: string;
|
|
41
|
+
readonly pointer?: string;
|
|
42
|
+
};
|
|
43
|
+
readonly observed: string;
|
|
44
|
+
readonly expected: string;
|
|
45
|
+
readonly nextAction: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type NpmPublicationReadiness =
|
|
49
|
+
| {
|
|
50
|
+
readonly kind: "blocked";
|
|
51
|
+
readonly mode: "safe-unconfigured" | "public-intent";
|
|
52
|
+
readonly target: { readonly packagePath: string };
|
|
53
|
+
readonly blockers: readonly [
|
|
54
|
+
NpmPublicationReadinessBlocker,
|
|
55
|
+
...NpmPublicationReadinessBlocker[],
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
| {
|
|
59
|
+
readonly kind: "ready";
|
|
60
|
+
readonly target: { readonly packagePath: string };
|
|
61
|
+
readonly blockers: readonly [];
|
|
62
|
+
readonly publication: {
|
|
63
|
+
readonly packagePath: string;
|
|
64
|
+
readonly packageName: string;
|
|
65
|
+
readonly commandName: string;
|
|
66
|
+
readonly version: string;
|
|
67
|
+
readonly repository: string;
|
|
68
|
+
readonly releaseDate: string;
|
|
69
|
+
readonly releaseNotes: string;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
type JsonObject = Record<string, unknown>;
|
|
74
|
+
|
|
75
|
+
type TextFact =
|
|
76
|
+
| { readonly kind: "present"; readonly source: string }
|
|
77
|
+
| { readonly kind: "missing" }
|
|
78
|
+
| { readonly kind: "unreadable"; readonly reason: string };
|
|
79
|
+
|
|
80
|
+
const publicationFiles = [
|
|
81
|
+
"dist",
|
|
82
|
+
"README.md",
|
|
83
|
+
"LICENSE",
|
|
84
|
+
"CHANGELOG.md",
|
|
85
|
+
] as const;
|
|
86
|
+
|
|
87
|
+
const canonicalPublicationPrepack = "pnpm exec turbo run build --filter=.";
|
|
88
|
+
|
|
89
|
+
function isObject(value: unknown): value is JsonObject {
|
|
90
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function display(value: unknown): string {
|
|
94
|
+
if (value === undefined) return "missing";
|
|
95
|
+
try {
|
|
96
|
+
return JSON.stringify(value);
|
|
97
|
+
} catch {
|
|
98
|
+
return `[unserializable ${typeof value}]`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function blocker(options: {
|
|
103
|
+
readonly code: NpmPublicationReadinessBlockerCode;
|
|
104
|
+
readonly path: string;
|
|
105
|
+
readonly pointer?: string;
|
|
106
|
+
readonly observed: string;
|
|
107
|
+
readonly expected: string;
|
|
108
|
+
readonly nextAction: string;
|
|
109
|
+
}): NpmPublicationReadinessBlocker {
|
|
110
|
+
return {
|
|
111
|
+
code: options.code,
|
|
112
|
+
owner: {
|
|
113
|
+
path: options.path,
|
|
114
|
+
...(options.pointer === undefined ? {} : { pointer: options.pointer }),
|
|
115
|
+
},
|
|
116
|
+
observed: options.observed,
|
|
117
|
+
expected: options.expected,
|
|
118
|
+
nextAction: options.nextAction,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function compareBlockers(
|
|
123
|
+
left: NpmPublicationReadinessBlocker,
|
|
124
|
+
right: NpmPublicationReadinessBlocker,
|
|
125
|
+
): number {
|
|
126
|
+
const leftKey = `${left.owner.path}\u0000${left.owner.pointer ?? ""}\u0000${left.code}`;
|
|
127
|
+
const rightKey = `${right.owner.path}\u0000${right.owner.pointer ?? ""}\u0000${right.code}`;
|
|
128
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function textFact(filePath: string): Promise<TextFact> {
|
|
132
|
+
try {
|
|
133
|
+
return { kind: "present", source: await readFile(filePath, "utf8") };
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (
|
|
136
|
+
isObject(error) &&
|
|
137
|
+
(error.code === "ENOENT" || error.code === "ENOTDIR")
|
|
138
|
+
) {
|
|
139
|
+
return { kind: "missing" };
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
kind: "unreadable",
|
|
143
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function parseJsonFact(fact: TextFact): JsonObject | undefined {
|
|
149
|
+
if (fact.kind !== "present") return undefined;
|
|
150
|
+
try {
|
|
151
|
+
const value: unknown = JSON.parse(fact.source);
|
|
152
|
+
return isObject(value) ? value : undefined;
|
|
153
|
+
} catch {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function validNpmPackageName(value: string): boolean {
|
|
159
|
+
if (
|
|
160
|
+
value.length === 0 ||
|
|
161
|
+
value.length > 214 ||
|
|
162
|
+
value !== value.trim() ||
|
|
163
|
+
value !== value.toLowerCase() ||
|
|
164
|
+
value === "." ||
|
|
165
|
+
value === ".." ||
|
|
166
|
+
value === "node_modules" ||
|
|
167
|
+
value === "favicon.ico" ||
|
|
168
|
+
isBuiltin(value)
|
|
169
|
+
) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const scoped = /^@([^/]+)\/([^/]+)$/u.exec(value);
|
|
174
|
+
if (value.startsWith("@") && scoped === null) return false;
|
|
175
|
+
const scope = scoped?.[1];
|
|
176
|
+
const leaf = scoped?.[2] ?? value;
|
|
177
|
+
if (scope === undefined && /^[._-]/u.test(leaf)) return false;
|
|
178
|
+
if (scope !== undefined && leaf.startsWith(".")) return false;
|
|
179
|
+
if (/[~'!()*]/u.test(leaf)) return false;
|
|
180
|
+
return (
|
|
181
|
+
encodeURIComponent(leaf) === leaf &&
|
|
182
|
+
(scope === undefined || encodeURIComponent(scope) === scope)
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function stableVersion(value: unknown): value is string {
|
|
187
|
+
if (typeof value !== "string" || validSemver(value) !== value) return false;
|
|
188
|
+
const parsed = parseSemver(value);
|
|
189
|
+
return (
|
|
190
|
+
parsed !== null &&
|
|
191
|
+
parsed.prerelease.length === 0 &&
|
|
192
|
+
parsed.build.length === 0
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function validSpdx(value: unknown): value is string {
|
|
197
|
+
if (typeof value !== "string" || value.length === 0) return false;
|
|
198
|
+
try {
|
|
199
|
+
parseSpdxExpression(value);
|
|
200
|
+
return true;
|
|
201
|
+
} catch {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function exactStringSet(value: unknown, expected: readonly string[]): boolean {
|
|
207
|
+
return (
|
|
208
|
+
Array.isArray(value) &&
|
|
209
|
+
value.every((item): item is string => typeof item === "string") &&
|
|
210
|
+
value.length === expected.length &&
|
|
211
|
+
new Set(value).size === value.length &&
|
|
212
|
+
expected.every((item) => value.includes(item))
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function nonPlaceholderText(source: string): boolean {
|
|
217
|
+
const normalized = source.trim();
|
|
218
|
+
return (
|
|
219
|
+
normalized.length > 0 &&
|
|
220
|
+
!/\bTODO\b/iu.test(normalized) &&
|
|
221
|
+
!/^(?:your name|name of copyright owner|copyright holder|\{\{[A-Z][A-Z0-9_]*\}\}|\[(?:yyyy|name of copyright owner|copyright holder)\])$/iu.test(
|
|
222
|
+
normalized,
|
|
223
|
+
)
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function hasNonPlaceholderCopyrightHolder(source: string): boolean {
|
|
228
|
+
return [
|
|
229
|
+
...source.matchAll(
|
|
230
|
+
/^Copyright(?: \(c\))?\s+(?:\d{4}(?:-\d{4})?\s+)?([^\r\n]+)$/gimu,
|
|
231
|
+
),
|
|
232
|
+
].some(
|
|
233
|
+
(match) =>
|
|
234
|
+
nonPlaceholderText(match[1]!) &&
|
|
235
|
+
!/\[(?:yyyy|name of copyright owner|copyright holder)\]/iu.test(
|
|
236
|
+
match[1]!,
|
|
237
|
+
),
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function githubRepository(
|
|
242
|
+
value: unknown,
|
|
243
|
+
packagePath: string,
|
|
244
|
+
):
|
|
245
|
+
| {
|
|
246
|
+
readonly canonicalUrl: string;
|
|
247
|
+
readonly webUrl: string;
|
|
248
|
+
}
|
|
249
|
+
| undefined {
|
|
250
|
+
if (!isObject(value)) return undefined;
|
|
251
|
+
if (
|
|
252
|
+
Object.keys(value).toSorted().join("\u0000") !==
|
|
253
|
+
["directory", "type", "url"].toSorted().join("\u0000") ||
|
|
254
|
+
value.type !== "git" ||
|
|
255
|
+
value.directory !== packagePath ||
|
|
256
|
+
typeof value.url !== "string"
|
|
257
|
+
) {
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
const match =
|
|
261
|
+
/^git\+https:\/\/github\.com\/([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\/([A-Za-z0-9._-]+)\.git$/u.exec(
|
|
262
|
+
value.url,
|
|
263
|
+
);
|
|
264
|
+
if (match === null || match[2]!.endsWith(".")) return undefined;
|
|
265
|
+
return {
|
|
266
|
+
canonicalUrl: value.url,
|
|
267
|
+
webUrl: `https://github.com/${match[1]}/${match[2]}`,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function validRecordOfStrings(value: unknown): value is JsonObject {
|
|
272
|
+
return (
|
|
273
|
+
isObject(value) &&
|
|
274
|
+
Object.values(value).every((item) => typeof item === "string")
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function jsonPointerSegment(value: string): string {
|
|
279
|
+
return value.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function runtimeDependencyNames(manifest: JsonObject): readonly string[] {
|
|
283
|
+
return [manifest.dependencies, manifest.optionalDependencies].flatMap(
|
|
284
|
+
(value) => (isObject(value) ? Object.keys(value) : []),
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function workspacePatterns(source: string): readonly string[] {
|
|
289
|
+
const lines = source.split(/\r?\n/u);
|
|
290
|
+
const patterns: string[] = [];
|
|
291
|
+
let inPackages = false;
|
|
292
|
+
for (const line of lines) {
|
|
293
|
+
if (/^packages:\s*$/u.test(line)) {
|
|
294
|
+
inPackages = true;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (inPackages && /^\S/u.test(line)) break;
|
|
298
|
+
const item = /^\s+-\s+["']?([^"']+?)["']?\s*$/u.exec(line)?.[1];
|
|
299
|
+
if (inPackages && item !== undefined) patterns.push(item);
|
|
300
|
+
}
|
|
301
|
+
return patterns;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function actualWorkspaceManifests(repositoryRoot: string): Promise<{
|
|
305
|
+
readonly manifests: readonly {
|
|
306
|
+
readonly path: string;
|
|
307
|
+
readonly manifest: JsonObject;
|
|
308
|
+
}[];
|
|
309
|
+
readonly failures: readonly {
|
|
310
|
+
readonly path: string;
|
|
311
|
+
readonly reason: string;
|
|
312
|
+
}[];
|
|
313
|
+
}> {
|
|
314
|
+
const workspace = await textFact(
|
|
315
|
+
path.join(repositoryRoot, "pnpm-workspace.yaml"),
|
|
316
|
+
);
|
|
317
|
+
if (workspace.kind !== "present") {
|
|
318
|
+
return {
|
|
319
|
+
manifests: [],
|
|
320
|
+
failures: [
|
|
321
|
+
{
|
|
322
|
+
path: "pnpm-workspace.yaml",
|
|
323
|
+
reason: workspace.kind === "missing" ? "missing" : workspace.reason,
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
const manifests: { path: string; manifest: JsonObject }[] = [];
|
|
329
|
+
const failures: { path: string; reason: string }[] = [];
|
|
330
|
+
const patterns = workspacePatterns(workspace.source);
|
|
331
|
+
if (patterns.length === 0) {
|
|
332
|
+
failures.push({
|
|
333
|
+
path: "pnpm-workspace.yaml",
|
|
334
|
+
reason: "No workspace package patterns were found",
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
for (const pattern of patterns) {
|
|
338
|
+
const match = /^([a-z0-9][a-z0-9-]*)\/\*$/u.exec(pattern);
|
|
339
|
+
if (match === null) {
|
|
340
|
+
failures.push({
|
|
341
|
+
path: "pnpm-workspace.yaml",
|
|
342
|
+
reason: `Unsupported workspace package pattern ${JSON.stringify(pattern)}`,
|
|
343
|
+
});
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const collection = match[1]!;
|
|
347
|
+
let entries;
|
|
348
|
+
try {
|
|
349
|
+
entries = await readdir(path.join(repositoryRoot, collection), {
|
|
350
|
+
withFileTypes: true,
|
|
351
|
+
});
|
|
352
|
+
} catch (error) {
|
|
353
|
+
if (isObject(error) && error.code === "ENOENT") continue;
|
|
354
|
+
failures.push({
|
|
355
|
+
path: collection,
|
|
356
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
357
|
+
});
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
for (const entry of entries) {
|
|
361
|
+
if (!entry.isDirectory()) continue;
|
|
362
|
+
const packagePath = `${collection}/${entry.name}`;
|
|
363
|
+
const manifestFact = await textFact(
|
|
364
|
+
path.join(repositoryRoot, packagePath, "package.json"),
|
|
365
|
+
);
|
|
366
|
+
if (manifestFact.kind === "missing") continue;
|
|
367
|
+
const manifest = parseJsonFact(manifestFact);
|
|
368
|
+
if (manifest === undefined) {
|
|
369
|
+
failures.push({
|
|
370
|
+
path: `${packagePath}/package.json`,
|
|
371
|
+
reason:
|
|
372
|
+
manifestFact.kind === "unreadable"
|
|
373
|
+
? manifestFact.reason
|
|
374
|
+
: "Invalid JSON object",
|
|
375
|
+
});
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (manifest !== undefined)
|
|
379
|
+
manifests.push({ path: packagePath, manifest });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return { manifests, failures };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function addManifestBlockers(options: {
|
|
386
|
+
readonly manifest: JsonObject;
|
|
387
|
+
readonly manifestPath: string;
|
|
388
|
+
readonly packagePath: string;
|
|
389
|
+
readonly rootManifest: JsonObject | undefined;
|
|
390
|
+
readonly blockers: NpmPublicationReadinessBlocker[];
|
|
391
|
+
}): {
|
|
392
|
+
readonly packageName?: string;
|
|
393
|
+
readonly commandName?: string;
|
|
394
|
+
readonly version?: string;
|
|
395
|
+
readonly repository?: {
|
|
396
|
+
readonly canonicalUrl: string;
|
|
397
|
+
readonly webUrl: string;
|
|
398
|
+
};
|
|
399
|
+
readonly license?: string;
|
|
400
|
+
} {
|
|
401
|
+
const { blockers, manifest, manifestPath, packagePath, rootManifest } =
|
|
402
|
+
options;
|
|
403
|
+
const packageName =
|
|
404
|
+
typeof manifest.name === "string" && validNpmPackageName(manifest.name)
|
|
405
|
+
? manifest.name
|
|
406
|
+
: undefined;
|
|
407
|
+
if (packageName === undefined) {
|
|
408
|
+
blockers.push(
|
|
409
|
+
blocker({
|
|
410
|
+
code: "package-name",
|
|
411
|
+
path: manifestPath,
|
|
412
|
+
pointer: "/name",
|
|
413
|
+
observed: display(manifest.name),
|
|
414
|
+
expected: "A valid scoped or unscoped npm package name",
|
|
415
|
+
nextAction: "Set the final public npm package name.",
|
|
416
|
+
}),
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
let commandName: string | undefined;
|
|
421
|
+
try {
|
|
422
|
+
commandName = cliCommandIdentity(manifest).commandName;
|
|
423
|
+
} catch (error) {
|
|
424
|
+
blockers.push(
|
|
425
|
+
blocker({
|
|
426
|
+
code: "cli-bin",
|
|
427
|
+
path: manifestPath,
|
|
428
|
+
pointer: "/bin",
|
|
429
|
+
observed: error instanceof Error ? error.message : String(error),
|
|
430
|
+
expected: 'Exactly one safe bin that executes "./dist/cli.js"',
|
|
431
|
+
nextAction: "Correct the public CLI bin declaration.",
|
|
432
|
+
}),
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (
|
|
437
|
+
typeof manifest.description !== "string" ||
|
|
438
|
+
manifest.description.trim().length === 0
|
|
439
|
+
) {
|
|
440
|
+
blockers.push(
|
|
441
|
+
blocker({
|
|
442
|
+
code: "description",
|
|
443
|
+
path: manifestPath,
|
|
444
|
+
pointer: "/description",
|
|
445
|
+
observed: display(manifest.description),
|
|
446
|
+
expected: "A non-empty public package description",
|
|
447
|
+
nextAction: "Describe the CLI for npm consumers.",
|
|
448
|
+
}),
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const version = stableVersion(manifest.version)
|
|
453
|
+
? manifest.version
|
|
454
|
+
: undefined;
|
|
455
|
+
if (version === undefined) {
|
|
456
|
+
blockers.push(
|
|
457
|
+
blocker({
|
|
458
|
+
code: "version",
|
|
459
|
+
path: manifestPath,
|
|
460
|
+
pointer: "/version",
|
|
461
|
+
observed: display(manifest.version),
|
|
462
|
+
expected: "A stable exact SemVer version",
|
|
463
|
+
nextAction: "Set the reviewed public package version.",
|
|
464
|
+
}),
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (Object.hasOwn(manifest, "private")) {
|
|
469
|
+
blockers.push(
|
|
470
|
+
blocker({
|
|
471
|
+
code: "private",
|
|
472
|
+
path: manifestPath,
|
|
473
|
+
pointer: "/private",
|
|
474
|
+
observed: display(manifest.private),
|
|
475
|
+
expected: "The private field to be absent",
|
|
476
|
+
nextAction:
|
|
477
|
+
"Remove private only after every publication fact is ready.",
|
|
478
|
+
}),
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const license = validSpdx(manifest.license) ? manifest.license : undefined;
|
|
483
|
+
if (license === undefined) {
|
|
484
|
+
blockers.push(
|
|
485
|
+
blocker({
|
|
486
|
+
code: "license-id",
|
|
487
|
+
path: manifestPath,
|
|
488
|
+
pointer: "/license",
|
|
489
|
+
observed: display(manifest.license),
|
|
490
|
+
expected: "A valid SPDX license expression",
|
|
491
|
+
nextAction: "Set the reviewed SPDX license identifier.",
|
|
492
|
+
}),
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const repository = githubRepository(manifest.repository, packagePath);
|
|
497
|
+
if (repository === undefined) {
|
|
498
|
+
blockers.push(
|
|
499
|
+
blocker({
|
|
500
|
+
code: "repository",
|
|
501
|
+
path: manifestPath,
|
|
502
|
+
pointer: "/repository",
|
|
503
|
+
observed: display(manifest.repository),
|
|
504
|
+
expected:
|
|
505
|
+
"A canonical public GitHub repository object with the Package Path directory",
|
|
506
|
+
nextAction: "Set the exact public GitHub repository metadata.",
|
|
507
|
+
}),
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
if (
|
|
511
|
+
repository === undefined ||
|
|
512
|
+
manifest.homepage !== `${repository.webUrl}#readme`
|
|
513
|
+
) {
|
|
514
|
+
blockers.push(
|
|
515
|
+
blocker({
|
|
516
|
+
code: "homepage",
|
|
517
|
+
path: manifestPath,
|
|
518
|
+
pointer: "/homepage",
|
|
519
|
+
observed: display(manifest.homepage),
|
|
520
|
+
expected:
|
|
521
|
+
repository === undefined
|
|
522
|
+
? "A homepage derived from the canonical GitHub repository"
|
|
523
|
+
: `${repository.webUrl}#readme`,
|
|
524
|
+
nextAction: "Set the package homepage from the public repository.",
|
|
525
|
+
}),
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
const expectedBugs =
|
|
529
|
+
repository === undefined ? undefined : `${repository.webUrl}/issues`;
|
|
530
|
+
if (
|
|
531
|
+
expectedBugs === undefined ||
|
|
532
|
+
!isObject(manifest.bugs) ||
|
|
533
|
+
Object.keys(manifest.bugs).length !== 1 ||
|
|
534
|
+
manifest.bugs.url !== expectedBugs
|
|
535
|
+
) {
|
|
536
|
+
blockers.push(
|
|
537
|
+
blocker({
|
|
538
|
+
code: "bugs",
|
|
539
|
+
path: manifestPath,
|
|
540
|
+
pointer: "/bugs",
|
|
541
|
+
observed: display(manifest.bugs),
|
|
542
|
+
expected:
|
|
543
|
+
expectedBugs === undefined
|
|
544
|
+
? "An issues URL derived from the canonical GitHub repository"
|
|
545
|
+
: display({ url: expectedBugs }),
|
|
546
|
+
nextAction: "Set the package issues URL from the public repository.",
|
|
547
|
+
}),
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const baseline =
|
|
552
|
+
isObject(rootManifest?.engines) &&
|
|
553
|
+
typeof rootManifest.engines.node === "string" &&
|
|
554
|
+
/^\d+$/u.test(rootManifest.engines.node)
|
|
555
|
+
? rootManifest.engines.node
|
|
556
|
+
: undefined;
|
|
557
|
+
const publishConfig = manifest.publishConfig;
|
|
558
|
+
const scripts = manifest.scripts;
|
|
559
|
+
const forbiddenFields = [
|
|
560
|
+
"main",
|
|
561
|
+
"types",
|
|
562
|
+
"exports",
|
|
563
|
+
"imports",
|
|
564
|
+
"provenance",
|
|
565
|
+
].filter((field) => Object.hasOwn(manifest, field));
|
|
566
|
+
const manifestContractValid =
|
|
567
|
+
manifest.type === "module" &&
|
|
568
|
+
exactStringSet(manifest.files, publicationFiles) &&
|
|
569
|
+
baseline !== undefined &&
|
|
570
|
+
isObject(manifest.engines) &&
|
|
571
|
+
manifest.engines.node === `>=${baseline}` &&
|
|
572
|
+
validRecordOfStrings(manifest.dependencies) &&
|
|
573
|
+
typeof manifest.dependencies.commander === "string" &&
|
|
574
|
+
manifest.dependencies.commander.trim().length > 0 &&
|
|
575
|
+
isObject(scripts) &&
|
|
576
|
+
typeof scripts.build === "string" &&
|
|
577
|
+
scripts.build.length > 0 &&
|
|
578
|
+
scripts.prepack === canonicalPublicationPrepack &&
|
|
579
|
+
isObject(publishConfig) &&
|
|
580
|
+
Object.keys(publishConfig).toSorted().join("\u0000") ===
|
|
581
|
+
["access", "registry"].join("\u0000") &&
|
|
582
|
+
publishConfig.access === "public" &&
|
|
583
|
+
publishConfig.registry === "https://registry.npmjs.org/" &&
|
|
584
|
+
forbiddenFields.length === 0;
|
|
585
|
+
if (!manifestContractValid) {
|
|
586
|
+
blockers.push(
|
|
587
|
+
blocker({
|
|
588
|
+
code: "manifest-contract",
|
|
589
|
+
path: manifestPath,
|
|
590
|
+
observed: display({
|
|
591
|
+
type: manifest.type,
|
|
592
|
+
files: manifest.files,
|
|
593
|
+
engines: manifest.engines,
|
|
594
|
+
scripts: manifest.scripts,
|
|
595
|
+
dependencies: manifest.dependencies,
|
|
596
|
+
publishConfig: manifest.publishConfig,
|
|
597
|
+
forbiddenFields,
|
|
598
|
+
}),
|
|
599
|
+
expected:
|
|
600
|
+
"The minimal public CLI manifest contract derived from the root Node baseline",
|
|
601
|
+
nextAction:
|
|
602
|
+
"Restore the minimal CLI files, engines, scripts, dependencies, and public publish configuration.",
|
|
603
|
+
}),
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return {
|
|
608
|
+
...(packageName === undefined ? {} : { packageName }),
|
|
609
|
+
...(commandName === undefined ? {} : { commandName }),
|
|
610
|
+
...(version === undefined ? {} : { version }),
|
|
611
|
+
...(repository === undefined ? {} : { repository }),
|
|
612
|
+
...(license === undefined ? {} : { license }),
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
async function addDocumentBlockers(options: {
|
|
617
|
+
readonly repositoryRoot: string;
|
|
618
|
+
readonly packagePath: string;
|
|
619
|
+
readonly version?: string;
|
|
620
|
+
readonly license?: string;
|
|
621
|
+
readonly blockers: NpmPublicationReadinessBlocker[];
|
|
622
|
+
}): Promise<{
|
|
623
|
+
readonly releaseDate?: string;
|
|
624
|
+
readonly releaseNotes?: string;
|
|
625
|
+
}> {
|
|
626
|
+
const { blockers, packagePath, repositoryRoot } = options;
|
|
627
|
+
const readmePath = `${packagePath}/README.md`;
|
|
628
|
+
const readme = await textFact(path.join(repositoryRoot, readmePath));
|
|
629
|
+
if (readme.kind === "missing") {
|
|
630
|
+
blockers.push(
|
|
631
|
+
blocker({
|
|
632
|
+
code: "readme-missing",
|
|
633
|
+
path: readmePath,
|
|
634
|
+
observed: "missing",
|
|
635
|
+
expected: "An independent package README",
|
|
636
|
+
nextAction: "Create the public package README.",
|
|
637
|
+
}),
|
|
638
|
+
);
|
|
639
|
+
} else if (
|
|
640
|
+
readme.kind === "unreadable" ||
|
|
641
|
+
!nonPlaceholderText(readme.source)
|
|
642
|
+
) {
|
|
643
|
+
blockers.push(
|
|
644
|
+
blocker({
|
|
645
|
+
code: "readme-content",
|
|
646
|
+
path: readmePath,
|
|
647
|
+
observed:
|
|
648
|
+
readme.kind === "unreadable"
|
|
649
|
+
? readme.reason
|
|
650
|
+
: "empty or placeholder text",
|
|
651
|
+
expected: "Non-placeholder documentation for npm consumers",
|
|
652
|
+
nextAction: "Write the public package README.",
|
|
653
|
+
}),
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const rootLicensePath = "LICENSE";
|
|
658
|
+
const packageLicensePath = `${packagePath}/LICENSE`;
|
|
659
|
+
const [rootLicense, packageLicense] = await Promise.all([
|
|
660
|
+
textFact(path.join(repositoryRoot, rootLicensePath)),
|
|
661
|
+
textFact(path.join(repositoryRoot, packageLicensePath)),
|
|
662
|
+
]);
|
|
663
|
+
for (const [licensePath, fact] of [
|
|
664
|
+
[rootLicensePath, rootLicense],
|
|
665
|
+
[packageLicensePath, packageLicense],
|
|
666
|
+
] as const) {
|
|
667
|
+
if (fact.kind === "missing") {
|
|
668
|
+
blockers.push(
|
|
669
|
+
blocker({
|
|
670
|
+
code: "license-missing",
|
|
671
|
+
path: licensePath,
|
|
672
|
+
observed: "missing",
|
|
673
|
+
expected: "The reviewed public package license text",
|
|
674
|
+
nextAction: "Create matching root and package LICENSE files.",
|
|
675
|
+
}),
|
|
676
|
+
);
|
|
677
|
+
} else if (fact.kind === "unreadable" || !nonPlaceholderText(fact.source)) {
|
|
678
|
+
blockers.push(
|
|
679
|
+
blocker({
|
|
680
|
+
code: "license-content",
|
|
681
|
+
path: licensePath,
|
|
682
|
+
observed:
|
|
683
|
+
fact.kind === "unreadable"
|
|
684
|
+
? fact.reason
|
|
685
|
+
: "empty or placeholder text",
|
|
686
|
+
expected: "Non-placeholder reviewed license text",
|
|
687
|
+
nextAction: "Write the selected license text and real holder facts.",
|
|
688
|
+
}),
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (
|
|
693
|
+
rootLicense.kind === "present" &&
|
|
694
|
+
packageLicense.kind === "present" &&
|
|
695
|
+
rootLicense.source !== packageLicense.source
|
|
696
|
+
) {
|
|
697
|
+
blockers.push(
|
|
698
|
+
blocker({
|
|
699
|
+
code: "license-mismatch",
|
|
700
|
+
path: packageLicensePath,
|
|
701
|
+
observed: "Package LICENSE bytes differ from root LICENSE",
|
|
702
|
+
expected: "Byte-identical root and package LICENSE files",
|
|
703
|
+
nextAction: "Copy the reviewed license bytes to both owners.",
|
|
704
|
+
}),
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
if (
|
|
708
|
+
options.license !== undefined &&
|
|
709
|
+
(options.license === "MIT" || options.license === "Apache-2.0") &&
|
|
710
|
+
packageLicense.kind === "present" &&
|
|
711
|
+
!hasNonPlaceholderCopyrightHolder(packageLicense.source)
|
|
712
|
+
) {
|
|
713
|
+
blockers.push(
|
|
714
|
+
blocker({
|
|
715
|
+
code: "license-content",
|
|
716
|
+
path: packageLicensePath,
|
|
717
|
+
observed: "LICENSE does not contain a non-placeholder copyright holder",
|
|
718
|
+
expected: `${options.license} license text with a non-placeholder copyright holder`,
|
|
719
|
+
nextAction:
|
|
720
|
+
"Write the selected license with the real copyright holder.",
|
|
721
|
+
}),
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const changelogPath = `${packagePath}/CHANGELOG.md`;
|
|
726
|
+
const changelog = await textFact(path.join(repositoryRoot, changelogPath));
|
|
727
|
+
if (changelog.kind !== "present") {
|
|
728
|
+
blockers.push(
|
|
729
|
+
blocker({
|
|
730
|
+
code: "changelog-missing",
|
|
731
|
+
path: changelogPath,
|
|
732
|
+
observed: changelog.kind === "missing" ? "missing" : changelog.reason,
|
|
733
|
+
expected: "A package-local Keep a Changelog document",
|
|
734
|
+
nextAction: "Create the reviewed package changelog.",
|
|
735
|
+
}),
|
|
736
|
+
);
|
|
737
|
+
return {};
|
|
738
|
+
}
|
|
739
|
+
if (options.version === undefined) return {};
|
|
740
|
+
const inspected = inspectPackageReleaseChangelog(
|
|
741
|
+
changelog.source,
|
|
742
|
+
options.version,
|
|
743
|
+
);
|
|
744
|
+
if (inspected.blockers.length > 0) {
|
|
745
|
+
blockers.push(
|
|
746
|
+
...inspected.blockers.map((item) => ({
|
|
747
|
+
...item,
|
|
748
|
+
owner: { path: changelogPath },
|
|
749
|
+
})),
|
|
750
|
+
);
|
|
751
|
+
return {};
|
|
752
|
+
}
|
|
753
|
+
return {
|
|
754
|
+
...(inspected.releaseDate === undefined
|
|
755
|
+
? {}
|
|
756
|
+
: { releaseDate: inspected.releaseDate }),
|
|
757
|
+
...(inspected.releaseNotes === undefined
|
|
758
|
+
? {}
|
|
759
|
+
: { releaseNotes: inspected.releaseNotes }),
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
async function addMetadataBlockers(options: {
|
|
764
|
+
readonly repositoryRoot: string;
|
|
765
|
+
readonly packagePath: string;
|
|
766
|
+
readonly packageName?: string;
|
|
767
|
+
readonly rootManifest?: JsonObject;
|
|
768
|
+
readonly blockers: NpmPublicationReadinessBlocker[];
|
|
769
|
+
}): Promise<void> {
|
|
770
|
+
const metadataRoot = path.join(options.repositoryRoot, ".template");
|
|
771
|
+
let metadataRootState: "absent" | "directory" | "invalid";
|
|
772
|
+
let metadataRootReason: string | undefined;
|
|
773
|
+
try {
|
|
774
|
+
metadataRootState = (await stat(metadataRoot)).isDirectory()
|
|
775
|
+
? "directory"
|
|
776
|
+
: "invalid";
|
|
777
|
+
if (metadataRootState === "invalid") {
|
|
778
|
+
metadataRootReason = "Local Template Metadata root is not a directory";
|
|
779
|
+
}
|
|
780
|
+
} catch (error) {
|
|
781
|
+
if (isObject(error) && error.code === "ENOENT") {
|
|
782
|
+
metadataRootState = "absent";
|
|
783
|
+
} else {
|
|
784
|
+
metadataRootState = "invalid";
|
|
785
|
+
metadataRootReason =
|
|
786
|
+
error instanceof Error ? error.message : String(error);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
const blueprintPath = ".template/blueprint.json";
|
|
790
|
+
const generationPath = ".template/generation.json";
|
|
791
|
+
const [blueprintFact, generationFact] = await Promise.all([
|
|
792
|
+
textFact(path.join(options.repositoryRoot, blueprintPath)),
|
|
793
|
+
textFact(path.join(options.repositoryRoot, generationPath)),
|
|
794
|
+
]);
|
|
795
|
+
if (
|
|
796
|
+
metadataRootState === "absent" &&
|
|
797
|
+
blueprintFact.kind === "missing" &&
|
|
798
|
+
generationFact.kind === "missing"
|
|
799
|
+
) {
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
if (metadataRootState === "invalid") {
|
|
803
|
+
options.blockers.push(
|
|
804
|
+
blocker({
|
|
805
|
+
code: "local-template-metadata-invalid",
|
|
806
|
+
path: ".template",
|
|
807
|
+
observed:
|
|
808
|
+
metadataRootReason ?? "Unreadable Local Template Metadata root",
|
|
809
|
+
expected: "A readable Local Template Metadata directory",
|
|
810
|
+
nextAction:
|
|
811
|
+
"Restore the .template directory before checking publication readiness.",
|
|
812
|
+
}),
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
for (const [metadataPath, fact] of [
|
|
816
|
+
[blueprintPath, blueprintFact],
|
|
817
|
+
[generationPath, generationFact],
|
|
818
|
+
] as const) {
|
|
819
|
+
if (fact.kind === "missing") {
|
|
820
|
+
options.blockers.push(
|
|
821
|
+
blocker({
|
|
822
|
+
code: "local-template-metadata-missing",
|
|
823
|
+
path: metadataPath,
|
|
824
|
+
observed: "missing",
|
|
825
|
+
expected: "Complete Local Template Metadata when .template exists",
|
|
826
|
+
nextAction: "Restore both supported Local Template Metadata files.",
|
|
827
|
+
}),
|
|
828
|
+
);
|
|
829
|
+
} else if (fact.kind === "unreadable") {
|
|
830
|
+
options.blockers.push(
|
|
831
|
+
blocker({
|
|
832
|
+
code: "local-template-metadata-invalid",
|
|
833
|
+
path: metadataPath,
|
|
834
|
+
observed: fact.reason,
|
|
835
|
+
expected: "Readable Local Template Metadata JSON",
|
|
836
|
+
nextAction: "Restore the supported Local Template Metadata file.",
|
|
837
|
+
}),
|
|
838
|
+
);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
const blueprint = parseJsonFact(blueprintFact);
|
|
842
|
+
const generation = parseJsonFact(generationFact);
|
|
843
|
+
const blueprintPackages =
|
|
844
|
+
blueprint?.schemaVersion === 3 && Array.isArray(blueprint.packages)
|
|
845
|
+
? blueprint.packages
|
|
846
|
+
: undefined;
|
|
847
|
+
if (blueprintFact.kind === "present" && blueprintPackages === undefined) {
|
|
848
|
+
options.blockers.push(
|
|
849
|
+
blocker({
|
|
850
|
+
code: "local-template-metadata-invalid",
|
|
851
|
+
path: blueprintPath,
|
|
852
|
+
observed: "Unreadable Blueprint publication facts",
|
|
853
|
+
expected: "Blueprint schema version 3 with package facts",
|
|
854
|
+
nextAction: "Restore the canonical Blueprint.",
|
|
855
|
+
}),
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
const generationPackages =
|
|
859
|
+
generation?.schemaVersion === 2 &&
|
|
860
|
+
typeof generation.repositoryName === "string" &&
|
|
861
|
+
generation.repositoryName.trim().length > 0 &&
|
|
862
|
+
typeof generation.defaultPackageScope === "string" &&
|
|
863
|
+
generation.defaultPackageScope.trim().length > 0 &&
|
|
864
|
+
Array.isArray(generation.packages)
|
|
865
|
+
? generation.packages
|
|
866
|
+
: undefined;
|
|
867
|
+
if (generationFact.kind === "present" && generationPackages === undefined) {
|
|
868
|
+
options.blockers.push(
|
|
869
|
+
blocker({
|
|
870
|
+
code: "local-template-metadata-invalid",
|
|
871
|
+
path: generationPath,
|
|
872
|
+
observed: "Unreadable Generation Record publication facts",
|
|
873
|
+
expected:
|
|
874
|
+
"Generation Record schema version 2 with repository, default scope, and package facts",
|
|
875
|
+
nextAction: "Restore the canonical Generation Record.",
|
|
876
|
+
}),
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
if (
|
|
880
|
+
blueprintPackages === undefined ||
|
|
881
|
+
generationPackages === undefined ||
|
|
882
|
+
generation === undefined
|
|
883
|
+
) {
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
const definitions = blueprintPackages.filter(
|
|
887
|
+
(item): item is JsonObject =>
|
|
888
|
+
isObject(item) && item.path === options.packagePath,
|
|
889
|
+
);
|
|
890
|
+
const records = generationPackages.filter(
|
|
891
|
+
(item): item is JsonObject =>
|
|
892
|
+
isObject(item) && item.path === options.packagePath,
|
|
893
|
+
);
|
|
894
|
+
const definition = definitions[0];
|
|
895
|
+
const record = records[0];
|
|
896
|
+
const targetId =
|
|
897
|
+
definition !== undefined &&
|
|
898
|
+
typeof definition.packageDefinitionId === "string"
|
|
899
|
+
? definition.packageDefinitionId
|
|
900
|
+
: undefined;
|
|
901
|
+
const definitionsWithTargetId =
|
|
902
|
+
targetId === undefined
|
|
903
|
+
? []
|
|
904
|
+
: blueprintPackages.filter(
|
|
905
|
+
(item): item is JsonObject =>
|
|
906
|
+
isObject(item) && item.packageDefinitionId === targetId,
|
|
907
|
+
);
|
|
908
|
+
const recordsWithTargetId =
|
|
909
|
+
targetId === undefined
|
|
910
|
+
? []
|
|
911
|
+
: generationPackages.filter(
|
|
912
|
+
(item): item is JsonObject =>
|
|
913
|
+
isObject(item) && item.packageDefinitionId === targetId,
|
|
914
|
+
);
|
|
915
|
+
const candidateProvenance = generationPackages.filter(
|
|
916
|
+
(item): item is JsonObject =>
|
|
917
|
+
isObject(item) &&
|
|
918
|
+
item.definitionName === "ts-cli" &&
|
|
919
|
+
item.contributionIdentity === "cli-publication-candidate",
|
|
920
|
+
);
|
|
921
|
+
const initCandidates = candidateProvenance.filter(
|
|
922
|
+
(item) => item.planningContribution === "planInitialization",
|
|
923
|
+
);
|
|
924
|
+
const definitionMatches =
|
|
925
|
+
definitions.length === 1 &&
|
|
926
|
+
definition !== undefined &&
|
|
927
|
+
targetId !== undefined &&
|
|
928
|
+
targetId.length > 0 &&
|
|
929
|
+
definitionsWithTargetId.length === 1 &&
|
|
930
|
+
definition.name === options.packageName &&
|
|
931
|
+
definition.role === "cli-tool";
|
|
932
|
+
const recordMatches =
|
|
933
|
+
records.length === 1 &&
|
|
934
|
+
record !== undefined &&
|
|
935
|
+
definition !== undefined &&
|
|
936
|
+
recordsWithTargetId.length === 1 &&
|
|
937
|
+
record.packageDefinitionId === definition.packageDefinitionId &&
|
|
938
|
+
candidateProvenance.length === 1 &&
|
|
939
|
+
initCandidates.length === 1 &&
|
|
940
|
+
initCandidates[0] === record;
|
|
941
|
+
const repositoryMatches =
|
|
942
|
+
generation.repositoryName === options.rootManifest?.name;
|
|
943
|
+
if (!definitionMatches || !recordMatches || !repositoryMatches) {
|
|
944
|
+
options.blockers.push(
|
|
945
|
+
blocker({
|
|
946
|
+
code: "local-template-metadata-mismatch",
|
|
947
|
+
path: !definitionMatches ? blueprintPath : generationPath,
|
|
948
|
+
observed: display({
|
|
949
|
+
definitions,
|
|
950
|
+
records,
|
|
951
|
+
definitionsWithTargetId,
|
|
952
|
+
recordsWithTargetId,
|
|
953
|
+
candidateProvenance,
|
|
954
|
+
repositoryName: generation.repositoryName,
|
|
955
|
+
}),
|
|
956
|
+
expected:
|
|
957
|
+
"Exactly one matching public CLI target with stable ID and candidate provenance",
|
|
958
|
+
nextAction:
|
|
959
|
+
"Restore the matching Blueprint name and unchanged candidate planning facts.",
|
|
960
|
+
}),
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
export async function inspectNpmPublicationReadiness(options: {
|
|
966
|
+
readonly repositoryRoot: string;
|
|
967
|
+
readonly packagePath: string;
|
|
968
|
+
}): Promise<NpmPublicationReadiness> {
|
|
969
|
+
const repositoryRoot = path.resolve(options.repositoryRoot);
|
|
970
|
+
const packagePath = options.packagePath.split(path.sep).join("/");
|
|
971
|
+
const targetRoot = path.resolve(repositoryRoot, packagePath);
|
|
972
|
+
const safePackagePath =
|
|
973
|
+
/^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/u.test(packagePath) &&
|
|
974
|
+
targetRoot.startsWith(`${repositoryRoot}${path.sep}`);
|
|
975
|
+
const manifestPath = `${packagePath}/package.json`;
|
|
976
|
+
const blockers: NpmPublicationReadinessBlocker[] = [];
|
|
977
|
+
if (!safePackagePath) {
|
|
978
|
+
blockers.push(
|
|
979
|
+
blocker({
|
|
980
|
+
code: "manifest-contract",
|
|
981
|
+
path: manifestPath,
|
|
982
|
+
observed: options.packagePath,
|
|
983
|
+
expected: "A safe two-segment Package Path inside the repository",
|
|
984
|
+
nextAction:
|
|
985
|
+
"Use the candidate Package Path projected by the repository.",
|
|
986
|
+
}),
|
|
987
|
+
);
|
|
988
|
+
return {
|
|
989
|
+
kind: "blocked",
|
|
990
|
+
mode: "public-intent",
|
|
991
|
+
target: { packagePath },
|
|
992
|
+
blockers: blockers as [
|
|
993
|
+
NpmPublicationReadinessBlocker,
|
|
994
|
+
...NpmPublicationReadinessBlocker[],
|
|
995
|
+
],
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const [manifestFact, rootManifestFact] = await Promise.all([
|
|
1000
|
+
textFact(path.join(repositoryRoot, manifestPath)),
|
|
1001
|
+
textFact(path.join(repositoryRoot, "package.json")),
|
|
1002
|
+
]);
|
|
1003
|
+
const manifest = parseJsonFact(manifestFact);
|
|
1004
|
+
const rootManifest = parseJsonFact(rootManifestFact);
|
|
1005
|
+
if (manifest === undefined) {
|
|
1006
|
+
blockers.push(
|
|
1007
|
+
blocker({
|
|
1008
|
+
code: "manifest-contract",
|
|
1009
|
+
path: manifestPath,
|
|
1010
|
+
observed:
|
|
1011
|
+
manifestFact.kind === "missing"
|
|
1012
|
+
? "missing"
|
|
1013
|
+
: manifestFact.kind === "unreadable"
|
|
1014
|
+
? manifestFact.reason
|
|
1015
|
+
: "Invalid JSON object",
|
|
1016
|
+
expected: "A readable public CLI package manifest",
|
|
1017
|
+
nextAction: "Restore the candidate package manifest.",
|
|
1018
|
+
}),
|
|
1019
|
+
);
|
|
1020
|
+
return {
|
|
1021
|
+
kind: "blocked",
|
|
1022
|
+
mode: "public-intent",
|
|
1023
|
+
target: { packagePath },
|
|
1024
|
+
blockers: blockers as [
|
|
1025
|
+
NpmPublicationReadinessBlocker,
|
|
1026
|
+
...NpmPublicationReadinessBlocker[],
|
|
1027
|
+
],
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const mode =
|
|
1032
|
+
manifest.private === true && manifest.version === undefined
|
|
1033
|
+
? "safe-unconfigured"
|
|
1034
|
+
: "public-intent";
|
|
1035
|
+
const identity = addManifestBlockers({
|
|
1036
|
+
manifest,
|
|
1037
|
+
manifestPath,
|
|
1038
|
+
packagePath,
|
|
1039
|
+
rootManifest,
|
|
1040
|
+
blockers,
|
|
1041
|
+
});
|
|
1042
|
+
const documents = await addDocumentBlockers({
|
|
1043
|
+
repositoryRoot,
|
|
1044
|
+
packagePath,
|
|
1045
|
+
...(identity.version === undefined ? {} : { version: identity.version }),
|
|
1046
|
+
...(identity.license === undefined ? {} : { license: identity.license }),
|
|
1047
|
+
blockers,
|
|
1048
|
+
});
|
|
1049
|
+
await addMetadataBlockers({
|
|
1050
|
+
repositoryRoot,
|
|
1051
|
+
packagePath,
|
|
1052
|
+
...(rootManifest === undefined ? {} : { rootManifest }),
|
|
1053
|
+
...(identity.packageName === undefined
|
|
1054
|
+
? {}
|
|
1055
|
+
: { packageName: identity.packageName }),
|
|
1056
|
+
blockers,
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
const workspaceFacts = await actualWorkspaceManifests(repositoryRoot);
|
|
1060
|
+
for (const failure of workspaceFacts.failures) {
|
|
1061
|
+
blockers.push(
|
|
1062
|
+
blocker({
|
|
1063
|
+
code: "manifest-contract",
|
|
1064
|
+
path: failure.path,
|
|
1065
|
+
observed: failure.reason,
|
|
1066
|
+
expected: "Readable actual workspace package manifests",
|
|
1067
|
+
nextAction: "Restore the generated workspace membership and manifests.",
|
|
1068
|
+
}),
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
for (const dependencyName of runtimeDependencyNames(manifest)) {
|
|
1072
|
+
const dependencyField =
|
|
1073
|
+
isObject(manifest.dependencies) &&
|
|
1074
|
+
Object.hasOwn(manifest.dependencies, dependencyName)
|
|
1075
|
+
? "dependencies"
|
|
1076
|
+
: "optionalDependencies";
|
|
1077
|
+
for (const dependency of workspaceFacts.manifests) {
|
|
1078
|
+
if (
|
|
1079
|
+
dependency.manifest.name !== dependencyName ||
|
|
1080
|
+
dependency.manifest.private !== true
|
|
1081
|
+
) {
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
blockers.push(
|
|
1085
|
+
blocker({
|
|
1086
|
+
code: "private-runtime-dependency",
|
|
1087
|
+
path: manifestPath,
|
|
1088
|
+
pointer: `/${dependencyField}/${jsonPointerSegment(dependencyName)}`,
|
|
1089
|
+
observed: `${dependencyName} resolves to private workspace package ${dependency.path}`,
|
|
1090
|
+
expected: "Every direct public runtime dependency to be installable",
|
|
1091
|
+
nextAction:
|
|
1092
|
+
"Publish the dependency separately or remove the private runtime edge.",
|
|
1093
|
+
}),
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
blockers.sort(compareBlockers);
|
|
1099
|
+
if (blockers.length > 0) {
|
|
1100
|
+
return {
|
|
1101
|
+
kind: "blocked",
|
|
1102
|
+
mode,
|
|
1103
|
+
target: { packagePath },
|
|
1104
|
+
blockers: blockers as [
|
|
1105
|
+
NpmPublicationReadinessBlocker,
|
|
1106
|
+
...NpmPublicationReadinessBlocker[],
|
|
1107
|
+
],
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
return {
|
|
1112
|
+
kind: "ready",
|
|
1113
|
+
target: { packagePath },
|
|
1114
|
+
blockers: [],
|
|
1115
|
+
publication: {
|
|
1116
|
+
packagePath,
|
|
1117
|
+
packageName: identity.packageName!,
|
|
1118
|
+
commandName: identity.commandName!,
|
|
1119
|
+
version: identity.version!,
|
|
1120
|
+
repository: identity.repository!.canonicalUrl,
|
|
1121
|
+
releaseDate: documents.releaseDate!,
|
|
1122
|
+
releaseNotes: documents.releaseNotes!,
|
|
1123
|
+
},
|
|
1124
|
+
};
|
|
1125
|
+
}
|