@wp-typia/create 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +43 -0
  2. package/dist/cli.js +2492 -0
  3. package/dist/runtime/cli-core.js +222 -0
  4. package/dist/runtime/index.js +4 -0
  5. package/dist/runtime/migration-constants.js +14 -0
  6. package/dist/runtime/migration-diff.js +521 -0
  7. package/dist/runtime/migration-fixtures.js +89 -0
  8. package/dist/runtime/migration-manifest.js +129 -0
  9. package/dist/runtime/migration-project.js +167 -0
  10. package/dist/runtime/migration-render.js +267 -0
  11. package/dist/runtime/migration-types.js +1 -0
  12. package/dist/runtime/migration-utils.js +184 -0
  13. package/dist/runtime/migrations.js +232 -0
  14. package/dist/runtime/package-managers.js +135 -0
  15. package/dist/runtime/scaffold.js +334 -0
  16. package/dist/runtime/template-registry.js +75 -0
  17. package/package.json +65 -0
  18. package/templates/advanced/README.md.mustache +150 -0
  19. package/templates/advanced/block.json.mustache +43 -0
  20. package/templates/advanced/index.js +21 -0
  21. package/templates/advanced/package.json.mustache +47 -0
  22. package/templates/advanced/render.php.mustache +83 -0
  23. package/templates/advanced/scripts/lib/typia-metadata-core.ts +1413 -0
  24. package/templates/advanced/scripts/sync-types-to-block-json.ts.mustache +32 -0
  25. package/templates/advanced/src/admin/migration-dashboard.tsx.mustache +315 -0
  26. package/templates/advanced/src/components/ErrorBoundary.tsx.mustache +47 -0
  27. package/templates/advanced/src/deprecated.ts.mustache +2 -0
  28. package/templates/advanced/src/edit.tsx.mustache +97 -0
  29. package/templates/advanced/src/hooks/useDebounce.ts.mustache +20 -0
  30. package/templates/advanced/src/hooks/useLocalStorage.ts.mustache +31 -0
  31. package/templates/advanced/src/hooks.ts.mustache +56 -0
  32. package/templates/advanced/src/index.tsx.mustache +18 -0
  33. package/templates/advanced/src/migration-detector.ts.mustache +9 -0
  34. package/templates/advanced/src/migrations/config.ts.mustache +8 -0
  35. package/templates/advanced/src/migrations/examples/rename-transform-union/README.md.mustache +23 -0
  36. package/templates/advanced/src/migrations/examples/rename-transform-union/fixture.example.json.mustache +36 -0
  37. package/templates/advanced/src/migrations/examples/rename-transform-union/rule.example.ts.mustache +47 -0
  38. package/templates/advanced/src/migrations/fixtures/README.md.mustache +3 -0
  39. package/templates/advanced/src/migrations/generated/deprecated.ts.mustache +3 -0
  40. package/templates/advanced/src/migrations/generated/registry.ts.mustache +9 -0
  41. package/templates/advanced/src/migrations/generated/verify.ts.mustache +1 -0
  42. package/templates/advanced/src/migrations/helpers.ts.mustache +354 -0
  43. package/templates/advanced/src/migrations/index.ts.mustache +616 -0
  44. package/templates/advanced/src/migrations/rules/README.md.mustache +3 -0
  45. package/templates/advanced/src/migrations/versions/README.md.mustache +3 -0
  46. package/templates/advanced/src/save.tsx.mustache +12 -0
  47. package/templates/advanced/src/style.scss.mustache +84 -0
  48. package/templates/advanced/src/types.ts.mustache +46 -0
  49. package/templates/advanced/src/utils/classnames.ts.mustache +51 -0
  50. package/templates/advanced/src/utils/debounce.ts.mustache +37 -0
  51. package/templates/advanced/src/utils/index.ts.mustache +7 -0
  52. package/templates/advanced/src/utils/uuid.ts.mustache +17 -0
  53. package/templates/advanced/src/validators.ts.mustache +39 -0
  54. package/templates/advanced/src/view.ts.mustache +59 -0
  55. package/templates/advanced/tsconfig.json.mustache +20 -0
  56. package/templates/advanced/webpack.config.js.mustache +95 -0
  57. package/templates/basic/package.json.mustache +39 -0
  58. package/templates/basic/scripts/lib/typia-metadata-core.ts +1413 -0
  59. package/templates/basic/scripts/sync-types-to-block-json.ts +25 -0
  60. package/templates/basic/src/block.json +51 -0
  61. package/templates/basic/src/edit.tsx +85 -0
  62. package/templates/basic/src/hooks.ts +75 -0
  63. package/templates/basic/src/index.tsx +37 -0
  64. package/templates/basic/src/save.tsx +27 -0
  65. package/templates/basic/src/style.scss +42 -0
  66. package/templates/basic/src/types.ts +48 -0
  67. package/templates/basic/src/validators.ts +39 -0
  68. package/templates/basic/tsconfig.json +20 -0
  69. package/templates/basic/webpack.config.js +89 -0
  70. package/templates/full/package.json.mustache +40 -0
  71. package/templates/full/scripts/lib/typia-metadata-core.ts +1413 -0
  72. package/templates/full/scripts/sync-types-to-block-json.ts.mustache +32 -0
  73. package/templates/full/src/block.json.mustache +120 -0
  74. package/templates/full/src/edit.tsx.mustache +300 -0
  75. package/templates/full/src/editor.scss.mustache +251 -0
  76. package/templates/full/src/hooks.ts.mustache +141 -0
  77. package/templates/full/src/index.tsx.mustache +27 -0
  78. package/templates/full/src/save.tsx.mustache +39 -0
  79. package/templates/full/src/style.scss.mustache +224 -0
  80. package/templates/full/src/types.ts.mustache +35 -0
  81. package/templates/full/src/validators.ts.mustache +84 -0
  82. package/templates/full/tsconfig.json.mustache +20 -0
  83. package/templates/full/webpack.config.js.mustache +89 -0
  84. package/templates/interactivity/package.json.mustache +41 -0
  85. package/templates/interactivity/scripts/lib/typia-metadata-core.ts +1413 -0
  86. package/templates/interactivity/scripts/sync-types-to-block-json.ts.mustache +32 -0
  87. package/templates/interactivity/src/block.json.mustache +74 -0
  88. package/templates/interactivity/src/edit.tsx.mustache +206 -0
  89. package/templates/interactivity/src/index.tsx.mustache +20 -0
  90. package/templates/interactivity/src/interactivity.ts.mustache +183 -0
  91. package/templates/interactivity/src/save.tsx.mustache +87 -0
  92. package/templates/interactivity/src/style.scss.mustache +60 -0
  93. package/templates/interactivity/src/types.ts.mustache +30 -0
  94. package/templates/interactivity/tsconfig.json.mustache +20 -0
  95. package/templates/interactivity/webpack.config.js.mustache +89 -0
