@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.
Files changed (98) hide show
  1. package/dist/application.d.ts +2 -1
  2. package/dist/application.js +64 -75
  3. package/dist/main.js +6 -2
  4. package/node_modules/@ykdz/template-builtin-presets/dist/src/foundation.d.ts +50 -36
  5. package/node_modules/@ykdz/template-builtin-presets/dist/src/foundation.js +970 -365
  6. package/node_modules/@ykdz/template-builtin-presets/dist/src/generation-record.d.ts +24 -0
  7. package/node_modules/@ykdz/template-builtin-presets/dist/src/generation-record.js +154 -0
  8. package/node_modules/@ykdz/template-builtin-presets/dist/src/index.d.ts +2 -2
  9. package/node_modules/@ykdz/template-builtin-presets/dist/src/index.js +1 -1
  10. package/node_modules/@ykdz/template-builtin-presets/dist/src/rust-bin/definition.d.ts +31 -2
  11. package/node_modules/@ykdz/template-builtin-presets/dist/src/rust-bin/definition.js +29 -33
  12. package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/development-container.d.ts +1 -0
  13. package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/development-container.js +17 -0
  14. package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/typescript.d.ts +4 -3
  15. package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/typescript.js +9 -5
  16. package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/vue.d.ts +1 -1
  17. package/node_modules/@ykdz/template-builtin-presets/dist/src/shared/vue.js +1 -2
  18. package/node_modules/@ykdz/template-builtin-presets/dist/src/ts-cli/definition.d.ts +31 -2
  19. package/node_modules/@ykdz/template-builtin-presets/dist/src/ts-cli/definition.js +79 -58
  20. package/node_modules/@ykdz/template-builtin-presets/dist/src/ts-lib/definition.d.ts +31 -2
  21. package/node_modules/@ykdz/template-builtin-presets/dist/src/ts-lib/definition.js +31 -23
  22. package/node_modules/@ykdz/template-builtin-presets/dist/src/vike-app/definition.d.ts +22 -2
  23. package/node_modules/@ykdz/template-builtin-presets/dist/src/vike-app/definition.js +61 -35
  24. package/node_modules/@ykdz/template-builtin-presets/dist/src/vue-app/definition.d.ts +26 -2
  25. package/node_modules/@ykdz/template-builtin-presets/dist/src/vue-app/definition.js +19 -8
  26. package/node_modules/@ykdz/template-builtin-presets/dist/src/vue-hono-app/definition.d.ts +22 -2
  27. package/node_modules/@ykdz/template-builtin-presets/dist/src/vue-hono-app/definition.js +31 -21
  28. package/node_modules/@ykdz/template-builtin-presets/package.json +12 -11
  29. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/.github/workflows/check-deployment-diagnostics.yml +1 -1
  30. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/.github/workflows/check-deployment.yml +1 -1
  31. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/.pnpmfile.mjs +19 -0
  32. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/gitignore +1 -0
  33. package/node_modules/@ykdz/template-builtin-presets/templates/foundation/tsconfig.packing-hook.json +8 -0
  34. package/node_modules/@ykdz/template-builtin-presets/templates/shared/devcontainer/github-cli.Dockerfile +9 -0
  35. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/CHANGELOG.md.template +17 -0
  36. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/RELEASING.md +17 -0
  37. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/artifact.ts +1271 -0
  38. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/changelog.ts +283 -0
  39. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/check-artifact.ts +99 -0
  40. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/check-readiness.ts +66 -0
  41. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/cli-command-identity.ts +4 -0
  42. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/handoff.ts +450 -0
  43. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/publish.ts +483 -0
  44. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/readiness.ts +1125 -0
  45. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/release.ts +819 -0
  46. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/release.yml +119 -0
  47. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/root-tsconfig.json +13 -0
  48. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/tsconfig.json +4 -0
  49. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication/turbo.json +6 -0
  50. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication-setup/README.md +48 -0
  51. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication-setup/assets/CHANGELOG.md.template +17 -0
  52. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication-setup/assets/LICENSE-APACHE-2.0.txt +202 -0
  53. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication-setup/assets/LICENSE-MIT.txt +21 -0
  54. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication-setup/assets/README.md.template +5 -0
  55. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication-setup/bridge.ts +3336 -0
  56. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/publication-setup/setup.sh +221 -0
  57. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/src/cli-command-identity.ts +133 -0
  58. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/src/cli.ts +4 -3
  59. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/src/main.ts +5 -4
  60. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/test/integration/command.test.ts +14 -13
  61. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/test/unit/cli-command-identity.test.ts +76 -0
  62. package/node_modules/@ykdz/template-builtin-presets/templates/ts-cli/tsconfig.build.json +2 -0
  63. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db/src/readiness.ts +1 -1
  64. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db/test/global-setup.ts +21 -0
  65. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db/tsconfig.json +6 -1
  66. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db/vitest.config.ts +7 -0
  67. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db-migrations/drizzle.config.ts +5 -1
  68. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/playwright.config.ts +102 -22
  69. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/test/playwright-teardown.ts +12 -0
  70. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/vite.config.ts +39 -1
  71. package/node_modules/@ykdz/template-builtin-presets/templates/vue-app/playwright.config.ts +48 -7
  72. package/node_modules/@ykdz/template-builtin-presets/templates/vue-hono-app/web/playwright.config.ts +59 -8
  73. package/node_modules/@ykdz/template-core/dist/module-graph.d.ts +15 -53
  74. package/node_modules/@ykdz/template-core/dist/module-graph.js +134 -101
  75. package/node_modules/@ykdz/template-core/dist/package-contribution.d.ts +13 -1
  76. package/node_modules/@ykdz/template-core/dist/package-contribution.js +92 -0
  77. package/node_modules/@ykdz/template-core/dist/pnpm-workspace.yaml +14 -10
  78. package/node_modules/@ykdz/template-core/dist/preset-definition.d.ts +71 -13
  79. package/node_modules/@ykdz/template-core/dist/preset-definition.js +16 -1
  80. package/node_modules/@ykdz/template-core/dist/project-blueprint.d.ts +56 -0
  81. package/node_modules/@ykdz/template-core/dist/project-blueprint.js +271 -0
  82. package/node_modules/@ykdz/template-core/dist/project-linking-v2.d.ts +2 -2
  83. package/node_modules/@ykdz/template-core/dist/project-linking-v2.js +2 -2
  84. package/node_modules/@ykdz/template-core/dist/project-projection.d.ts +2 -1
  85. package/node_modules/@ykdz/template-core/dist/project-projection.js +107 -20
  86. package/node_modules/@ykdz/template-core/dist/renderer.d.ts +0 -2
  87. package/node_modules/@ykdz/template-core/dist/renderer.js +17 -38
  88. package/node_modules/@ykdz/template-core/dist/template-boundary-check.d.ts +2 -2
  89. package/node_modules/@ykdz/template-core/dist/template-boundary-check.js +6 -0
  90. package/node_modules/@ykdz/template-core/dist/toolchain-resolution.d.ts +2 -0
  91. package/node_modules/@ykdz/template-core/dist/toolchain-resolution.js +9 -3
  92. package/package.json +3 -3
  93. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/db-migrations/scripts/prepare-database.ts +0 -45
  94. package/node_modules/@ykdz/template-builtin-presets/templates/vike-app/web/scripts/run-playwright.ts +0 -151
  95. package/node_modules/@ykdz/template-builtin-presets/templates/vue-app/scripts/run-playwright.ts +0 -62
  96. package/node_modules/@ykdz/template-builtin-presets/templates/vue-hono-app/web/scripts/run-playwright.ts +0 -103
  97. package/node_modules/@ykdz/template-core/dist/project-blueprint-v2.d.ts +0 -33
  98. 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
+ }