@ykdz/template 0.0.5 → 0.0.6

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.
@@ -0,0 +1,940 @@
1
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync, } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import * as v from "valibot";
5
+ import { loadTemplateDependencyCatalog, } from "./dependency-catalog.js";
6
+ import { PackageAdditionSupport } from "./package-addition-support.js";
7
+ import { normalizePresetProjectionDeclaration, projectionCapabilityIssues, } from "./projection-capabilities.js";
8
+ import { packageTemplateRoot } from "./runtime-paths.js";
9
+ const featureNames = [
10
+ "pnpm-catalog",
11
+ "oxc-format-lint",
12
+ "strict-typescript",
13
+ "root-check",
14
+ "fix-command",
15
+ "devcontainer",
16
+ "github-actions",
17
+ "dependabot",
18
+ "rustfmt-clippy",
19
+ "cargo-test",
20
+ "native-binary-release",
21
+ ];
22
+ const packageLeafNamePattern = "^[a-z0-9][a-z0-9-]*$";
23
+ const packageLeafNameRegExp = new RegExp(packageLeafNamePattern);
24
+ const requiredFixtureMatrixCheckRequirements = [
25
+ "machine-verifiable-next-steps",
26
+ "root-check-ci",
27
+ ];
28
+ const requiredFixtureMatrixEnvironmentPreparation = [
29
+ "playwright-browser-assets",
30
+ ];
31
+ export const presetSourceManifestJsonSchema = {
32
+ $schema: "https://json-schema.org/draft/2020-12/schema",
33
+ $id: "https://ykdz.dev/schemas/template/preset-source-manifest.schema.json",
34
+ title: "Preset Source Manifest",
35
+ type: "object",
36
+ additionalProperties: false,
37
+ required: ["schemaVersion", "name", "presets"],
38
+ properties: {
39
+ schemaVersion: { const: 1 },
40
+ name: { type: "string", minLength: 1 },
41
+ sharedResources: {
42
+ type: "array",
43
+ items: {
44
+ type: "object",
45
+ additionalProperties: false,
46
+ required: ["id", "path"],
47
+ properties: {
48
+ id: { type: "string", minLength: 1 },
49
+ path: { type: "string", minLength: 1 },
50
+ },
51
+ },
52
+ },
53
+ fixtureMatrix: {
54
+ type: "object",
55
+ additionalProperties: false,
56
+ required: [
57
+ "initSupport",
58
+ "packageAdditionSupport",
59
+ "supportedCombinations",
60
+ "semanticSkips",
61
+ "checkRequirements",
62
+ "environmentPreparation",
63
+ ],
64
+ properties: {
65
+ initSupport: {
66
+ type: "array",
67
+ items: {
68
+ type: "object",
69
+ additionalProperties: false,
70
+ required: ["preset"],
71
+ properties: {
72
+ preset: { type: "string", minLength: 1 },
73
+ },
74
+ },
75
+ },
76
+ packageAdditionSupport: {
77
+ type: "array",
78
+ items: {
79
+ type: "object",
80
+ additionalProperties: false,
81
+ required: ["preset", "packageLeafName"],
82
+ properties: {
83
+ preset: { type: "string", minLength: 1 },
84
+ packageLeafName: {
85
+ type: "string",
86
+ minLength: 1,
87
+ pattern: packageLeafNamePattern,
88
+ },
89
+ },
90
+ },
91
+ },
92
+ supportedCombinations: {
93
+ type: "array",
94
+ items: {
95
+ type: "object",
96
+ additionalProperties: false,
97
+ required: ["basePreset", "addedPreset"],
98
+ properties: {
99
+ basePreset: { type: "string", minLength: 1 },
100
+ addedPreset: { type: "string", minLength: 1 },
101
+ linkFrom: {
102
+ type: "array",
103
+ items: { type: "string", minLength: 1 },
104
+ },
105
+ },
106
+ },
107
+ },
108
+ semanticSkips: {
109
+ type: "array",
110
+ items: {
111
+ type: "object",
112
+ additionalProperties: false,
113
+ required: ["basePreset", "addedPreset", "reason"],
114
+ properties: {
115
+ basePreset: { type: "string", minLength: 1 },
116
+ addedPreset: { type: "string", minLength: 1 },
117
+ reason: { type: "string", minLength: 1 },
118
+ },
119
+ },
120
+ },
121
+ checkRequirements: {
122
+ type: "array",
123
+ items: {
124
+ enum: [...requiredFixtureMatrixCheckRequirements],
125
+ },
126
+ uniqueItems: true,
127
+ },
128
+ environmentPreparation: {
129
+ type: "array",
130
+ items: {
131
+ enum: [...requiredFixtureMatrixEnvironmentPreparation],
132
+ },
133
+ uniqueItems: true,
134
+ },
135
+ },
136
+ },
137
+ presets: {
138
+ type: "array",
139
+ minItems: 1,
140
+ items: {
141
+ type: "object",
142
+ additionalProperties: false,
143
+ required: [
144
+ "name",
145
+ "title",
146
+ "description",
147
+ "generation",
148
+ "supportedPackageManagers",
149
+ "supportedProjectKinds",
150
+ "packageAdditionSupport",
151
+ "features",
152
+ ],
153
+ properties: {
154
+ name: { type: "string", minLength: 1 },
155
+ title: { type: "string", minLength: 1 },
156
+ description: { type: "string", minLength: 1 },
157
+ generation: { enum: ["supported", "future"] },
158
+ supportedPackageManagers: {
159
+ type: "array",
160
+ items: { enum: ["pnpm"] },
161
+ uniqueItems: true,
162
+ },
163
+ supportedProjectKinds: {
164
+ type: "array",
165
+ minItems: 1,
166
+ items: { enum: ["multi-package"] },
167
+ uniqueItems: true,
168
+ },
169
+ packageAdditionSupport: {
170
+ enum: [
171
+ PackageAdditionSupport.Supported,
172
+ PackageAdditionSupport.Unsupported,
173
+ ],
174
+ },
175
+ features: {
176
+ type: "array",
177
+ items: { enum: featureNames },
178
+ uniqueItems: true,
179
+ },
180
+ dependencyCatalog: {
181
+ type: "array",
182
+ items: { type: "string", minLength: 1 },
183
+ uniqueItems: true,
184
+ },
185
+ projection: {
186
+ type: "object",
187
+ additionalProperties: false,
188
+ required: ["capabilities"],
189
+ properties: {
190
+ capabilities: {
191
+ type: "array",
192
+ items: {
193
+ oneOf: [
194
+ {
195
+ type: "object",
196
+ additionalProperties: false,
197
+ required: [
198
+ "kind",
199
+ "workspacePackageGlob",
200
+ "packageRole",
201
+ "packageSourcePreset",
202
+ "sourceFiles",
203
+ ],
204
+ properties: {
205
+ kind: { const: "workspace-library-package" },
206
+ workspacePackageGlob: { const: "packages/*" },
207
+ packageRole: { const: "shared-library" },
208
+ packageSourcePreset: { const: "ts-lib" },
209
+ sourceFiles: {
210
+ type: "array",
211
+ minItems: 1,
212
+ items: { type: "string", minLength: 1 },
213
+ },
214
+ },
215
+ },
216
+ {
217
+ type: "object",
218
+ additionalProperties: false,
219
+ required: ["kind", "workspacePackageGlob", "packages"],
220
+ properties: {
221
+ kind: { const: "workspace-node-packages" },
222
+ workspacePackageGlob: { const: "apps/*" },
223
+ packages: {
224
+ type: "array",
225
+ minItems: 1,
226
+ items: {
227
+ type: "object",
228
+ additionalProperties: false,
229
+ required: ["kind", "path", "sourceFiles"],
230
+ properties: {
231
+ kind: { enum: ["hono-api", "vue-app"] },
232
+ path: { enum: ["apps/api", "apps/web"] },
233
+ sourceFiles: {
234
+ type: "array",
235
+ minItems: 1,
236
+ items: { type: "string", minLength: 1 },
237
+ },
238
+ },
239
+ },
240
+ },
241
+ packageLinks: {
242
+ type: "array",
243
+ items: {
244
+ type: "object",
245
+ additionalProperties: false,
246
+ required: [
247
+ "consumerPackagePath",
248
+ "providerPackagePath",
249
+ ],
250
+ properties: {
251
+ consumerPackagePath: { const: "apps/web" },
252
+ providerPackagePath: { const: "apps/api" },
253
+ },
254
+ },
255
+ },
256
+ },
257
+ },
258
+ {
259
+ type: "object",
260
+ additionalProperties: false,
261
+ required: ["kind", "workspacePackageGlob", "sourceFiles"],
262
+ properties: {
263
+ kind: { const: "rust-binary-workspace" },
264
+ workspacePackageGlob: { const: "packages/*" },
265
+ sourceFiles: {
266
+ type: "array",
267
+ minItems: 1,
268
+ items: { type: "string", minLength: 1 },
269
+ },
270
+ },
271
+ },
272
+ {
273
+ type: "object",
274
+ additionalProperties: false,
275
+ required: ["kind"],
276
+ properties: {
277
+ kind: { const: "strict-typescript-root" },
278
+ },
279
+ },
280
+ {
281
+ type: "object",
282
+ additionalProperties: false,
283
+ required: ["kind"],
284
+ properties: {
285
+ kind: { const: "oxc-format-lint" },
286
+ },
287
+ },
288
+ {
289
+ type: "object",
290
+ additionalProperties: false,
291
+ required: ["kind"],
292
+ properties: {
293
+ kind: { const: "node-pnpm-devcontainer" },
294
+ },
295
+ },
296
+ {
297
+ type: "object",
298
+ additionalProperties: false,
299
+ required: ["kind"],
300
+ properties: {
301
+ kind: { const: "github-maintenance" },
302
+ },
303
+ },
304
+ ],
305
+ },
306
+ },
307
+ },
308
+ },
309
+ source: {
310
+ type: "object",
311
+ additionalProperties: false,
312
+ properties: {
313
+ roots: {
314
+ type: "array",
315
+ items: { type: "string", minLength: 1 },
316
+ },
317
+ files: {
318
+ type: "array",
319
+ items: { type: "string", minLength: 1 },
320
+ },
321
+ sharedResources: {
322
+ type: "array",
323
+ items: { type: "string", minLength: 1 },
324
+ },
325
+ },
326
+ },
327
+ },
328
+ },
329
+ },
330
+ },
331
+ };
332
+ const nonEmptyString = v.pipe(v.string(), v.minLength(1));
333
+ const packageManagerSchema = v.picklist(["pnpm"]);
334
+ const projectKindSchema = v.picklist([
335
+ "single-package",
336
+ "multi-package",
337
+ ]);
338
+ const packageAdditionSupportSchema = v.picklist([
339
+ PackageAdditionSupport.Supported,
340
+ PackageAdditionSupport.Unsupported,
341
+ ]);
342
+ const featureNameSchema = v.picklist(featureNames);
343
+ const presetSourceManifestPresetSourceSchema = v.strictObject({
344
+ roots: v.optional(v.array(nonEmptyString), []),
345
+ files: v.optional(v.array(nonEmptyString), []),
346
+ sharedResources: v.optional(v.array(nonEmptyString), []),
347
+ });
348
+ const fixtureMatrixPresetSupportSchema = v.strictObject({
349
+ preset: nonEmptyString,
350
+ });
351
+ const fixtureMatrixPackageAdditionSupportSchema = v.strictObject({
352
+ preset: nonEmptyString,
353
+ packageLeafName: v.pipe(v.string(), v.regex(packageLeafNameRegExp, "Fixture Matrix Package Addition packageLeafName must be a lowercase package leaf name using letters, numbers, and hyphens")),
354
+ });
355
+ const fixtureMatrixCombinationSchema = v.strictObject({
356
+ basePreset: nonEmptyString,
357
+ addedPreset: nonEmptyString,
358
+ linkFrom: v.optional(v.array(nonEmptyString), []),
359
+ });
360
+ const fixtureMatrixSemanticSkipSchema = v.strictObject({
361
+ basePreset: nonEmptyString,
362
+ addedPreset: nonEmptyString,
363
+ reason: nonEmptyString,
364
+ });
365
+ const fixtureMatrixContractSchema = v.strictObject({
366
+ initSupport: v.array(fixtureMatrixPresetSupportSchema),
367
+ packageAdditionSupport: v.array(fixtureMatrixPackageAdditionSupportSchema),
368
+ supportedCombinations: v.array(fixtureMatrixCombinationSchema),
369
+ semanticSkips: v.array(fixtureMatrixSemanticSkipSchema),
370
+ checkRequirements: v.array(v.picklist(requiredFixtureMatrixCheckRequirements)),
371
+ environmentPreparation: v.array(v.picklist(requiredFixtureMatrixEnvironmentPreparation)),
372
+ });
373
+ const projectionCapabilityKindSchema = v.string();
374
+ const presetProjectionDeclarationSchema = v.strictObject({
375
+ capabilities: v.array(v.looseObject({
376
+ kind: projectionCapabilityKindSchema,
377
+ })),
378
+ });
379
+ const presetSourceManifestSchema = v.strictObject({
380
+ schemaVersion: v.literal(1),
381
+ name: nonEmptyString,
382
+ sharedResources: v.optional(v.array(v.strictObject({
383
+ id: nonEmptyString,
384
+ path: nonEmptyString,
385
+ })), []),
386
+ fixtureMatrix: v.optional(fixtureMatrixContractSchema),
387
+ presets: v.pipe(v.array(v.strictObject({
388
+ name: nonEmptyString,
389
+ title: nonEmptyString,
390
+ description: nonEmptyString,
391
+ generation: v.picklist(["supported", "future"]),
392
+ supportedPackageManagers: v.array(packageManagerSchema),
393
+ supportedProjectKinds: v.pipe(v.array(projectKindSchema), v.minLength(1)),
394
+ packageAdditionSupport: packageAdditionSupportSchema,
395
+ features: v.array(featureNameSchema),
396
+ dependencyCatalog: v.optional(v.array(nonEmptyString), []),
397
+ projection: v.optional(presetProjectionDeclarationSchema),
398
+ source: v.optional(presetSourceManifestPresetSourceSchema),
399
+ })), v.minLength(1)),
400
+ });
401
+ function formatIssuePath(issue) {
402
+ if (!issue.path || issue.path.length === 0) {
403
+ return "$";
404
+ }
405
+ return issue.path.reduce((path, item) => {
406
+ if (typeof item.key === "number") {
407
+ return `${path}[${item.key}]`;
408
+ }
409
+ return `${path}.${String(item.key)}`;
410
+ }, "$");
411
+ }
412
+ function shapeIssues(issues) {
413
+ return issues.map((issue) => {
414
+ const path = formatIssuePath(issue);
415
+ const missingPresetMetadata = path.match(/^\$\.presets\[\d+\]\.(.+)$/);
416
+ if (missingPresetMetadata && issue.message.includes("received undefined")) {
417
+ return {
418
+ path,
419
+ message: `Preset metadata is missing required field: ${missingPresetMetadata[1]}`,
420
+ };
421
+ }
422
+ if (issue.message.startsWith("Invalid key:") &&
423
+ /\.(body|content|text)$/.test(path)) {
424
+ const key = path.split(".").at(-1);
425
+ return {
426
+ path,
427
+ message: `Preset Source Manifests must reference Generated Repository file bodies by path, not inline ${key}`,
428
+ };
429
+ }
430
+ return {
431
+ path,
432
+ message: path.match(/^\$\.presets\[\d+\]\.packageAdditionSupport$/)
433
+ ? "Package Addition Support must be one of: supported, unsupported"
434
+ : issue.message,
435
+ };
436
+ });
437
+ }
438
+ function duplicatePresetNameIssues(presets) {
439
+ const seen = new Set();
440
+ const duplicates = new Set();
441
+ for (const preset of presets) {
442
+ if (seen.has(preset.name)) {
443
+ duplicates.add(preset.name);
444
+ }
445
+ seen.add(preset.name);
446
+ }
447
+ return [...duplicates].map((name) => ({
448
+ path: "$.presets.name",
449
+ message: `Duplicate Preset name: ${name}`,
450
+ }));
451
+ }
452
+ function duplicateValueIssues(values, path) {
453
+ const seen = new Set();
454
+ const duplicates = new Set();
455
+ for (const value of values) {
456
+ if (seen.has(value)) {
457
+ duplicates.add(value);
458
+ }
459
+ seen.add(value);
460
+ }
461
+ return [...duplicates].map((value) => ({
462
+ path,
463
+ message: `Duplicate value: ${value}`,
464
+ }));
465
+ }
466
+ function duplicatePresetMetadataArrayIssues(presets) {
467
+ return presets.flatMap((preset, index) => [
468
+ ...duplicateValueIssues(preset.supportedPackageManagers, `$.presets[${index}].supportedPackageManagers`),
469
+ ...duplicateValueIssues(preset.supportedProjectKinds, `$.presets[${index}].supportedProjectKinds`),
470
+ ...duplicateValueIssues(preset.features, `$.presets[${index}].features`),
471
+ ...duplicateValueIssues(preset.dependencyCatalog ?? [], `$.presets[${index}].dependencyCatalog`),
472
+ ]);
473
+ }
474
+ function fixtureCombinationKey(combination) {
475
+ return [
476
+ combination.basePreset,
477
+ combination.addedPreset,
478
+ ...(combination.linkFrom ?? []),
479
+ ].join("\0");
480
+ }
481
+ function fixtureCombinationPairKey(combination) {
482
+ return [combination.basePreset, combination.addedPreset].join("\0");
483
+ }
484
+ function fixtureMatrixContractIssues(manifest) {
485
+ if (!manifest.fixtureMatrix) {
486
+ return [];
487
+ }
488
+ const issues = [];
489
+ const presetsByName = new Map(manifest.presets.map((preset) => [preset.name, preset]));
490
+ const initPresets = new Set(manifest.fixtureMatrix.initSupport.map((support) => support.preset));
491
+ const addablePresets = new Set(manifest.fixtureMatrix.packageAdditionSupport.map((support) => support.preset));
492
+ const validInitPresets = new Set();
493
+ const validAddablePresets = new Set();
494
+ const supportedCombinationKeys = new Set();
495
+ const supportedCombinationPairKeys = new Set();
496
+ const semanticSkipKeys = new Set();
497
+ const semanticSkipPairKeys = new Set();
498
+ manifest.fixtureMatrix.initSupport.forEach((support, index) => {
499
+ const preset = presetsByName.get(support.preset);
500
+ if (!preset) {
501
+ issues.push({
502
+ path: `$.fixtureMatrix.initSupport[${index}].preset`,
503
+ message: `Fixture Matrix init support references unknown Preset: ${support.preset}`,
504
+ });
505
+ return;
506
+ }
507
+ if (preset.generation !== "supported") {
508
+ issues.push({
509
+ path: `$.fixtureMatrix.initSupport[${index}].preset`,
510
+ message: `Fixture Matrix init support must reference supported Presets: ${support.preset}`,
511
+ });
512
+ return;
513
+ }
514
+ validInitPresets.add(support.preset);
515
+ });
516
+ manifest.presets.forEach((preset, index) => {
517
+ if (preset.generation === "supported" &&
518
+ preset.packageAdditionSupport === PackageAdditionSupport.Supported &&
519
+ !addablePresets.has(preset.name)) {
520
+ issues.push({
521
+ path: `$.presets[${index}].packageAdditionSupport`,
522
+ message: `Fixture Matrix Package Addition support must declare supported Preset: ${preset.name}`,
523
+ });
524
+ }
525
+ });
526
+ manifest.fixtureMatrix.packageAdditionSupport.forEach((support, index) => {
527
+ const preset = presetsByName.get(support.preset);
528
+ if (!preset) {
529
+ issues.push({
530
+ path: `$.fixtureMatrix.packageAdditionSupport[${index}].preset`,
531
+ message: `Fixture Matrix Package Addition support references unknown Preset: ${support.preset}`,
532
+ });
533
+ return;
534
+ }
535
+ if (preset.packageAdditionSupport !== PackageAdditionSupport.Supported) {
536
+ issues.push({
537
+ path: `$.fixtureMatrix.packageAdditionSupport[${index}].preset`,
538
+ message: `Fixture Matrix Package Addition support must match Preset metadata: ${support.preset} is ${preset.packageAdditionSupport}`,
539
+ });
540
+ return;
541
+ }
542
+ validAddablePresets.add(support.preset);
543
+ });
544
+ manifest.fixtureMatrix.supportedCombinations.forEach((combination, index) => {
545
+ const key = fixtureCombinationKey(combination);
546
+ supportedCombinationPairKeys.add(fixtureCombinationPairKey(combination));
547
+ if (supportedCombinationKeys.has(key)) {
548
+ issues.push({
549
+ path: `$.fixtureMatrix.supportedCombinations[${index}]`,
550
+ message: `Duplicate Fixture Matrix supported combination: ${combination.basePreset} + ${combination.addedPreset}`,
551
+ });
552
+ }
553
+ supportedCombinationKeys.add(key);
554
+ if (!initPresets.has(combination.basePreset)) {
555
+ issues.push({
556
+ path: `$.fixtureMatrix.supportedCombinations[${index}].basePreset`,
557
+ message: `Fixture Matrix supported combination base Preset is not init-supported: ${combination.basePreset}`,
558
+ });
559
+ }
560
+ if (!addablePresets.has(combination.addedPreset)) {
561
+ issues.push({
562
+ path: `$.fixtureMatrix.supportedCombinations[${index}].addedPreset`,
563
+ message: `Fixture Matrix supported combination added Preset is not Package Addition-supported: ${combination.addedPreset}`,
564
+ });
565
+ }
566
+ });
567
+ manifest.fixtureMatrix.semanticSkips.forEach((skip, index) => {
568
+ const key = fixtureCombinationKey(skip);
569
+ semanticSkipPairKeys.add(fixtureCombinationPairKey(skip));
570
+ if (semanticSkipKeys.has(key)) {
571
+ issues.push({
572
+ path: `$.fixtureMatrix.semanticSkips[${index}]`,
573
+ message: `Duplicate Fixture Matrix semantic skip: ${skip.basePreset} + ${skip.addedPreset}`,
574
+ });
575
+ }
576
+ semanticSkipKeys.add(key);
577
+ if (!initPresets.has(skip.basePreset)) {
578
+ issues.push({
579
+ path: `$.fixtureMatrix.semanticSkips[${index}].basePreset`,
580
+ message: `Fixture Matrix semantic skip base Preset is not init-supported: ${skip.basePreset}`,
581
+ });
582
+ }
583
+ if (!presetsByName.has(skip.addedPreset)) {
584
+ issues.push({
585
+ path: `$.fixtureMatrix.semanticSkips[${index}].addedPreset`,
586
+ message: `Fixture Matrix semantic skip references unknown added Preset: ${skip.addedPreset}`,
587
+ });
588
+ }
589
+ });
590
+ for (const basePreset of validInitPresets) {
591
+ for (const addedPreset of validAddablePresets) {
592
+ const key = fixtureCombinationPairKey({ basePreset, addedPreset });
593
+ if (!supportedCombinationPairKeys.has(key) &&
594
+ !semanticSkipPairKeys.has(key)) {
595
+ issues.push({
596
+ path: "$.fixtureMatrix.supportedCombinations",
597
+ message: `Fixture Matrix must explicitly cover supported combination or semantic skip: ${basePreset} + ${addedPreset}`,
598
+ });
599
+ }
600
+ }
601
+ }
602
+ for (const checkRequirement of requiredFixtureMatrixCheckRequirements) {
603
+ if (!manifest.fixtureMatrix.checkRequirements.includes(checkRequirement)) {
604
+ issues.push({
605
+ path: "$.fixtureMatrix.checkRequirements",
606
+ message: `Fixture Matrix check requirements must include ${checkRequirement}`,
607
+ });
608
+ }
609
+ }
610
+ for (const environmentPreparation of requiredFixtureMatrixEnvironmentPreparation) {
611
+ if (!manifest.fixtureMatrix.environmentPreparation.includes(environmentPreparation)) {
612
+ issues.push({
613
+ path: "$.fixtureMatrix.environmentPreparation",
614
+ message: `Fixture Matrix environment preparation must include ${environmentPreparation}`,
615
+ });
616
+ }
617
+ }
618
+ for (const key of semanticSkipPairKeys) {
619
+ if (supportedCombinationPairKeys.has(key)) {
620
+ issues.push({
621
+ path: "$.fixtureMatrix.semanticSkips",
622
+ message: "Fixture Matrix semantic skips must not duplicate supported combinations",
623
+ });
624
+ }
625
+ }
626
+ return [
627
+ ...duplicateValueIssues(manifest.fixtureMatrix.initSupport.map((support) => support.preset), "$.fixtureMatrix.initSupport.preset"),
628
+ ...duplicateValueIssues(manifest.fixtureMatrix.packageAdditionSupport.map((support) => support.preset), "$.fixtureMatrix.packageAdditionSupport.preset"),
629
+ ...duplicateValueIssues(manifest.fixtureMatrix.checkRequirements, "$.fixtureMatrix.checkRequirements"),
630
+ ...duplicateValueIssues(manifest.fixtureMatrix.environmentPreparation, "$.fixtureMatrix.environmentPreparation"),
631
+ ...issues,
632
+ ];
633
+ }
634
+ function dependencyCatalogReferenceIssues(presets, dependencyCatalog) {
635
+ return presets.flatMap((preset, presetIndex) => (preset.dependencyCatalog ?? [])
636
+ .filter((dependency) => dependencyCatalog[dependency] === undefined)
637
+ .map((dependency) => ({
638
+ path: `$.presets[${presetIndex}].dependencyCatalog`,
639
+ message: `Preset ${preset.name} references missing Template Dependency Catalog entry: ${dependency}`,
640
+ })));
641
+ }
642
+ function unsupportedProjectShapeIssues(presets) {
643
+ return presets.flatMap((preset, index) => preset.supportedProjectKinds.includes("single-package")
644
+ ? [
645
+ {
646
+ path: `$.presets[${index}].supportedProjectKinds`,
647
+ message: "single-package Project Shape is unsupported in V1; use the workspace monorepo Project Shape",
648
+ },
649
+ ]
650
+ : []);
651
+ }
652
+ function resolvePresetSourcePath(sourceRoot, referencePath) {
653
+ if (path.isAbsolute(referencePath)) {
654
+ return { issue: `Preset Source paths must be relative: ${referencePath}` };
655
+ }
656
+ const resolvedRoot = path.resolve(sourceRoot);
657
+ const resolvedPath = path.resolve(resolvedRoot, referencePath);
658
+ const insideRoot = resolvedPath === resolvedRoot ||
659
+ resolvedPath.startsWith(`${resolvedRoot}${path.sep}`);
660
+ if (!insideRoot) {
661
+ return {
662
+ issue: `Preset Source path escapes its source boundary: ${referencePath}`,
663
+ };
664
+ }
665
+ return resolvedPath;
666
+ }
667
+ function realPresetSourcePathIssue(sourceRoot, referencePath, resolvedPath) {
668
+ const realRoot = realpathSync(sourceRoot);
669
+ const realPath = realpathSync(resolvedPath);
670
+ const insideRoot = realPath === realRoot || realPath.startsWith(`${realRoot}${path.sep}`);
671
+ return insideRoot
672
+ ? undefined
673
+ : `Preset Source path escapes its source boundary: ${referencePath}`;
674
+ }
675
+ function sharedResourcePathIssues(manifest, sourceRoot) {
676
+ if (sourceRoot === undefined) {
677
+ return [];
678
+ }
679
+ return manifest.sharedResources.flatMap((resource, index) => {
680
+ const resolvedPath = resolvePresetSourcePath(sourceRoot, resource.path);
681
+ if (typeof resolvedPath !== "string") {
682
+ return [
683
+ {
684
+ path: `$.sharedResources[${index}].path`,
685
+ message: resolvedPath.issue,
686
+ },
687
+ ];
688
+ }
689
+ if (!existsSync(resolvedPath)) {
690
+ return [
691
+ {
692
+ path: `$.sharedResources[${index}].path`,
693
+ message: `Shared Resource ${resource.id} path does not exist: ${resource.path}`,
694
+ },
695
+ ];
696
+ }
697
+ const realPathIssue = realPresetSourcePathIssue(sourceRoot, resource.path, resolvedPath);
698
+ if (realPathIssue) {
699
+ return [
700
+ {
701
+ path: `$.sharedResources[${index}].path`,
702
+ message: realPathIssue,
703
+ },
704
+ ];
705
+ }
706
+ return [];
707
+ });
708
+ }
709
+ function isRecord(value) {
710
+ return typeof value === "object" && value !== null && !Array.isArray(value);
711
+ }
712
+ function isDependencySemverSpecifier(value) {
713
+ return /^(?:[~^]|[<>=]=?)?\d+(?:\.\d+){0,2}(?:[-+][\w.-]+)?(?:\s|$|\|\|)/.test(value);
714
+ }
715
+ function inlineDependencyCatalogSpecifierIssues(input) {
716
+ if (!isRecord(input) || !Array.isArray(input.presets)) {
717
+ return [];
718
+ }
719
+ return input.presets.flatMap((preset, presetIndex) => {
720
+ if (!isRecord(preset)) {
721
+ return [];
722
+ }
723
+ if (Array.isArray(preset.dependencyCatalog)) {
724
+ return preset.dependencyCatalog.flatMap((specifier, dependencyIndex) => typeof specifier === "string" && isDependencySemverSpecifier(specifier)
725
+ ? [
726
+ {
727
+ path: `$.presets[${presetIndex}].dependencyCatalog[${dependencyIndex}]`,
728
+ message: `Preset Source Manifests must reference Template Dependency Catalog entries by name, not inline semver specifier ${specifier}`,
729
+ },
730
+ ]
731
+ : []);
732
+ }
733
+ if (!isRecord(preset.dependencyCatalog)) {
734
+ return [];
735
+ }
736
+ return Object.entries(preset.dependencyCatalog)
737
+ .filter((entry) => typeof entry[1] === "string" && isDependencySemverSpecifier(entry[1]))
738
+ .map(([dependency, specifier]) => ({
739
+ path: `$.presets[${presetIndex}].dependencyCatalog.${dependency}`,
740
+ message: `Preset Source Manifests must reference Template Dependency Catalog entries by name, not inline semver specifier ${specifier}`,
741
+ }));
742
+ });
743
+ }
744
+ function presetSourceReferenceIssues(manifest, sourceRoot) {
745
+ const sharedResourceIds = new Set(manifest.sharedResources.map((resource) => resource.id));
746
+ return manifest.presets.flatMap((preset, presetIndex) => {
747
+ const source = preset.source;
748
+ if (!source) {
749
+ return [];
750
+ }
751
+ const sharedResourceIssues = source.sharedResources
752
+ .filter((resourceId) => !sharedResourceIds.has(resourceId))
753
+ .map((resourceId) => ({
754
+ path: `$.presets[${presetIndex}].source.sharedResources`,
755
+ message: `Preset ${preset.name} references undeclared Shared Resource: ${resourceId}`,
756
+ }));
757
+ const pathIssues = sourceRoot === undefined
758
+ ? []
759
+ : [
760
+ ...source.roots.map((sourcePath, index) => ({
761
+ path: `$.presets[${presetIndex}].source.roots[${index}]`,
762
+ sourcePath,
763
+ kind: "source root",
764
+ })),
765
+ ...source.files.map((sourcePath, index) => ({
766
+ path: `$.presets[${presetIndex}].source.files[${index}]`,
767
+ sourcePath,
768
+ kind: "source file",
769
+ })),
770
+ ].flatMap(({ path: issuePath, sourcePath, kind }) => {
771
+ const resolvedPath = resolvePresetSourcePath(sourceRoot, sourcePath);
772
+ if (typeof resolvedPath !== "string") {
773
+ return [{ path: issuePath, message: resolvedPath.issue }];
774
+ }
775
+ if (!existsSync(resolvedPath)) {
776
+ return [
777
+ {
778
+ path: issuePath,
779
+ message: `Preset ${preset.name} ${kind} does not exist: ${sourcePath}`,
780
+ },
781
+ ];
782
+ }
783
+ const realPathIssue = realPresetSourcePathIssue(sourceRoot, sourcePath, resolvedPath);
784
+ if (realPathIssue) {
785
+ return [{ path: issuePath, message: realPathIssue }];
786
+ }
787
+ return [];
788
+ });
789
+ return [...sharedResourceIssues, ...pathIssues];
790
+ });
791
+ }
792
+ function supportedProjectionDeclarationIssues(presets) {
793
+ return presets.flatMap((preset, index) => preset.generation === "supported" && preset.projection === undefined
794
+ ? [
795
+ {
796
+ path: `$.presets[${index}].projection`,
797
+ message: `Supported Preset ${preset.name} must declare a Projection Declaration`,
798
+ },
799
+ ]
800
+ : []);
801
+ }
802
+ export function validatePresetSourceManifest(input, options = {}) {
803
+ const inlineDependencyIssues = inlineDependencyCatalogSpecifierIssues(input);
804
+ if (inlineDependencyIssues.length > 0) {
805
+ return { ok: false, issues: inlineDependencyIssues };
806
+ }
807
+ const result = v.safeParse(presetSourceManifestSchema, input);
808
+ if (!result.success) {
809
+ return { ok: false, issues: shapeIssues(result.issues) };
810
+ }
811
+ const parsedManifest = result.output;
812
+ const semanticIssues = [
813
+ ...duplicateValueIssues(parsedManifest.sharedResources.map((resource) => resource.id), "$.sharedResources.id"),
814
+ ...duplicatePresetNameIssues(parsedManifest.presets),
815
+ ...duplicatePresetMetadataArrayIssues(parsedManifest.presets),
816
+ ...unsupportedProjectShapeIssues(parsedManifest.presets),
817
+ ...fixtureMatrixContractIssues(parsedManifest),
818
+ ...dependencyCatalogReferenceIssues(parsedManifest.presets, options.dependencyCatalog ?? loadTemplateDependencyCatalog()),
819
+ ...projectionCapabilityIssues(parsedManifest.presets),
820
+ ...sharedResourcePathIssues(parsedManifest, options.sourceRoot),
821
+ ...presetSourceReferenceIssues(parsedManifest, options.sourceRoot),
822
+ ];
823
+ if (semanticIssues.length > 0) {
824
+ return { ok: false, issues: semanticIssues };
825
+ }
826
+ return {
827
+ ok: true,
828
+ value: {
829
+ ...result.output,
830
+ sharedResources: result.output.sharedResources.map((resource) => ({
831
+ ...resource,
832
+ })),
833
+ fixtureMatrix: result.output.fixtureMatrix
834
+ ? {
835
+ initSupport: result.output.fixtureMatrix.initSupport.map((support) => ({ ...support })),
836
+ packageAdditionSupport: result.output.fixtureMatrix.packageAdditionSupport.map((support) => ({ ...support })),
837
+ supportedCombinations: result.output.fixtureMatrix.supportedCombinations.map((combination) => ({
838
+ ...combination,
839
+ linkFrom: [...(combination.linkFrom ?? [])],
840
+ })),
841
+ semanticSkips: result.output.fixtureMatrix.semanticSkips.map((skip) => ({ ...skip })),
842
+ checkRequirements: [
843
+ ...result.output.fixtureMatrix.checkRequirements,
844
+ ],
845
+ environmentPreparation: [
846
+ ...result.output.fixtureMatrix.environmentPreparation,
847
+ ],
848
+ }
849
+ : undefined,
850
+ presets: parsedManifest.presets.map((preset) => ({
851
+ ...preset,
852
+ supportedPackageManagers: [
853
+ ...preset.supportedPackageManagers,
854
+ ],
855
+ supportedProjectKinds: [
856
+ ...preset.supportedProjectKinds,
857
+ ],
858
+ features: [...preset.features],
859
+ dependencyCatalog: [...(preset.dependencyCatalog ?? [])],
860
+ projection: preset.projection
861
+ ? normalizePresetProjectionDeclaration(preset.projection)
862
+ : undefined,
863
+ source: preset.source
864
+ ? {
865
+ roots: [...preset.source.roots],
866
+ files: [...preset.source.files],
867
+ sharedResources: [...preset.source.sharedResources],
868
+ }
869
+ : undefined,
870
+ })),
871
+ },
872
+ };
873
+ }
874
+ export function validateBuiltInPresetSourceManifest(input, options = {}) {
875
+ const result = validatePresetSourceManifest(input, options);
876
+ if (!result.ok) {
877
+ return result;
878
+ }
879
+ const projectionIssues = supportedProjectionDeclarationIssues(result.value.presets);
880
+ if (projectionIssues.length > 0) {
881
+ return { ok: false, issues: projectionIssues };
882
+ }
883
+ return result;
884
+ }
885
+ function listPresetSourceFiles(sourcePath) {
886
+ const stats = statSync(sourcePath);
887
+ if (stats.isFile()) {
888
+ return [sourcePath];
889
+ }
890
+ if (!stats.isDirectory()) {
891
+ return [];
892
+ }
893
+ return readdirSync(sourcePath, { withFileTypes: true }).flatMap((entry) => listPresetSourceFiles(path.join(sourcePath, entry.name)));
894
+ }
895
+ export function manifestReferencedSourceFiles(manifest, sourceRoot) {
896
+ const files = new Set();
897
+ function addReference(referencePath) {
898
+ for (const file of listPresetSourceFiles(path.resolve(sourceRoot, referencePath))) {
899
+ files.add(file);
900
+ }
901
+ }
902
+ for (const resource of manifest.sharedResources) {
903
+ addReference(resource.path);
904
+ }
905
+ for (const preset of manifest.presets) {
906
+ for (const root of preset.source?.roots ?? []) {
907
+ addReference(root);
908
+ }
909
+ for (const file of preset.source?.files ?? []) {
910
+ addReference(file);
911
+ }
912
+ }
913
+ return [...files].sort();
914
+ }
915
+ export function loadPresetSourceManifestFile(filePath) {
916
+ const result = validatePresetSourceManifest(JSON.parse(readFileSync(filePath, "utf8")), { sourceRoot: path.dirname(filePath) });
917
+ if (!result.ok) {
918
+ throw new Error(`Preset Source Manifest is invalid:\n${result.issues
919
+ .map((issue) => ` - ${issue.path}: ${issue.message}`)
920
+ .join("\n")}`);
921
+ }
922
+ return result.value;
923
+ }
924
+ export function loadBuiltInPresetSourceManifest() {
925
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
926
+ const filePath = packageTemplateRoot(moduleDir, "preset-source.json");
927
+ const result = validateBuiltInPresetSourceManifest(JSON.parse(readFileSync(filePath, "utf8")), { sourceRoot: path.dirname(filePath) });
928
+ if (!result.ok) {
929
+ throw new Error(`Built-in Preset Source Manifest is invalid:\n${result.issues
930
+ .map((issue) => ` - ${issue.path}: ${issue.message}`)
931
+ .join("\n")}`);
932
+ }
933
+ return result.value;
934
+ }
935
+ export function findPresetSourceManifestPreset(manifest, presetName) {
936
+ return manifest.presets.find((preset) => preset.name === presetName);
937
+ }
938
+ export function findBuiltInPresetSourceManifestPreset(presetName) {
939
+ return findPresetSourceManifestPreset(loadBuiltInPresetSourceManifest(), presetName);
940
+ }