@@ -0,0 +1,1413 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import ts from "typescript";
4
+
5
+ type JsonPrimitive = string | number | boolean | null;
6
+ type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
7
+ type AttributeKind = "string" | "number" | "boolean" | "array" | "object" | "union";
8
+ type WordPressAttributeKind = "string" | "number" | "boolean" | "array" | "object";
9
+
10
+ interface AttributeConstraints {
11
+ format: string | null;
12
+ maxLength: number | null;
13
+ maximum: number | null;
14
+ minLength: number | null;
15
+ minimum: number | null;
16
+ pattern: string | null;
17
+ typeTag: string | null;
18
+ }
19
+
20
+ interface AttributeNode {
21
+ constraints: AttributeConstraints;
22
+ defaultValue?: JsonValue;
23
+ enumValues: Array<string | number | boolean> | null;
24
+ items?: AttributeNode;
25
+ kind: AttributeKind;
26
+ path: string;
27
+ properties?: Record<string, AttributeNode>;
28
+ required: boolean;
29
+ union?: AttributeUnion | null;
30
+ }
31
+
32
+ interface AttributeUnion {
33
+ branches: Record<string, AttributeNode>;
34
+ discriminator: string;
35
+ }
36
+
37
+ interface BlockJsonAttribute {
38
+ default?: JsonValue;
39
+ enum?: Array<string | number | boolean>;
40
+ type: WordPressAttributeKind;
41
+ }
42
+
43
+ interface ManifestAttribute {
44
+ typia: {
45
+ constraints: AttributeConstraints;
46
+ defaultValue: JsonValue | null;
47
+ hasDefault: boolean;
48
+ };
49
+ ts: {
50
+ items: ManifestAttribute | null;
51
+ kind: AttributeKind;
52
+ properties: Record<string, ManifestAttribute> | null;
53
+ required: boolean;
54
+ union: ManifestUnion | null;
55
+ };
56
+ wp: {
57
+ defaultValue: JsonValue | null;
58
+ enum: Array<string | number | boolean> | null;
59
+ hasDefault: boolean;
60
+ type: WordPressAttributeKind;
61
+ };
62
+ }
63
+
64
+ interface ManifestUnion {
65
+ branches: Record<string, ManifestAttribute>;
66
+ discriminator: string;
67
+ }
68
+
69
+ interface ManifestDocument {
70
+ attributes: Record<string, ManifestAttribute>;
71
+ manifestVersion: 2;
72
+ sourceType: string;
73
+ }
74
+
75
+ export interface SyncBlockMetadataOptions {
76
+ blockJsonFile: string;
77
+ manifestFile?: string;
78
+ phpValidatorFile?: string;
79
+ projectRoot?: string;
80
+ sourceTypeName: string;
81
+ typesFile: string;
82
+ }
83
+
84
+ export interface SyncBlockMetadataResult {
85
+ attributeNames: string[];
86
+ blockJsonPath: string;
87
+ lossyProjectionWarnings: string[];
88
+ manifestPath: string;
89
+ phpGenerationWarnings: string[];
90
+ phpValidatorPath: string;
91
+ }
92
+
93
+ interface AnalysisContext {
94
+ allowedExternalPackages: Set<string>;
95
+ checker: ts.TypeChecker;
96
+ packageNameCache: Map<string, string | null>;
97
+ projectRoot: string;
98
+ program: ts.Program;
99
+ recursionGuard: Set<string>;
100
+ }
101
+
102
+ const SUPPORTED_TAGS = new Set([
103
+ "Default",
104
+ "Format",
105
+ "MaxLength",
106
+ "Maximum",
107
+ "MinLength",
108
+ "Minimum",
109
+ "Pattern",
110
+ "Type",
111
+ ]);
112
+
113
+ const DEFAULT_CONSTRAINTS = (): AttributeConstraints => ({
114
+ format: null,
115
+ maxLength: null,
116
+ maximum: null,
117
+ minLength: null,
118
+ minimum: null,
119
+ pattern: null,
120
+ typeTag: null,
121
+ });
122
+
123
+ export async function syncBlockMetadata(
124
+ options: SyncBlockMetadataOptions,
125
+ ): Promise<SyncBlockMetadataResult> {
126
+ const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
127
+ const typesFilePath = path.resolve(projectRoot, options.typesFile);
128
+ const blockJsonPath = path.resolve(projectRoot, options.blockJsonFile);
129
+ const manifestRelativePath =
130
+ options.manifestFile ?? path.join(path.dirname(options.blockJsonFile), "typia.manifest.json");
131
+ const manifestPath = path.resolve(
132
+ projectRoot,
133
+ manifestRelativePath,
134
+ );
135
+ const phpValidatorPath = path.resolve(
136
+ projectRoot,
137
+ options.phpValidatorFile ?? path.join(path.dirname(manifestRelativePath), "typia-validator.php"),
138
+ );
139
+
140
+ const ctx = createAnalysisContext(projectRoot, typesFilePath);
141
+ const sourceFile = ctx.program.getSourceFile(typesFilePath);
142
+ if (sourceFile === undefined) {
143
+ throw new Error(`Unable to load types file: ${typesFilePath}`);
144
+ }
145
+
146
+ const declaration = findNamedDeclaration(sourceFile, options.sourceTypeName);
147
+ if (declaration === undefined) {
148
+ throw new Error(
149
+ `Unable to find source type "${options.sourceTypeName}" in ${path.relative(projectRoot, typesFilePath)}`,
150
+ );
151
+ }
152
+
153
+ const rootNode = parseNamedDeclaration(declaration, ctx, options.sourceTypeName, true);
154
+ if (rootNode.kind !== "object" || rootNode.properties === undefined) {
155
+ throw new Error(`Source type "${options.sourceTypeName}" must resolve to an object shape`);
156
+ }
157
+
158
+ const blockJson = JSON.parse(fs.readFileSync(blockJsonPath, "utf8")) as Record<string, unknown>;
159
+ const lossyProjectionWarnings: string[] = [];
160
+
161
+ blockJson.attributes = Object.fromEntries(
162
+ Object.entries(rootNode.properties).map(([key, node]) => [
163
+ key,
164
+ createBlockJsonAttribute(node, lossyProjectionWarnings),
165
+ ]),
166
+ );
167
+ blockJson.example = {
168
+ attributes: Object.fromEntries(
169
+ Object.entries(rootNode.properties).map(([key, node]) => [key, createExampleValue(node, key)]),
170
+ ),
171
+ };
172
+
173
+ const manifest: ManifestDocument = {
174
+ attributes: Object.fromEntries(
175
+ Object.entries(rootNode.properties).map(([key, node]) => [key, createManifestAttribute(node)]),
176
+ ),
177
+ manifestVersion: 2,
178
+ sourceType: options.sourceTypeName,
179
+ };
180
+
181
+ fs.writeFileSync(blockJsonPath, JSON.stringify(blockJson, null, "\t"));
182
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
183
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, "\t"));
184
+
185
+ const phpValidator = renderPhpValidator(manifest);
186
+ fs.mkdirSync(path.dirname(phpValidatorPath), { recursive: true });
187
+ fs.writeFileSync(phpValidatorPath, phpValidator.source);
188
+
189
+ return {
190
+ attributeNames: Object.keys(rootNode.properties),
191
+ blockJsonPath,
192
+ lossyProjectionWarnings: [...new Set(lossyProjectionWarnings)].sort(),
193
+ manifestPath,
194
+ phpGenerationWarnings: [...new Set(phpValidator.warnings)].sort(),
195
+ phpValidatorPath,
196
+ };
197
+ }
198
+
199
+ function createAnalysisContext(projectRoot: string, typesFilePath: string): AnalysisContext {
200
+ const configPath = ts.findConfigFile(projectRoot, ts.sys.fileExists, "tsconfig.json");
201
+ const compilerOptions: ts.CompilerOptions = {
202
+ allowJs: false,
203
+ esModuleInterop: true,
204
+ module: ts.ModuleKind.NodeNext,
205
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
206
+ resolveJsonModule: true,
207
+ skipLibCheck: true,
208
+ target: ts.ScriptTarget.ES2022,
209
+ };
210
+
211
+ let rootNames = [typesFilePath];
212
+
213
+ if (configPath !== undefined) {
214
+ const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
215
+ if (configFile.error) {
216
+ throw formatDiagnosticError(configFile.error);
217
+ }
218
+
219
+ const parsed = ts.parseJsonConfigFileContent(
220
+ configFile.config,
221
+ ts.sys,
222
+ path.dirname(configPath),
223
+ compilerOptions,
224
+ configPath,
225
+ );
226
+
227
+ if (parsed.errors.length > 0) {
228
+ throw formatDiagnosticError(parsed.errors[0]);
229
+ }
230
+
231
+ rootNames = parsed.fileNames.includes(typesFilePath)
232
+ ? parsed.fileNames
233
+ : [...parsed.fileNames, typesFilePath];
234
+ Object.assign(compilerOptions, parsed.options);
235
+ }
236
+
237
+ const program = ts.createProgram({
238
+ options: compilerOptions,
239
+ rootNames,
240
+ });
241
+ const diagnostics = ts.getPreEmitDiagnostics(program);
242
+ const blockingDiagnostic = diagnostics.find(
243
+ (diagnostic) =>
244
+ diagnostic.category === ts.DiagnosticCategory.Error &&
245
+ diagnostic.file?.fileName === typesFilePath,
246
+ );
247
+ if (blockingDiagnostic) {
248
+ throw formatDiagnosticError(blockingDiagnostic);
249
+ }
250
+
251
+ return {
252
+ allowedExternalPackages: new Set(["@wp-typia/block-types"]),
253
+ checker: program.getTypeChecker(),
254
+ packageNameCache: new Map(),
255
+ projectRoot,
256
+ program,
257
+ recursionGuard: new Set<string>(),
258
+ };
259
+ }
260
+
261
+ function findNamedDeclaration(
262
+ sourceFile: ts.SourceFile,
263
+ name: string,
264
+ ): ts.InterfaceDeclaration | ts.TypeAliasDeclaration | undefined {
265
+ for (const statement of sourceFile.statements) {
266
+ if ((ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)) && statement.name.text === name) {
267
+ return statement;
268
+ }
269
+ }
270
+ return undefined;
271
+ }
272
+
273
+ function parseNamedDeclaration(
274
+ declaration: ts.InterfaceDeclaration | ts.TypeAliasDeclaration,
275
+ ctx: AnalysisContext,
276
+ pathLabel: string,
277
+ required: boolean,
278
+ ): AttributeNode {
279
+ const recursionKey = `${declaration.getSourceFile().fileName}:${declaration.name.text}`;
280
+ if (ctx.recursionGuard.has(recursionKey)) {
281
+ throw new Error(`Recursive types are not supported: ${pathLabel}`);
282
+ }
283
+
284
+ ctx.recursionGuard.add(recursionKey);
285
+ try {
286
+ if (ts.isInterfaceDeclaration(declaration)) {
287
+ return parseInterfaceDeclaration(declaration, ctx, pathLabel, required);
288
+ }
289
+ return withRequired(parseTypeNode(declaration.type, ctx, pathLabel), required);
290
+ } finally {
291
+ ctx.recursionGuard.delete(recursionKey);
292
+ }
293
+ }
294
+
295
+ function parseInterfaceDeclaration(
296
+ declaration: ts.InterfaceDeclaration,
297
+ ctx: AnalysisContext,
298
+ pathLabel: string,
299
+ required: boolean,
300
+ ): AttributeNode {
301
+ const properties: Record<string, AttributeNode> = {};
302
+
303
+ for (const heritageClause of declaration.heritageClauses ?? []) {
304
+ if (heritageClause.token !== ts.SyntaxKind.ExtendsKeyword) {
305
+ continue;
306
+ }
307
+
308
+ for (const baseType of heritageClause.types) {
309
+ const baseNode = parseTypeReference(baseType, ctx, `${pathLabel}<extends>`);
310
+ if (baseNode.kind !== "object" || baseNode.properties === undefined) {
311
+ throw new Error(`Only object-like interface extensions are supported: ${pathLabel}`);
312
+ }
313
+ Object.assign(properties, cloneProperties(baseNode.properties));
314
+ }
315
+ }
316
+
317
+ for (const member of declaration.members) {
318
+ if (!ts.isPropertySignature(member) || member.type === undefined) {
319
+ throw new Error(`Unsupported member in ${pathLabel}; only typed properties are supported`);
320
+ }
321
+
322
+ const propertyName = getPropertyName(member.name);
323
+ properties[propertyName] = withRequired(
324
+ parseTypeNode(member.type, ctx, `${pathLabel}.${propertyName}`),
325
+ member.questionToken === undefined,
326
+ );
327
+ }
328
+
329
+ return {
330
+ constraints: DEFAULT_CONSTRAINTS(),
331
+ enumValues: null,
332
+ kind: "object",
333
+ path: pathLabel,
334
+ properties,
335
+ required,
336
+ union: null,
337
+ };
338
+ }
339
+
340
+ function parseTypeNode(node: ts.TypeNode, ctx: AnalysisContext, pathLabel: string): AttributeNode {
341
+ if (ts.isParenthesizedTypeNode(node)) {
342
+ return parseTypeNode(node.type, ctx, pathLabel);
343
+ }
344
+ if (ts.isIntersectionTypeNode(node)) {
345
+ return parseIntersectionType(node, ctx, pathLabel);
346
+ }
347
+ if (ts.isUnionTypeNode(node)) {
348
+ return parseUnionType(node, ctx, pathLabel);
349
+ }
350
+ if (ts.isTypeLiteralNode(node)) {
351
+ return parseTypeLiteral(node, ctx, pathLabel);
352
+ }
353
+ if (ts.isArrayTypeNode(node)) {
354
+ return {
355
+ constraints: DEFAULT_CONSTRAINTS(),
356
+ enumValues: null,
357
+ items: withRequired(parseTypeNode(node.elementType, ctx, `${pathLabel}[]`), true),
358
+ kind: "array",
359
+ path: pathLabel,
360
+ required: true,
361
+ union: null,
362
+ };
363
+ }
364
+ if (ts.isLiteralTypeNode(node)) {
365
+ return parseLiteralType(node, pathLabel);
366
+ }
367
+ if (ts.isTypeReferenceNode(node)) {
368
+ return parseTypeReference(node, ctx, pathLabel);
369
+ }
370
+ if (node.kind === ts.SyntaxKind.StringKeyword) {
371
+ return baseNode("string", pathLabel);
372
+ }
373
+ if (node.kind === ts.SyntaxKind.NumberKeyword || node.kind === ts.SyntaxKind.BigIntKeyword) {
374
+ return baseNode("number", pathLabel);
375
+ }
376
+ if (node.kind === ts.SyntaxKind.BooleanKeyword) {
377
+ return baseNode("boolean", pathLabel);
378
+ }
379
+
380
+ throw new Error(`Unsupported type node at ${pathLabel}: ${node.getText()}`);
381
+ }
382
+
383
+ function parseIntersectionType(
384
+ node: ts.IntersectionTypeNode,
385
+ ctx: AnalysisContext,
386
+ pathLabel: string,
387
+ ): AttributeNode {
388
+ const tagNodes: ts.TypeReferenceNode[] = [];
389
+ const valueNodes: ts.TypeNode[] = [];
390
+
391
+ for (const typeNode of node.types) {
392
+ if (ts.isTypeReferenceNode(typeNode) && getSupportedTagName(typeNode) !== null) {
393
+ tagNodes.push(typeNode);
394
+ } else {
395
+ valueNodes.push(typeNode);
396
+ }
397
+ }
398
+
399
+ if (valueNodes.length === 0) {
400
+ throw new Error(`Intersection at ${pathLabel} does not contain a value type`);
401
+ }
402
+ if (valueNodes.length > 1) {
403
+ throw new Error(
404
+ `Unsupported intersection at ${pathLabel}; only a single value type plus typia tags is supported`,
405
+ );
406
+ }
407
+
408
+ const parsed = parseTypeNode(valueNodes[0], ctx, pathLabel);
409
+ for (const tagNode of tagNodes) {
410
+ applyTag(parsed, tagNode, pathLabel);
411
+ }
412
+
413
+ return parsed;
414
+ }
415
+
416
+ function parseUnionType(node: ts.UnionTypeNode, ctx: AnalysisContext, pathLabel: string): AttributeNode {
417
+ const literalValues = node.types
418
+ .map((typeNode) => extractLiteralValue(typeNode))
419
+ .filter((value): value is string | number | boolean => value !== undefined);
420
+
421
+ if (literalValues.length === node.types.length && literalValues.length > 0) {
422
+ const uniqueKinds = new Set(literalValues.map((value) => typeof value));
423
+ if (uniqueKinds.size !== 1) {
424
+ throw new Error(`Mixed primitive enums are not supported at ${pathLabel}`);
425
+ }
426
+
427
+ const kind = [...uniqueKinds][0] as "string" | "number" | "boolean";
428
+ return {
429
+ constraints: DEFAULT_CONSTRAINTS(),
430
+ enumValues: literalValues,
431
+ kind,
432
+ path: pathLabel,
433
+ required: true,
434
+ union: null,
435
+ };
436
+ }
437
+
438
+ const withoutUndefined = node.types.filter(
439
+ (typeNode) => typeNode.kind !== ts.SyntaxKind.UndefinedKeyword && typeNode.kind !== ts.SyntaxKind.NullKeyword,
440
+ );
441
+
442
+ if (withoutUndefined.length === 1) {
443
+ return parseTypeNode(withoutUndefined[0], ctx, pathLabel);
444
+ }
445
+
446
+ if (withoutUndefined.length > 1) {
447
+ return parseDiscriminatedUnion(withoutUndefined, ctx, pathLabel);
448
+ }
449
+
450
+ throw new Error(`Unsupported union type at ${pathLabel}: ${node.getText()}`);
451
+ }
452
+
453
+ function parseDiscriminatedUnion(
454
+ typeNodes: ts.TypeNode[],
455
+ ctx: AnalysisContext,
456
+ pathLabel: string,
457
+ ): AttributeNode {
458
+ const branchNodes = typeNodes.map((typeNode, index) => ({
459
+ node: parseTypeNode(typeNode, ctx, `${pathLabel}<branch:${index}>`),
460
+ source: typeNode,
461
+ }));
462
+
463
+ for (const branch of branchNodes) {
464
+ if (branch.node.kind !== "object" || branch.node.properties === undefined) {
465
+ throw new Error(
466
+ `Unsupported union type at ${pathLabel}; only discriminated object unions are supported`,
467
+ );
468
+ }
469
+ }
470
+
471
+ const discriminator = findDiscriminatorKey(branchNodes.map((branch) => branch.node), pathLabel);
472
+ const branches: Record<string, AttributeNode> = {};
473
+
474
+ for (const branch of branchNodes) {
475
+ const discriminatorNode = branch.node.properties?.[discriminator];
476
+ const discriminatorValue = discriminatorNode?.enumValues?.[0];
477
+
478
+ if (typeof discriminatorValue !== "string") {
479
+ throw new Error(
480
+ `Discriminated union at ${pathLabel} must use string literal discriminator values`,
481
+ );
482
+ }
483
+ if (branches[discriminatorValue] !== undefined) {
484
+ throw new Error(
485
+ `Discriminated union at ${pathLabel} has duplicate discriminator value "${discriminatorValue}"`,
486
+ );
487
+ }
488
+
489
+ branches[discriminatorValue] = withRequired(branch.node, true);
490
+ }
491
+
492
+ return {
493
+ constraints: DEFAULT_CONSTRAINTS(),
494
+ enumValues: null,
495
+ kind: "union",
496
+ path: pathLabel,
497
+ required: true,
498
+ union: {
499
+ branches,
500
+ discriminator,
501
+ },
502
+ };
503
+ }
504
+
505
+ function findDiscriminatorKey(branches: AttributeNode[], pathLabel: string): string {
506
+ const candidateKeys = new Set(Object.keys(branches[0].properties ?? {}));
507
+
508
+ for (const branch of branches.slice(1)) {
509
+ for (const key of [...candidateKeys]) {
510
+ if (!(branch.properties && key in branch.properties)) {
511
+ candidateKeys.delete(key);
512
+ }
513
+ }
514
+ }
515
+
516
+ const discriminatorCandidates = [...candidateKeys].filter((key) =>
517
+ branches.every((branch) => isDiscriminatorProperty(branch.properties?.[key])),
518
+ );
519
+
520
+ if (discriminatorCandidates.length !== 1) {
521
+ throw new Error(
522
+ `Unsupported union type at ${pathLabel}; expected exactly one shared discriminator property`,
523
+ );
524
+ }
525
+
526
+ return discriminatorCandidates[0];
527
+ }
528
+
529
+ function isDiscriminatorProperty(node: AttributeNode | undefined): boolean {
530
+ return Boolean(
531
+ node &&
532
+ node.required &&
533
+ node.kind === "string" &&
534
+ node.enumValues !== null &&
535
+ node.enumValues.length === 1 &&
536
+ typeof node.enumValues[0] === "string",
537
+ );
538
+ }
539
+
540
+ function parseTypeLiteral(node: ts.TypeLiteralNode, ctx: AnalysisContext, pathLabel: string): AttributeNode {
541
+ const properties: Record<string, AttributeNode> = {};
542
+
543
+ for (const member of node.members) {
544
+ if (!ts.isPropertySignature(member) || member.type === undefined) {
545
+ throw new Error(`Unsupported inline object member at ${pathLabel}`);
546
+ }
547
+
548
+ const propertyName = getPropertyName(member.name);
549
+ properties[propertyName] = withRequired(
550
+ parseTypeNode(member.type, ctx, `${pathLabel}.${propertyName}`),
551
+ member.questionToken === undefined,
552
+ );
553
+ }
554
+
555
+ return {
556
+ constraints: DEFAULT_CONSTRAINTS(),
557
+ enumValues: null,
558
+ kind: "object",
559
+ path: pathLabel,
560
+ properties,
561
+ required: true,
562
+ union: null,
563
+ };
564
+ }
565
+
566
+ function parseLiteralType(node: ts.LiteralTypeNode, pathLabel: string): AttributeNode {
567
+ const literal = extractLiteralValue(node);
568
+ if (literal === undefined) {
569
+ throw new Error(`Unsupported literal type at ${pathLabel}: ${node.getText()}`);
570
+ }
571
+
572
+ return {
573
+ constraints: DEFAULT_CONSTRAINTS(),
574
+ enumValues: [literal],
575
+ kind: typeof literal as "string" | "number" | "boolean",
576
+ path: pathLabel,
577
+ required: true,
578
+ union: null,
579
+ };
580
+ }
581
+
582
+ function parseTypeReference(
583
+ node: ts.TypeReferenceNode | ts.ExpressionWithTypeArguments,
584
+ ctx: AnalysisContext,
585
+ pathLabel: string,
586
+ ): AttributeNode {
587
+ const typeName = getReferenceName(node);
588
+ const typeArguments = node.typeArguments ?? [];
589
+
590
+ if (typeName === "Array" || typeName === "ReadonlyArray") {
591
+ const [itemNode] = typeArguments;
592
+ if (itemNode === undefined) {
593
+ throw new Error(`Array type is missing an item type at ${pathLabel}`);
594
+ }
595
+
596
+ return {
597
+ constraints: DEFAULT_CONSTRAINTS(),
598
+ enumValues: null,
599
+ items: withRequired(parseTypeNode(itemNode, ctx, `${pathLabel}[]`), true),
600
+ kind: "array",
601
+ path: pathLabel,
602
+ required: true,
603
+ union: null,
604
+ };
605
+ }
606
+ if (typeArguments.length > 0) {
607
+ throw new Error(`Generic type references are not supported at ${pathLabel}: ${typeName}`);
608
+ }
609
+
610
+ const symbol = resolveSymbol(node, ctx.checker);
611
+ if (symbol === undefined) {
612
+ throw new Error(`Unable to resolve type reference "${typeName}" at ${pathLabel}`);
613
+ }
614
+
615
+ const declaration = symbol.declarations?.find(
616
+ (candidate) =>
617
+ ts.isInterfaceDeclaration(candidate) ||
618
+ ts.isTypeAliasDeclaration(candidate) ||
619
+ ts.isEnumDeclaration(candidate) ||
620
+ ts.isClassDeclaration(candidate),
621
+ );
622
+ if (declaration === undefined) {
623
+ throw new Error(`Unsupported referenced type "${typeName}" at ${pathLabel}`);
624
+ }
625
+ if (!isSerializableExternalDeclaration(declaration, ctx)) {
626
+ throw new Error(
627
+ `External or non-serializable referenced type "${typeName}" is not supported at ${pathLabel}`,
628
+ );
629
+ }
630
+ if (ts.isClassDeclaration(declaration) || ts.isEnumDeclaration(declaration)) {
631
+ throw new Error(`Class and enum references are not supported at ${pathLabel}`);
632
+ }
633
+ if ((declaration.typeParameters?.length ?? 0) > 0) {
634
+ throw new Error(`Generic type declarations are not supported at ${pathLabel}: ${typeName}`);
635
+ }
636
+
637
+ return parseNamedDeclaration(declaration, ctx, pathLabel, true);
638
+ }
639
+
640
+ function applyTag(node: AttributeNode, tagNode: ts.TypeReferenceNode, pathLabel: string): void {
641
+ const tagName = getSupportedTagName(tagNode);
642
+ if (tagName === null) {
643
+ return;
644
+ }
645
+
646
+ const [arg] = tagNode.typeArguments ?? [];
647
+ if (arg === undefined) {
648
+ throw new Error(`Tag "${tagName}" is missing its generic argument at ${pathLabel}`);
649
+ }
650
+
651
+ switch (tagName) {
652
+ case "Default": {
653
+ const value = parseDefaultValue(arg, pathLabel);
654
+ if (value === undefined) {
655
+ throw new Error(`Unsupported Default value at ${pathLabel}: ${arg.getText()}`);
656
+ }
657
+ node.defaultValue = value;
658
+ return;
659
+ }
660
+ case "Format":
661
+ node.constraints.format = parseStringLikeArgument(arg, tagName, pathLabel);
662
+ return;
663
+ case "Pattern":
664
+ node.constraints.pattern = parseStringLikeArgument(arg, tagName, pathLabel);
665
+ return;
666
+ case "Type":
667
+ node.constraints.typeTag = parseStringLikeArgument(arg, tagName, pathLabel);
668
+ return;
669
+ case "MinLength":
670
+ node.constraints.minLength = parseNumericArgument(arg, tagName, pathLabel);
671
+ return;
672
+ case "MaxLength":
673
+ node.constraints.maxLength = parseNumericArgument(arg, tagName, pathLabel);
674
+ return;
675
+ case "Minimum":
676
+ node.constraints.minimum = parseNumericArgument(arg, tagName, pathLabel);
677
+ return;
678
+ case "Maximum":
679
+ node.constraints.maximum = parseNumericArgument(arg, tagName, pathLabel);
680
+ return;
681
+ default:
682
+ return;
683
+ }
684
+ }
685
+
686
+ function parseDefaultValue(node: ts.TypeNode, pathLabel: string): JsonValue | undefined {
687
+ if (ts.isParenthesizedTypeNode(node)) {
688
+ return parseDefaultValue(node.type, pathLabel);
689
+ }
690
+ if (ts.isLiteralTypeNode(node)) {
691
+ const literal = extractLiteralValue(node);
692
+ return literal === undefined ? undefined : literal;
693
+ }
694
+ if (ts.isTypeLiteralNode(node)) {
695
+ const objectValue: Record<string, JsonValue> = {};
696
+ for (const member of node.members) {
697
+ if (!ts.isPropertySignature(member) || member.type === undefined) {
698
+ throw new Error(`Unsupported object Default value at ${pathLabel}`);
699
+ }
700
+ const propertyName = getPropertyName(member.name);
701
+ const value = parseDefaultValue(member.type, `${pathLabel}.${propertyName}`);
702
+ if (value === undefined) {
703
+ throw new Error(`Unsupported object Default value at ${pathLabel}.${propertyName}`);
704
+ }
705
+ objectValue[propertyName] = value;
706
+ }
707
+ return objectValue;
708
+ }
709
+ if (ts.isTupleTypeNode(node)) {
710
+ return node.elements.map((element, index) => {
711
+ const value = parseDefaultValue(element, `${pathLabel}[${index}]`);
712
+ if (value === undefined) {
713
+ throw new Error(`Unsupported array Default value at ${pathLabel}[${index}]`);
714
+ }
715
+ return value;
716
+ });
717
+ }
718
+ if (node.kind === ts.SyntaxKind.NullKeyword) {
719
+ return null;
720
+ }
721
+ return undefined;
722
+ }
723
+
724
+ function parseNumericArgument(node: ts.TypeNode, tagName: string, pathLabel: string): number {
725
+ const value = extractLiteralValue(node);
726
+ if (typeof value !== "number") {
727
+ throw new Error(`Tag "${tagName}" expects a numeric literal at ${pathLabel}`);
728
+ }
729
+ return value;
730
+ }
731
+
732
+ function parseStringLikeArgument(node: ts.TypeNode, tagName: string, pathLabel: string): string {
733
+ const value = extractLiteralValue(node);
734
+ if (typeof value !== "string") {
735
+ throw new Error(`Tag "${tagName}" expects a string literal at ${pathLabel}`);
736
+ }
737
+ return value;
738
+ }
739
+
740
+ function extractLiteralValue(node: ts.TypeNode): string | number | boolean | undefined {
741
+ if (ts.isParenthesizedTypeNode(node)) {
742
+ return extractLiteralValue(node.type);
743
+ }
744
+ if (node.kind === ts.SyntaxKind.TrueKeyword) {
745
+ return true;
746
+ }
747
+ if (node.kind === ts.SyntaxKind.FalseKeyword) {
748
+ return false;
749
+ }
750
+ if (!ts.isLiteralTypeNode(node)) {
751
+ return undefined;
752
+ }
753
+
754
+ if (ts.isStringLiteral(node.literal)) {
755
+ return node.literal.text;
756
+ }
757
+ if (ts.isNumericLiteral(node.literal)) {
758
+ return Number(node.literal.text);
759
+ }
760
+ if (node.literal.kind === ts.SyntaxKind.TrueKeyword) {
761
+ return true;
762
+ }
763
+ if (node.literal.kind === ts.SyntaxKind.FalseKeyword) {
764
+ return false;
765
+ }
766
+ return undefined;
767
+ }
768
+
769
+ function createBlockJsonAttribute(
770
+ node: AttributeNode,
771
+ warnings: string[],
772
+ ): BlockJsonAttribute {
773
+ const attribute: BlockJsonAttribute = {
774
+ type: getWordPressKind(node),
775
+ };
776
+
777
+ if (node.defaultValue !== undefined) {
778
+ attribute.default = cloneJson(node.defaultValue);
779
+ }
780
+ if (node.enumValues !== null && node.enumValues.length > 0) {
781
+ attribute.enum = [...node.enumValues];
782
+ }
783
+
784
+ const reasons: string[] = [];
785
+ if (node.constraints.format !== null) reasons.push("format");
786
+ if (node.constraints.maxLength !== null) reasons.push("maxLength");
787
+ if (node.constraints.maximum !== null) reasons.push("maximum");
788
+ if (node.constraints.minLength !== null) reasons.push("minLength");
789
+ if (node.constraints.minimum !== null) reasons.push("minimum");
790
+ if (node.constraints.pattern !== null) reasons.push("pattern");
791
+ if (node.constraints.typeTag !== null) reasons.push("typeTag");
792
+ if (node.kind === "array" && node.items !== undefined) reasons.push("items");
793
+ if (node.kind === "object" && node.properties !== undefined) reasons.push("properties");
794
+ if (node.kind === "union" && node.union !== null) reasons.push("union");
795
+
796
+ if (reasons.length > 0) {
797
+ warnings.push(`${node.path}: ${reasons.join(", ")}`);
798
+ }
799
+
800
+ return attribute;
801
+ }
802
+
803
+ function createManifestAttribute(node: AttributeNode): ManifestAttribute {
804
+ return {
805
+ typia: {
806
+ constraints: { ...node.constraints },
807
+ defaultValue: node.defaultValue === undefined ? null : cloneJson(node.defaultValue),
808
+ hasDefault: node.defaultValue !== undefined,
809
+ },
810
+ ts: {
811
+ items: node.items ? createManifestAttribute(node.items) : null,
812
+ kind: node.kind,
813
+ properties: node.properties
814
+ ? Object.fromEntries(
815
+ Object.entries(node.properties).map(([key, property]) => [key, createManifestAttribute(property)]),
816
+ )
817
+ : null,
818
+ required: node.required,
819
+ union: node.union
820
+ ? {
821
+ branches: Object.fromEntries(
822
+ Object.entries(node.union.branches).map(([key, branch]) => [key, createManifestAttribute(branch)]),
823
+ ),
824
+ discriminator: node.union.discriminator,
825
+ }
826
+ : null,
827
+ },
828
+ wp: {
829
+ defaultValue: node.defaultValue === undefined ? null : cloneJson(node.defaultValue),
830
+ enum: node.enumValues ? [...node.enumValues] : null,
831
+ hasDefault: node.defaultValue !== undefined,
832
+ type: getWordPressKind(node),
833
+ },
834
+ };
835
+ }
836
+
837
+ function renderPhpValidator(manifest: ManifestDocument): { source: string; warnings: string[] } {
838
+ const warnings: string[] = [];
839
+
840
+ for (const [key, attribute] of Object.entries(manifest.attributes)) {
841
+ collectPhpGenerationWarnings(attribute, key, warnings);
842
+ }
843
+
844
+ const phpManifest = renderPhpValue(manifest, 2);
845
+
846
+ return {
847
+ source: `<?php
848
+ declare(strict_types=1);
849
+
850
+ /**
851
+ * Generated from typia.manifest.json. Do not edit manually.
852
+ */
853
+ return new class {
854
+ \tprivate array $manifest = ${phpManifest};
855
+
856
+ \tpublic function apply_defaults(array $attributes): array
857
+ \t{
858
+ \t\treturn $this->applyDefaultsForObject($attributes, $this->manifest['attributes'] ?? []);
859
+ \t}
860
+
861
+ \tpublic function validate(array $attributes): array
862
+ \t{
863
+ \t\t$normalized = $this->apply_defaults($attributes);
864
+ \t\t$errors = [];
865
+
866
+ \t\tforeach (($this->manifest['attributes'] ?? []) as $name => $attribute) {
867
+ \t\t\t$this->validateAttribute(
868
+ \t\t\t\tarray_key_exists($name, $normalized),
869
+ \t\t\t\t$normalized[$name] ?? null,
870
+ \t\t\t\t$attribute,
871
+ \t\t\t\t(string) $name,
872
+ \t\t\t\t$errors,
873
+ \t\t\t);
874
+ \t\t}
875
+
876
+ \t\treturn [
877
+ \t\t\t'errors' => $errors,
878
+ \t\t\t'valid' => count($errors) === 0,
879
+ \t\t];
880
+ \t}
881
+
882
+ \tpublic function is_valid(array $attributes): bool
883
+ \t{
884
+ \t\treturn $this->validate($attributes)['valid'];
885
+ \t}
886
+
887
+ \tprivate function applyDefaultsForObject(array $attributes, array $schema): array
888
+ \t{
889
+ \t\t$result = $attributes;
890
+
891
+ \t\tforeach ($schema as $name => $attribute) {
892
+ \t\t\tif (!array_key_exists($name, $result)) {
893
+ \t\t\t\tif ($this->hasDefault($attribute)) {
894
+ \t\t\t\t\t$result[$name] = $attribute['typia']['defaultValue'];
895
+ \t\t\t\t}
896
+ \t\t\t\tcontinue;
897
+ \t\t\t}
898
+
899
+ \t\t\t$result[$name] = $this->applyDefaultsForNode($result[$name], $attribute);
900
+ \t\t}
901
+
902
+ \t\treturn $result;
903
+ \t}
904
+
905
+ \tprivate function applyDefaultsForNode($value, array $attribute)
906
+ \t{
907
+ \t\tif ($value === null) {
908
+ \t\t\treturn null;
909
+ \t\t}
910
+
911
+ \t\t$kind = $attribute['ts']['kind'] ?? $attribute['wp']['type'] ?? null;
912
+ \t\tif ($kind === 'union') {
913
+ \t\t\treturn $this->applyDefaultsForUnion($value, $attribute);
914
+ \t\t}
915
+ \t\tif ($kind === 'object' && is_array($value) && !$this->isListArray($value)) {
916
+ \t\t\treturn $this->applyDefaultsForObject($value, $attribute['ts']['properties'] ?? []);
917
+ \t\t}
918
+ \t\tif (
919
+ \t\t\t$kind === 'array' &&
920
+ \t\t\tis_array($value) &&
921
+ \t\t\t$this->isListArray($value) &&
922
+ \t\t\tisset($attribute['ts']['items']) &&
923
+ \t\t\tis_array($attribute['ts']['items'])
924
+ \t\t) {
925
+ \t\t\t$result = [];
926
+ \t\t\tforeach ($value as $index => $item) {
927
+ \t\t\t\t$result[$index] = $this->applyDefaultsForNode($item, $attribute['ts']['items']);
928
+ \t\t\t}
929
+ \t\t\treturn $result;
930
+ \t\t}
931
+
932
+ \t\treturn $value;
933
+ \t}
934
+
935
+ \tprivate function applyDefaultsForUnion($value, array $attribute)
936
+ \t{
937
+ \t\tif (!is_array($value) || $this->isListArray($value)) {
938
+ \t\t\treturn $value;
939
+ \t\t}
940
+
941
+ \t\t$union = $attribute['ts']['union'] ?? null;
942
+ \t\tif (!is_array($union)) {
943
+ \t\t\treturn $value;
944
+ \t\t}
945
+
946
+ \t\t$discriminator = $union['discriminator'] ?? null;
947
+ \t\tif (!is_string($discriminator) || !array_key_exists($discriminator, $value)) {
948
+ \t\t\treturn $value;
949
+ \t\t}
950
+
951
+ \t\t$branchKey = $value[$discriminator];
952
+ \t\tif (!is_string($branchKey) || !isset($union['branches'][$branchKey]) || !is_array($union['branches'][$branchKey])) {
953
+ \t\t\treturn $value;
954
+ \t\t}
955
+
956
+ \t\treturn $this->applyDefaultsForNode($value, $union['branches'][$branchKey]);
957
+ \t}
958
+
959
+ \tprivate function validateAttribute(bool $exists, $value, array $attribute, string $path, array &$errors): void
960
+ \t{
961
+ \t\tif (!$exists) {
962
+ \t\t\tif (($attribute['ts']['required'] ?? false) && !$this->hasDefault($attribute)) {
963
+ \t\t\t\t$errors[] = sprintf('%s is required', $path);
964
+ \t\t\t}
965
+ \t\t\treturn;
966
+ \t\t}
967
+
968
+ \t\t$kind = $attribute['ts']['kind'] ?? $attribute['wp']['type'] ?? null;
969
+ \t\tif (!is_string($kind) || $kind === '') {
970
+ \t\t\t$errors[] = sprintf('%s has an invalid schema kind', $path);
971
+ \t\t\treturn;
972
+ \t\t}
973
+ \t\tif ($value === null) {
974
+ \t\t\t$errors[] = sprintf('%s must be %s', $path, $this->expectedKindLabel($attribute));
975
+ \t\t\treturn;
976
+ \t\t}
977
+
978
+ \t\tif (($attribute['wp']['enum'] ?? null) !== null && !$this->valueInEnum($value, $attribute['wp']['enum'])) {
979
+ \t\t\t$errors[] = sprintf('%s must be one of %s', $path, implode(', ', $attribute['wp']['enum']));
980
+ \t\t}
981
+
982
+ \t\tswitch ($kind) {
983
+ \t\t\tcase 'string':
984
+ \t\t\t\tif (!is_string($value)) {
985
+ \t\t\t\t\t$errors[] = sprintf('%s must be string', $path);
986
+ \t\t\t\t\treturn;
987
+ \t\t\t\t}
988
+ \t\t\t\t$this->validateString($value, $attribute, $path, $errors);
989
+ \t\t\t\treturn;
990
+ \t\t\tcase 'number':
991
+ \t\t\t\tif (!$this->isNumber($value)) {
992
+ \t\t\t\t\t$errors[] = sprintf('%s must be number', $path);
993
+ \t\t\t\t\treturn;
994
+ \t\t\t\t}
995
+ \t\t\t\t$this->validateNumber($value, $attribute, $path, $errors);
996
+ \t\t\t\treturn;
997
+ \t\t\tcase 'boolean':
998
+ \t\t\t\tif (!is_bool($value)) {
999
+ \t\t\t\t\t$errors[] = sprintf('%s must be boolean', $path);
1000
+ \t\t\t\t}
1001
+ \t\t\t\treturn;
1002
+ \t\t\tcase 'array':
1003
+ \t\t\t\tif (!is_array($value) || !$this->isListArray($value)) {
1004
+ \t\t\t\t\t$errors[] = sprintf('%s must be array', $path);
1005
+ \t\t\t\t\treturn;
1006
+ \t\t\t\t}
1007
+ \t\t\t\tif (isset($attribute['ts']['items']) && is_array($attribute['ts']['items'])) {
1008
+ \t\t\t\t\tforeach ($value as $index => $item) {
1009
+ \t\t\t\t\t\t$this->validateAttribute(true, $item, $attribute['ts']['items'], sprintf('%s[%s]', $path, (string) $index), $errors);
1010
+ \t\t\t\t\t}
1011
+ \t\t\t\t}
1012
+ \t\t\t\treturn;
1013
+ \t\t\tcase 'object':
1014
+ \t\t\t\tif (!is_array($value) || $this->isListArray($value)) {
1015
+ \t\t\t\t\t$errors[] = sprintf('%s must be object', $path);
1016
+ \t\t\t\t\treturn;
1017
+ \t\t\t\t}
1018
+ \t\t\t\tforeach (($attribute['ts']['properties'] ?? []) as $name => $child) {
1019
+ \t\t\t\t\t$this->validateAttribute(
1020
+ \t\t\t\t\t\tarray_key_exists($name, $value),
1021
+ \t\t\t\t\t\t$value[$name] ?? null,
1022
+ \t\t\t\t\t\t$child,
1023
+ \t\t\t\t\t\tsprintf('%s.%s', $path, (string) $name),
1024
+ \t\t\t\t\t\t$errors,
1025
+ \t\t\t\t\t);
1026
+ \t\t\t\t}
1027
+ \t\t\t\treturn;
1028
+ \t\t\tcase 'union':
1029
+ \t\t\t\t$this->validateUnion($value, $attribute, $path, $errors);
1030
+ \t\t\t\treturn;
1031
+ \t\t\tdefault:
1032
+ \t\t\t\t$errors[] = sprintf('%s has unsupported schema kind %s', $path, $kind);
1033
+ \t\t}
1034
+ \t}
1035
+
1036
+ \tprivate function validateUnion($value, array $attribute, string $path, array &$errors): void
1037
+ \t{
1038
+ \t\tif (!is_array($value) || $this->isListArray($value)) {
1039
+ \t\t\t$errors[] = sprintf('%s must be object', $path);
1040
+ \t\t\treturn;
1041
+ \t\t}
1042
+
1043
+ \t\t$union = $attribute['ts']['union'] ?? null;
1044
+ \t\tif (!is_array($union)) {
1045
+ \t\t\t$errors[] = sprintf('%s has invalid union schema metadata', $path);
1046
+ \t\t\treturn;
1047
+ \t\t}
1048
+
1049
+ \t\t$discriminator = $union['discriminator'] ?? null;
1050
+ \t\tif (!is_string($discriminator) || $discriminator === '') {
1051
+ \t\t\t$errors[] = sprintf('%s has invalid union discriminator metadata', $path);
1052
+ \t\t\treturn;
1053
+ \t\t}
1054
+ \t\tif (!array_key_exists($discriminator, $value)) {
1055
+ \t\t\t$errors[] = sprintf('%s.%s is required', $path, $discriminator);
1056
+ \t\t\treturn;
1057
+ \t\t}
1058
+
1059
+ \t\t$branchKey = $value[$discriminator];
1060
+ \t\tif (!is_string($branchKey)) {
1061
+ \t\t\t$errors[] = sprintf('%s.%s must be string', $path, $discriminator);
1062
+ \t\t\treturn;
1063
+ \t\t}
1064
+ \t\tif (!isset($union['branches'][$branchKey]) || !is_array($union['branches'][$branchKey])) {
1065
+ \t\t\t$errors[] = sprintf('%s.%s must be one of %s', $path, $discriminator, implode(', ', array_keys($union['branches'] ?? [])));
1066
+ \t\t\treturn;
1067
+ \t\t}
1068
+
1069
+ \t\t$this->validateAttribute(true, $value, $union['branches'][$branchKey], $path, $errors);
1070
+ \t}
1071
+
1072
+ \tprivate function validateString(string $value, array $attribute, string $path, array &$errors): void
1073
+ \t{
1074
+ \t\t$constraints = $attribute['typia']['constraints'] ?? [];
1075
+
1076
+ \t\tif (isset($constraints['minLength']) && is_int($constraints['minLength']) && strlen($value) < $constraints['minLength']) {
1077
+ \t\t\t$errors[] = sprintf('%s must be at least %d characters', $path, $constraints['minLength']);
1078
+ \t\t}
1079
+ \t\tif (isset($constraints['maxLength']) && is_int($constraints['maxLength']) && strlen($value) > $constraints['maxLength']) {
1080
+ \t\t\t$errors[] = sprintf('%s must be at most %d characters', $path, $constraints['maxLength']);
1081
+ \t\t}
1082
+ \t\tif (
1083
+ \t\t\tisset($constraints['pattern']) &&
1084
+ \t\t\tis_string($constraints['pattern']) &&
1085
+ \t\t\t$constraints['pattern'] !== '' &&
1086
+ \t\t\t!$this->matchesPattern($constraints['pattern'], $value)
1087
+ \t\t) {
1088
+ \t\t\t$errors[] = sprintf('%s does not match %s', $path, $constraints['pattern']);
1089
+ \t\t}
1090
+ \t\tif (
1091
+ \t\t\tisset($constraints['format']) &&
1092
+ \t\t\t$constraints['format'] === 'uuid' &&
1093
+ \t\t\t!$this->matchesUuid($value)
1094
+ \t\t) {
1095
+ \t\t\t$errors[] = sprintf('%s must be a uuid', $path);
1096
+ \t\t}
1097
+ \t}
1098
+
1099
+ \tprivate function validateNumber($value, array $attribute, string $path, array &$errors): void
1100
+ \t{
1101
+ \t\t$constraints = $attribute['typia']['constraints'] ?? [];
1102
+
1103
+ \t\tif (isset($constraints['minimum']) && $this->isNumber($constraints['minimum']) && $value < $constraints['minimum']) {
1104
+ \t\t\t$errors[] = sprintf('%s must be >= %s', $path, (string) $constraints['minimum']);
1105
+ \t\t}
1106
+ \t\tif (isset($constraints['maximum']) && $this->isNumber($constraints['maximum']) && $value > $constraints['maximum']) {
1107
+ \t\t\t$errors[] = sprintf('%s must be <= %s', $path, (string) $constraints['maximum']);
1108
+ \t\t}
1109
+ \t\tif (($constraints['typeTag'] ?? null) === 'uint32') {
1110
+ \t\t\tif (!is_int($value) || $value < 0 || $value > 4294967295) {
1111
+ \t\t\t\t$errors[] = sprintf('%s must be a uint32', $path);
1112
+ \t\t\t}
1113
+ \t\t}
1114
+ \t}
1115
+
1116
+ \tprivate function hasDefault(array $attribute): bool
1117
+ \t{
1118
+ \t\treturn ($attribute['typia']['hasDefault'] ?? false) === true;
1119
+ \t}
1120
+
1121
+ \tprivate function valueInEnum($value, array $enum): bool
1122
+ \t{
1123
+ \t\tforeach ($enum as $candidate) {
1124
+ \t\t\tif ($candidate === $value) {
1125
+ \t\t\t\treturn true;
1126
+ \t\t\t}
1127
+ \t\t}
1128
+ \t\treturn false;
1129
+ \t}
1130
+
1131
+ \tprivate function matchesPattern(string $pattern, string $value): bool
1132
+ \t{
1133
+ \t\t$escapedPattern = str_replace('~', '\\\\~', $pattern);
1134
+ \t\t$result = @preg_match('~' . $escapedPattern . '~u', $value);
1135
+ \t\treturn $result === 1;
1136
+ \t}
1137
+
1138
+ \tprivate function matchesUuid(string $value): bool
1139
+ \t{
1140
+ \t\treturn preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i', $value) === 1;
1141
+ \t}
1142
+
1143
+ \tprivate function isNumber($value): bool
1144
+ \t{
1145
+ \t\treturn is_int($value) || is_float($value);
1146
+ \t}
1147
+
1148
+ \tprivate function isListArray(array $value): bool
1149
+ \t{
1150
+ \t\t$expectedKey = 0;
1151
+ \t\tforeach ($value as $key => $_item) {
1152
+ \t\t\tif ($key !== $expectedKey) {
1153
+ \t\t\t\treturn false;
1154
+ \t\t\t}
1155
+ \t\t\t$expectedKey += 1;
1156
+ \t\t}
1157
+ \t\treturn true;
1158
+ \t}
1159
+
1160
+ \tprivate function expectedKindLabel(array $attribute): string
1161
+ \t{
1162
+ \t\t$kind = $attribute['ts']['kind'] ?? $attribute['wp']['type'] ?? 'value';
1163
+ \t\treturn $kind === 'union' ? 'object' : (string) $kind;
1164
+ \t}
1165
+ };
1166
+ `,
1167
+ warnings,
1168
+ };
1169
+ }
1170
+
1171
+ function collectPhpGenerationWarnings(
1172
+ attribute: ManifestAttribute,
1173
+ pathLabel: string,
1174
+ warnings: string[],
1175
+ ): void {
1176
+ const { format, typeTag } = attribute.typia.constraints;
1177
+ if (format !== null && format !== "uuid") {
1178
+ warnings.push(`${pathLabel}: unsupported PHP validator format "${format}"`);
1179
+ }
1180
+ if (typeTag !== null && typeTag !== "uint32") {
1181
+ warnings.push(`${pathLabel}: unsupported PHP validator type tag "${typeTag}"`);
1182
+ }
1183
+
1184
+ if (attribute.ts.items) {
1185
+ collectPhpGenerationWarnings(attribute.ts.items, `${pathLabel}[]`, warnings);
1186
+ }
1187
+ for (const [key, property] of Object.entries(attribute.ts.properties ?? {})) {
1188
+ collectPhpGenerationWarnings(property, `${pathLabel}.${key}`, warnings);
1189
+ }
1190
+ for (const [branchKey, branch] of Object.entries(attribute.ts.union?.branches ?? {})) {
1191
+ collectPhpGenerationWarnings(branch, `${pathLabel}<${branchKey}>`, warnings);
1192
+ }
1193
+ }
1194
+
1195
+ function renderPhpValue(value: unknown, indentLevel: number): string {
1196
+ const indent = "\t".repeat(indentLevel);
1197
+ const nestedIndent = "\t".repeat(indentLevel + 1);
1198
+
1199
+ if (value === null) {
1200
+ return "null";
1201
+ }
1202
+ if (typeof value === "string") {
1203
+ return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
1204
+ }
1205
+ if (typeof value === "number" || typeof value === "boolean") {
1206
+ return String(value);
1207
+ }
1208
+ if (Array.isArray(value)) {
1209
+ if (value.length === 0) {
1210
+ return "[]";
1211
+ }
1212
+ const items = value.map((item) => `${nestedIndent}${renderPhpValue(item, indentLevel + 1)}`);
1213
+ return `[\n${items.join(",\n")}\n${indent}]`;
1214
+ }
1215
+ if (typeof value === "object") {
1216
+ const entries = Object.entries(value as Record<string, unknown>);
1217
+ if (entries.length === 0) {
1218
+ return "[]";
1219
+ }
1220
+ const items = entries.map(
1221
+ ([key, item]) =>
1222
+ `${nestedIndent}'${key.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}' => ${renderPhpValue(item, indentLevel + 1)}`,
1223
+ );
1224
+ return `[\n${items.join(",\n")}\n${indent}]`;
1225
+ }
1226
+
1227
+ throw new Error(`Unable to encode PHP value for manifest node: ${String(value)}`);
1228
+ }
1229
+
1230
+ function createExampleValue(node: AttributeNode, key: string): JsonValue {
1231
+ if (node.defaultValue !== undefined) {
1232
+ return cloneJson(node.defaultValue);
1233
+ }
1234
+ if (node.enumValues !== null && node.enumValues.length > 0) {
1235
+ return cloneJson(node.enumValues[0]);
1236
+ }
1237
+
1238
+ switch (node.kind) {
1239
+ case "string":
1240
+ if (node.constraints.format === "uuid") {
1241
+ return "00000000-0000-4000-8000-000000000000";
1242
+ }
1243
+ return `Example ${key}`;
1244
+ case "number":
1245
+ return node.constraints.minimum ?? 42;
1246
+ case "boolean":
1247
+ return true;
1248
+ case "array":
1249
+ return [];
1250
+ case "object":
1251
+ return Object.fromEntries(
1252
+ Object.entries(node.properties ?? {}).map(([propertyKey, propertyNode]) => [
1253
+ propertyKey,
1254
+ createExampleValue(propertyNode, propertyKey),
1255
+ ]),
1256
+ );
1257
+ case "union": {
1258
+ const firstBranch = node.union ? Object.values(node.union.branches)[0] : undefined;
1259
+ if (!firstBranch || firstBranch.kind !== "object") {
1260
+ return {};
1261
+ }
1262
+ return Object.fromEntries(
1263
+ Object.entries(firstBranch.properties ?? {}).map(([propertyKey, propertyNode]) => [
1264
+ propertyKey,
1265
+ createExampleValue(propertyNode, propertyKey),
1266
+ ]),
1267
+ );
1268
+ }
1269
+ }
1270
+ }
1271
+
1272
+ function getWordPressKind(node: AttributeNode): WordPressAttributeKind {
1273
+ return node.kind === "union" ? "object" : node.kind;
1274
+ }
1275
+
1276
+ function baseNode(kind: AttributeKind, pathLabel: string): AttributeNode {
1277
+ return {
1278
+ constraints: DEFAULT_CONSTRAINTS(),
1279
+ enumValues: null,
1280
+ kind,
1281
+ path: pathLabel,
1282
+ required: true,
1283
+ union: null,
1284
+ };
1285
+ }
1286
+
1287
+ function withRequired(node: AttributeNode, required: boolean): AttributeNode {
1288
+ return {
1289
+ ...node,
1290
+ items: node.items ? withRequired(node.items, node.items.required) : undefined,
1291
+ properties: node.properties ? cloneProperties(node.properties) : undefined,
1292
+ required,
1293
+ union: node.union ? cloneUnion(node.union) : null,
1294
+ };
1295
+ }
1296
+
1297
+ function cloneUnion(union: AttributeUnion): AttributeUnion {
1298
+ return {
1299
+ branches: Object.fromEntries(
1300
+ Object.entries(union.branches).map(([key, branch]) => [key, withRequired(branch, branch.required)]),
1301
+ ),
1302
+ discriminator: union.discriminator,
1303
+ };
1304
+ }
1305
+
1306
+ function cloneProperties(properties: Record<string, AttributeNode>): Record<string, AttributeNode> {
1307
+ return Object.fromEntries(
1308
+ Object.entries(properties).map(([key, node]) => [key, withRequired(node, node.required)]),
1309
+ );
1310
+ }
1311
+
1312
+ function cloneJson<T extends JsonValue | Array<string | number | boolean>>(value: T): T {
1313
+ return JSON.parse(JSON.stringify(value)) as T;
1314
+ }
1315
+
1316
+ function getPropertyName(name: ts.PropertyName): string {
1317
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
1318
+ return name.text;
1319
+ }
1320
+ throw new Error(`Unsupported property name: ${name.getText()}`);
1321
+ }
1322
+
1323
+ function getSupportedTagName(node: ts.TypeReferenceNode): string | null {
1324
+ const typeName = getEntityNameText(node.typeName);
1325
+ const [, tagName] = typeName.split(".");
1326
+ if (!typeName.startsWith("tags.") || tagName === undefined || !SUPPORTED_TAGS.has(tagName)) {
1327
+ return null;
1328
+ }
1329
+ return tagName;
1330
+ }
1331
+
1332
+ function getEntityNameText(name: ts.EntityName): string {
1333
+ if (ts.isIdentifier(name)) {
1334
+ return name.text;
1335
+ }
1336
+ return `${getEntityNameText(name.left)}.${name.right.text}`;
1337
+ }
1338
+
1339
+ function resolveSymbol(
1340
+ node: ts.TypeReferenceNode | ts.ExpressionWithTypeArguments,
1341
+ checker: ts.TypeChecker,
1342
+ ): ts.Symbol | undefined {
1343
+ const symbol = checker.getSymbolAtLocation(
1344
+ ts.isTypeReferenceNode(node) ? node.typeName : node.expression,
1345
+ );
1346
+ if (symbol === undefined) {
1347
+ return undefined;
1348
+ }
1349
+ return symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
1350
+ }
1351
+
1352
+ function getReferenceName(node: ts.TypeReferenceNode | ts.ExpressionWithTypeArguments): string {
1353
+ if (ts.isTypeReferenceNode(node)) {
1354
+ return getEntityNameText(node.typeName);
1355
+ }
1356
+ return node.expression.getText();
1357
+ }
1358
+
1359
+ function isProjectLocalDeclaration(declaration: ts.Declaration, projectRoot: string): boolean {
1360
+ const fileName = declaration.getSourceFile().fileName;
1361
+ return !fileName.includes("node_modules") && !path.relative(projectRoot, fileName).startsWith("..");
1362
+ }
1363
+
1364
+ function isSerializableExternalDeclaration(declaration: ts.Declaration, ctx: AnalysisContext): boolean {
1365
+ if (isProjectLocalDeclaration(declaration, ctx.projectRoot)) {
1366
+ return true;
1367
+ }
1368
+
1369
+ const packageName = getOwningPackageName(declaration.getSourceFile().fileName, ctx.packageNameCache);
1370
+ return packageName !== null && ctx.allowedExternalPackages.has(packageName);
1371
+ }
1372
+
1373
+ function getOwningPackageName(
1374
+ fileName: string,
1375
+ cache: Map<string, string | null>,
1376
+ ): string | null {
1377
+ let currentDir = path.dirname(fileName);
1378
+
1379
+ while (true) {
1380
+ if (cache.has(currentDir)) {
1381
+ return cache.get(currentDir) ?? null;
1382
+ }
1383
+
1384
+ const packageJsonPath = path.join(currentDir, "package.json");
1385
+ if (fs.existsSync(packageJsonPath)) {
1386
+ try {
1387
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as {
1388
+ name?: string;
1389
+ };
1390
+ const packageName = typeof packageJson.name === "string" ? packageJson.name : null;
1391
+ cache.set(currentDir, packageName);
1392
+ return packageName;
1393
+ } catch {
1394
+ cache.set(currentDir, null);
1395
+ return null;
1396
+ }
1397
+ }
1398
+
1399
+ const parentDir = path.dirname(currentDir);
1400
+ if (parentDir === currentDir) {
1401
+ cache.set(currentDir, null);
1402
+ return null;
1403
+ }
1404
+
1405
+ currentDir = parentDir;
1406
+ }
1407
+ }
1408
+
1409
+ function formatDiagnosticError(diagnostic: ts.Diagnostic): Error {
1410
+ return new Error(
1411
+ ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"),
1412
+ );
1413
+ }