@wp-typia/project-tools 0.11.1

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 (187) hide show
  1. package/README.md +32 -0
  2. package/dist/runtime/cli-add.d.ts +38 -0
  3. package/dist/runtime/cli-add.js +561 -0
  4. package/dist/runtime/cli-core.d.ts +25 -0
  5. package/dist/runtime/cli-core.js +25 -0
  6. package/dist/runtime/cli-doctor.d.ts +34 -0
  7. package/dist/runtime/cli-doctor.js +131 -0
  8. package/dist/runtime/cli-help.d.ts +9 -0
  9. package/dist/runtime/cli-help.js +37 -0
  10. package/dist/runtime/cli-prompt.d.ts +21 -0
  11. package/dist/runtime/cli-prompt.js +53 -0
  12. package/dist/runtime/cli-scaffold.d.ts +79 -0
  13. package/dist/runtime/cli-scaffold.js +206 -0
  14. package/dist/runtime/cli-templates.d.ts +30 -0
  15. package/dist/runtime/cli-templates.js +61 -0
  16. package/dist/runtime/index.d.ts +9 -0
  17. package/dist/runtime/index.js +7 -0
  18. package/dist/runtime/json-utils.d.ts +10 -0
  19. package/dist/runtime/json-utils.js +12 -0
  20. package/dist/runtime/local-dev-presets.d.ts +26 -0
  21. package/dist/runtime/local-dev-presets.js +132 -0
  22. package/dist/runtime/metadata-analysis.d.ts +11 -0
  23. package/dist/runtime/metadata-analysis.js +285 -0
  24. package/dist/runtime/metadata-model.d.ts +84 -0
  25. package/dist/runtime/metadata-model.js +59 -0
  26. package/dist/runtime/metadata-parser.d.ts +53 -0
  27. package/dist/runtime/metadata-parser.js +794 -0
  28. package/dist/runtime/metadata-php-render.d.ts +29 -0
  29. package/dist/runtime/metadata-php-render.js +549 -0
  30. package/dist/runtime/metadata-projection.d.ts +7 -0
  31. package/dist/runtime/metadata-projection.js +233 -0
  32. package/dist/runtime/migration-constants.d.ts +15 -0
  33. package/dist/runtime/migration-constants.js +16 -0
  34. package/dist/runtime/migration-diff.d.ts +2 -0
  35. package/dist/runtime/migration-diff.js +537 -0
  36. package/dist/runtime/migration-fixtures.d.ts +8 -0
  37. package/dist/runtime/migration-fixtures.js +94 -0
  38. package/dist/runtime/migration-fuzz-plan.d.ts +2 -0
  39. package/dist/runtime/migration-fuzz-plan.js +50 -0
  40. package/dist/runtime/migration-manifest.d.ts +19 -0
  41. package/dist/runtime/migration-manifest.js +129 -0
  42. package/dist/runtime/migration-project.d.ts +94 -0
  43. package/dist/runtime/migration-project.js +1101 -0
  44. package/dist/runtime/migration-render.d.ts +11 -0
  45. package/dist/runtime/migration-render.js +741 -0
  46. package/dist/runtime/migration-risk.d.ts +4 -0
  47. package/dist/runtime/migration-risk.js +52 -0
  48. package/dist/runtime/migration-types.d.ts +249 -0
  49. package/dist/runtime/migration-types.js +1 -0
  50. package/dist/runtime/migration-ui-capability.d.ts +17 -0
  51. package/dist/runtime/migration-ui-capability.js +190 -0
  52. package/dist/runtime/migration-utils.d.ts +69 -0
  53. package/dist/runtime/migration-utils.js +246 -0
  54. package/dist/runtime/migrations.d.ts +249 -0
  55. package/dist/runtime/migrations.js +1061 -0
  56. package/dist/runtime/object-utils.d.ts +12 -0
  57. package/dist/runtime/object-utils.js +14 -0
  58. package/dist/runtime/package-managers.d.ts +28 -0
  59. package/dist/runtime/package-managers.js +156 -0
  60. package/dist/runtime/package-versions.d.ts +10 -0
  61. package/dist/runtime/package-versions.js +68 -0
  62. package/dist/runtime/scaffold-onboarding.d.ts +32 -0
  63. package/dist/runtime/scaffold-onboarding.js +99 -0
  64. package/dist/runtime/scaffold.d.ts +146 -0
  65. package/dist/runtime/scaffold.js +612 -0
  66. package/dist/runtime/schema-core.d.ts +267 -0
  67. package/dist/runtime/schema-core.js +597 -0
  68. package/dist/runtime/starter-manifests.d.ts +25 -0
  69. package/dist/runtime/starter-manifests.js +383 -0
  70. package/dist/runtime/string-case.d.ts +36 -0
  71. package/dist/runtime/string-case.js +69 -0
  72. package/dist/runtime/template-builtins.d.ts +38 -0
  73. package/dist/runtime/template-builtins.js +72 -0
  74. package/dist/runtime/template-defaults.d.ts +75 -0
  75. package/dist/runtime/template-defaults.js +65 -0
  76. package/dist/runtime/template-registry.d.ts +36 -0
  77. package/dist/runtime/template-registry.js +94 -0
  78. package/dist/runtime/template-render.d.ts +24 -0
  79. package/dist/runtime/template-render.js +113 -0
  80. package/dist/runtime/template-source.d.ts +71 -0
  81. package/dist/runtime/template-source.js +821 -0
  82. package/dist/runtime/typia-tags.d.ts +1 -0
  83. package/dist/runtime/typia-tags.js +1 -0
  84. package/package.json +79 -0
  85. package/templates/_shared/base/languages/.gitkeep +1 -0
  86. package/templates/_shared/base/package.json.mustache +41 -0
  87. package/templates/_shared/base/scripts/sync-types-to-block-json.ts.mustache +118 -0
  88. package/templates/_shared/base/src/hooks.ts.mustache +19 -0
  89. package/templates/_shared/base/src/validator-toolkit.ts.mustache +31 -0
  90. package/templates/_shared/base/tsconfig.json.mustache +21 -0
  91. package/templates/_shared/base/webpack.config.js.mustache +99 -0
  92. package/templates/_shared/base/{{slugKebabCase}}.php.mustache +53 -0
  93. package/templates/_shared/compound/core/package.json.mustache +45 -0
  94. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +559 -0
  95. package/templates/_shared/compound/core/scripts/block-config.ts.mustache +13 -0
  96. package/templates/_shared/compound/core/scripts/sync-types-to-block-json.ts.mustache +53 -0
  97. package/templates/_shared/compound/core/webpack.config.js.mustache +141 -0
  98. package/templates/_shared/compound/core/{{slugKebabCase}}.php.mustache +51 -0
  99. package/templates/_shared/compound/persistence/package.json.mustache +50 -0
  100. package/templates/_shared/compound/persistence/scripts/block-config.ts.mustache +59 -0
  101. package/templates/_shared/compound/persistence/scripts/sync-rest-contracts.ts.mustache +101 -0
  102. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api-types.ts.mustache +21 -0
  103. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api-validators.ts.mustache +32 -0
  104. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api.ts.mustache +68 -0
  105. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/block.json.mustache +52 -0
  106. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/data.ts.mustache +192 -0
  107. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/edit.tsx.mustache +123 -0
  108. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/hooks.ts.mustache +11 -0
  109. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/interactivity.ts.mustache +132 -0
  110. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/render.php.mustache +158 -0
  111. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/save.tsx.mustache +3 -0
  112. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/types.ts.mustache +56 -0
  113. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/validators.ts.mustache +32 -0
  114. package/templates/_shared/compound/persistence-auth/{{slugKebabCase}}.php.mustache +294 -0
  115. package/templates/_shared/compound/persistence-public/{{slugKebabCase}}.php.mustache +312 -0
  116. package/templates/_shared/migration-ui/common/src/admin/migration-dashboard.tsx +394 -0
  117. package/templates/_shared/migration-ui/common/src/migration-detector.ts +9 -0
  118. package/templates/_shared/migration-ui/common/src/migrations/helpers.ts +490 -0
  119. package/templates/_shared/migration-ui/common/src/migrations/index.ts +886 -0
  120. package/templates/_shared/persistence/auth/{{slugKebabCase}}.php.mustache +290 -0
  121. package/templates/_shared/persistence/core/package.json.mustache +46 -0
  122. package/templates/_shared/persistence/core/scripts/sync-rest-contracts.ts.mustache +113 -0
  123. package/templates/_shared/persistence/core/scripts/sync-types-to-block-json.ts.mustache +125 -0
  124. package/templates/_shared/persistence/core/src/api-types.ts.mustache +21 -0
  125. package/templates/_shared/persistence/core/src/api-validators.ts.mustache +32 -0
  126. package/templates/_shared/persistence/core/src/api.ts.mustache +68 -0
  127. package/templates/_shared/persistence/core/src/data.ts.mustache +192 -0
  128. package/templates/_shared/persistence/core/src/index.tsx.mustache +25 -0
  129. package/templates/_shared/persistence/core/src/interactivity.ts.mustache +134 -0
  130. package/templates/_shared/persistence/core/src/save.tsx.mustache +5 -0
  131. package/templates/_shared/persistence/core/src/validators.ts.mustache +32 -0
  132. package/templates/_shared/persistence/core/{{slugKebabCase}}.php.mustache +336 -0
  133. package/templates/_shared/persistence/public/{{slugKebabCase}}.php.mustache +308 -0
  134. package/templates/_shared/presets/test-preset/.wp-env.test.json.mustache +16 -0
  135. package/templates/_shared/presets/test-preset/playwright.config.ts.mustache +22 -0
  136. package/templates/_shared/presets/test-preset/scripts/wait-for-wp-env.mjs.mustache +102 -0
  137. package/templates/_shared/presets/test-preset/scripts/wp-env-utils.cjs.mustache +32 -0
  138. package/templates/_shared/presets/test-preset/tests/e2e/smoke.spec.ts.mustache +34 -0
  139. package/templates/_shared/presets/wp-env/.wp-env.json.mustache +16 -0
  140. package/templates/_shared/rest-helpers/auth/inc/rest-auth.php.mustache +37 -0
  141. package/templates/_shared/rest-helpers/public/inc/rest-public.php.mustache +314 -0
  142. package/templates/_shared/rest-helpers/shared/inc/rest-shared.php.mustache +58 -0
  143. package/templates/_shared/workspace/persistence-auth/inc/rest-auth.php.mustache +36 -0
  144. package/templates/_shared/workspace/persistence-auth/inc/rest-shared.php.mustache +55 -0
  145. package/templates/_shared/workspace/persistence-auth/server.php.mustache +237 -0
  146. package/templates/_shared/workspace/persistence-public/inc/rest-public.php.mustache +273 -0
  147. package/templates/_shared/workspace/persistence-public/inc/rest-shared.php.mustache +55 -0
  148. package/templates/_shared/workspace/persistence-public/server.php.mustache +252 -0
  149. package/templates/basic/src/block.json.mustache +51 -0
  150. package/templates/basic/src/edit.tsx.mustache +128 -0
  151. package/templates/basic/src/editor.scss.mustache +8 -0
  152. package/templates/basic/src/hooks.ts.mustache +18 -0
  153. package/templates/basic/src/index.tsx.mustache +45 -0
  154. package/templates/basic/src/save.tsx.mustache +30 -0
  155. package/templates/basic/src/style.scss.mustache +40 -0
  156. package/templates/basic/src/types.ts.mustache +56 -0
  157. package/templates/basic/src/validators.ts.mustache +26 -0
  158. package/templates/compound/src/blocks/{{slugKebabCase}}/block.json.mustache +37 -0
  159. package/templates/compound/src/blocks/{{slugKebabCase}}/children.ts.mustache +25 -0
  160. package/templates/compound/src/blocks/{{slugKebabCase}}/edit.tsx.mustache +93 -0
  161. package/templates/compound/src/blocks/{{slugKebabCase}}/hooks.ts.mustache +11 -0
  162. package/templates/compound/src/blocks/{{slugKebabCase}}/index.tsx.mustache +25 -0
  163. package/templates/compound/src/blocks/{{slugKebabCase}}/save.tsx.mustache +32 -0
  164. package/templates/compound/src/blocks/{{slugKebabCase}}/style.scss.mustache +31 -0
  165. package/templates/compound/src/blocks/{{slugKebabCase}}/types.ts.mustache +13 -0
  166. package/templates/compound/src/blocks/{{slugKebabCase}}/validators.ts.mustache +17 -0
  167. package/templates/compound/src/blocks/{{slugKebabCase}}-item/block.json.mustache +35 -0
  168. package/templates/compound/src/blocks/{{slugKebabCase}}-item/edit.tsx.mustache +50 -0
  169. package/templates/compound/src/blocks/{{slugKebabCase}}-item/hooks.ts.mustache +11 -0
  170. package/templates/compound/src/blocks/{{slugKebabCase}}-item/index.tsx.mustache +25 -0
  171. package/templates/compound/src/blocks/{{slugKebabCase}}-item/save.tsx.mustache +24 -0
  172. package/templates/compound/src/blocks/{{slugKebabCase}}-item/types.ts.mustache +12 -0
  173. package/templates/compound/src/blocks/{{slugKebabCase}}-item/validators.ts.mustache +17 -0
  174. package/templates/interactivity/package.json.mustache +42 -0
  175. package/templates/interactivity/src/block.json.mustache +73 -0
  176. package/templates/interactivity/src/edit.tsx.mustache +270 -0
  177. package/templates/interactivity/src/index.tsx.mustache +32 -0
  178. package/templates/interactivity/src/interactivity.ts.mustache +152 -0
  179. package/templates/interactivity/src/save.tsx.mustache +101 -0
  180. package/templates/interactivity/src/style.scss.mustache +60 -0
  181. package/templates/interactivity/src/types.ts.mustache +32 -0
  182. package/templates/interactivity/src/validators.ts.mustache +36 -0
  183. package/templates/persistence/src/block.json.mustache +52 -0
  184. package/templates/persistence/src/edit.tsx.mustache +165 -0
  185. package/templates/persistence/src/render.php.mustache +126 -0
  186. package/templates/persistence/src/style.scss.mustache +46 -0
  187. package/templates/persistence/src/types.ts.mustache +55 -0
@@ -0,0 +1,490 @@
1
+ export interface ManifestUnion {
2
+ branches: Record< string, ManifestAttribute >;
3
+ discriminator: string;
4
+ }
5
+
6
+ export interface ManifestAttribute {
7
+ typia: {
8
+ constraints: {
9
+ format: string | null;
10
+ maxLength: number | null;
11
+ maximum: number | null;
12
+ minLength: number | null;
13
+ minimum: number | null;
14
+ pattern: string | null;
15
+ typeTag: string | null;
16
+ };
17
+ defaultValue: unknown;
18
+ hasDefault: boolean;
19
+ };
20
+ ts: {
21
+ items: ManifestAttribute | null;
22
+ kind: 'string' | 'number' | 'boolean' | 'array' | 'object' | 'union';
23
+ properties: Record< string, ManifestAttribute > | null;
24
+ required: boolean;
25
+ union: ManifestUnion | null;
26
+ };
27
+ wp: {
28
+ defaultValue: unknown;
29
+ enum: Array< string | number | boolean > | null;
30
+ hasDefault: boolean;
31
+ type: 'string' | 'number' | 'boolean' | 'array' | 'object';
32
+ };
33
+ }
34
+
35
+ export interface ManifestDocument {
36
+ attributes: Record< string, ManifestAttribute >;
37
+ manifestVersion: 2;
38
+ sourceType: string;
39
+ }
40
+
41
+ export interface MigrationRiskBucket {
42
+ count: number;
43
+ items: string[];
44
+ }
45
+
46
+ export interface MigrationRiskSummary {
47
+ additive: MigrationRiskBucket;
48
+ rename: MigrationRiskBucket;
49
+ semanticTransform: MigrationRiskBucket;
50
+ unionBreaking: MigrationRiskBucket;
51
+ }
52
+
53
+ export type RenameMap = Record< string, string >;
54
+ export type TransformMap = Record<
55
+ string,
56
+ ( legacyValue: unknown, legacyInput: Record< string, unknown > ) => unknown
57
+ >;
58
+
59
+ function hasOwnKey( record: Record< string, unknown >, key: string ): boolean {
60
+ return Object.prototype.hasOwnProperty.call( record, key );
61
+ }
62
+
63
+ function getSourcePath(
64
+ currentPath: string,
65
+ fallbackPath: string,
66
+ renameMap: RenameMap
67
+ ): string {
68
+ return renameMap[ currentPath ] ?? fallbackPath;
69
+ }
70
+
71
+ export function createDefaultValue( attribute: ManifestAttribute ): unknown {
72
+ if ( attribute.typia.hasDefault ) {
73
+ return attribute.typia.defaultValue;
74
+ }
75
+ if ( attribute.wp.hasDefault ) {
76
+ return attribute.wp.defaultValue;
77
+ }
78
+ if ( attribute.wp.enum && attribute.wp.enum.length > 0 ) {
79
+ return attribute.wp.enum[ 0 ];
80
+ }
81
+
82
+ switch ( attribute.ts.kind ) {
83
+ case 'string':
84
+ return '';
85
+ case 'number':
86
+ return 0;
87
+ case 'boolean':
88
+ return false;
89
+ case 'array':
90
+ return [];
91
+ case 'object': {
92
+ const result: Record< string, unknown > = {};
93
+ for ( const [ key, property ] of Object.entries(
94
+ attribute.ts.properties ?? {}
95
+ ) ) {
96
+ result[ key ] = createDefaultValue( property );
97
+ }
98
+ return result;
99
+ }
100
+ case 'union': {
101
+ const firstBranch = Object.values(
102
+ attribute.ts.union?.branches ?? {}
103
+ )[ 0 ];
104
+ return firstBranch ? createDefaultValue( firstBranch ) : null;
105
+ }
106
+ default:
107
+ return null;
108
+ }
109
+ }
110
+
111
+ export function getValueAtPath(
112
+ input: Record< string, unknown >,
113
+ path: string
114
+ ): unknown {
115
+ if ( ! path ) {
116
+ return undefined;
117
+ }
118
+
119
+ return path.split( '.' ).reduce< unknown >( ( value, segment ) => {
120
+ if (
121
+ typeof value !== 'object' ||
122
+ value === null ||
123
+ Array.isArray( value )
124
+ ) {
125
+ return undefined;
126
+ }
127
+ return ( value as Record< string, unknown > )[ segment ];
128
+ }, input );
129
+ }
130
+
131
+ export function resolveMigrationValue(
132
+ attribute: ManifestAttribute,
133
+ currentKey: string,
134
+ fallbackPath: string,
135
+ input: Record< string, unknown >,
136
+ renameMap: RenameMap,
137
+ transforms: TransformMap
138
+ ): unknown {
139
+ const sourcePath = getSourcePath( currentKey, fallbackPath, renameMap );
140
+ const legacyValue = getValueAtPath( input, sourcePath );
141
+ const transformedValue = transforms[ currentKey ]
142
+ ? transforms[ currentKey ]( legacyValue, input )
143
+ : legacyValue;
144
+
145
+ return coerceValueFromManifest( attribute, transformedValue );
146
+ }
147
+
148
+ export function resolveMigrationAttribute(
149
+ attribute: ManifestAttribute,
150
+ currentPath: string,
151
+ fallbackPath: string,
152
+ input: Record< string, unknown >,
153
+ renameMap: RenameMap,
154
+ transforms: TransformMap
155
+ ): unknown {
156
+ const sourcePath = getSourcePath( currentPath, fallbackPath, renameMap );
157
+
158
+ switch ( attribute.ts.kind ) {
159
+ case 'object': {
160
+ const nextInput = Object.fromEntries(
161
+ Object.entries( attribute.ts.properties ?? {} ).map(
162
+ ( [ key, property ] ) => [
163
+ key,
164
+ resolveMigrationAttribute(
165
+ property,
166
+ `${ currentPath }.${ key }`,
167
+ `${ sourcePath }.${ key }`,
168
+ input,
169
+ renameMap,
170
+ transforms
171
+ ),
172
+ ]
173
+ )
174
+ );
175
+
176
+ return coerceValueFromManifest( attribute, nextInput );
177
+ }
178
+ case 'union': {
179
+ const legacyValue = getValueAtPath( input, sourcePath );
180
+ if ( ! isPlainObject( legacyValue ) || ! attribute.ts.union ) {
181
+ return createDefaultValue( attribute );
182
+ }
183
+
184
+ const { discriminator, branches } = attribute.ts.union;
185
+ const branchKey = legacyValue[ discriminator ];
186
+ if (
187
+ typeof branchKey !== 'string' ||
188
+ ! hasOwnKey( branches, branchKey )
189
+ ) {
190
+ return createDefaultValue( attribute );
191
+ }
192
+
193
+ const branchAttribute = branches[ branchKey ];
194
+ const properties = branchAttribute.ts.properties ?? {};
195
+ const nextInput = Object.fromEntries(
196
+ Object.entries( properties )
197
+ .filter( ( [ key ] ) => key !== discriminator )
198
+ .map( ( [ key, property ] ) => [
199
+ key,
200
+ resolveMigrationAttribute(
201
+ property,
202
+ `${ currentPath }.${ branchKey }.${ key }`,
203
+ `${ sourcePath }.${ key }`,
204
+ input,
205
+ renameMap,
206
+ transforms
207
+ ),
208
+ ] )
209
+ );
210
+
211
+ return coerceValueFromManifest( attribute, {
212
+ ...nextInput,
213
+ [ discriminator ]: branchKey,
214
+ } );
215
+ }
216
+ default:
217
+ return resolveMigrationValue(
218
+ attribute,
219
+ currentPath,
220
+ sourcePath,
221
+ input,
222
+ renameMap,
223
+ transforms
224
+ );
225
+ }
226
+ }
227
+
228
+ export function coerceValueFromManifest(
229
+ attribute: ManifestAttribute,
230
+ value: unknown
231
+ ): unknown {
232
+ if ( value === undefined || value === null ) {
233
+ return createDefaultValue( attribute );
234
+ }
235
+
236
+ switch ( attribute.ts.kind ) {
237
+ case 'string':
238
+ return isValidString( attribute, value )
239
+ ? value
240
+ : createDefaultValue( attribute );
241
+ case 'number':
242
+ return isValidNumber( attribute, value )
243
+ ? value
244
+ : createDefaultValue( attribute );
245
+ case 'boolean':
246
+ return typeof value === 'boolean'
247
+ ? value
248
+ : createDefaultValue( attribute );
249
+ case 'array':
250
+ if ( ! Array.isArray( value ) || ! attribute.ts.items ) {
251
+ return createDefaultValue( attribute );
252
+ }
253
+ return value.map( ( item ) =>
254
+ coerceValueFromManifest(
255
+ attribute.ts.items as ManifestAttribute,
256
+ item
257
+ )
258
+ );
259
+ case 'object':
260
+ if ( ! isPlainObject( value ) ) {
261
+ return createDefaultValue( attribute );
262
+ }
263
+ return Object.fromEntries(
264
+ Object.entries( attribute.ts.properties ?? {} ).map(
265
+ ( [ key, property ] ) => [
266
+ key,
267
+ coerceValueFromManifest(
268
+ property,
269
+ ( value as Record< string, unknown > )[ key ]
270
+ ),
271
+ ]
272
+ )
273
+ );
274
+ case 'union':
275
+ return coerceUnionValue( attribute, value );
276
+ default:
277
+ return createDefaultValue( attribute );
278
+ }
279
+ }
280
+
281
+ export function manifestMatchesDocument(
282
+ manifest: ManifestDocument,
283
+ attributes: Record< string, unknown >
284
+ ): boolean {
285
+ for ( const [ key, attribute ] of Object.entries( manifest.attributes ) ) {
286
+ const value = attributes[ key ];
287
+ if (
288
+ ( value === undefined || value === null ) &&
289
+ ( ! attribute.ts.required ||
290
+ attribute.typia.hasDefault ||
291
+ attribute.wp.hasDefault )
292
+ ) {
293
+ continue;
294
+ }
295
+ if ( ! manifestMatchesAttribute( attribute, value ) ) {
296
+ return false;
297
+ }
298
+ }
299
+
300
+ return true;
301
+ }
302
+
303
+ export function summarizeVersionDelta(
304
+ legacyManifest: ManifestDocument,
305
+ currentManifest: ManifestDocument
306
+ ): { added: string[]; removed: string[]; changed: string[] } {
307
+ const added = Object.keys( currentManifest.attributes ).filter(
308
+ ( key ) => ! ( key in legacyManifest.attributes )
309
+ );
310
+ const removed = Object.keys( legacyManifest.attributes ).filter(
311
+ ( key ) => ! ( key in currentManifest.attributes )
312
+ );
313
+ const changed = Object.keys( currentManifest.attributes ).filter(
314
+ ( key ) => {
315
+ if ( ! ( key in legacyManifest.attributes ) ) {
316
+ return false;
317
+ }
318
+ return (
319
+ JSON.stringify( currentManifest.attributes[ key ] ) !==
320
+ JSON.stringify( legacyManifest.attributes[ key ] )
321
+ );
322
+ }
323
+ );
324
+
325
+ return { added, changed, removed };
326
+ }
327
+
328
+ function manifestMatchesAttribute(
329
+ attribute: ManifestAttribute,
330
+ value: unknown
331
+ ): boolean {
332
+ if (
333
+ ( value === undefined || value === null ) &&
334
+ ( ! attribute.ts.required ||
335
+ attribute.typia.hasDefault ||
336
+ attribute.wp.hasDefault )
337
+ ) {
338
+ return true;
339
+ }
340
+
341
+ switch ( attribute.ts.kind ) {
342
+ case 'string':
343
+ return isValidString( attribute, value );
344
+ case 'number':
345
+ return isValidNumber( attribute, value );
346
+ case 'boolean':
347
+ return typeof value === 'boolean';
348
+ case 'array':
349
+ return (
350
+ Array.isArray( value ) &&
351
+ Boolean( attribute.ts.items ) &&
352
+ value.every( ( item ) =>
353
+ manifestMatchesAttribute(
354
+ attribute.ts.items as ManifestAttribute,
355
+ item
356
+ )
357
+ )
358
+ );
359
+ case 'object':
360
+ return (
361
+ isPlainObject( value ) &&
362
+ Object.entries( attribute.ts.properties ?? {} ).every(
363
+ ( [ key, property ] ) =>
364
+ manifestMatchesAttribute(
365
+ property,
366
+ ( value as Record< string, unknown > )[ key ]
367
+ )
368
+ )
369
+ );
370
+ case 'union':
371
+ return matchesUnionAttribute( attribute, value );
372
+ default:
373
+ return false;
374
+ }
375
+ }
376
+
377
+ function matchesUnionAttribute(
378
+ attribute: ManifestAttribute,
379
+ value: unknown
380
+ ): boolean {
381
+ if ( ! isPlainObject( value ) || ! attribute.ts.union ) {
382
+ return false;
383
+ }
384
+
385
+ const { discriminator, branches } = attribute.ts.union;
386
+ const branchKey = ( value as Record< string, unknown > )[ discriminator ];
387
+
388
+ return (
389
+ typeof branchKey === 'string' &&
390
+ hasOwnKey( branches, branchKey ) &&
391
+ manifestMatchesAttribute( branches[ branchKey ], value )
392
+ );
393
+ }
394
+
395
+ function coerceUnionValue(
396
+ attribute: ManifestAttribute,
397
+ value: unknown
398
+ ): unknown {
399
+ if ( ! isPlainObject( value ) || ! attribute.ts.union ) {
400
+ return createDefaultValue( attribute );
401
+ }
402
+
403
+ const { discriminator, branches } = attribute.ts.union;
404
+ const branchKey = ( value as Record< string, unknown > )[ discriminator ];
405
+ if ( typeof branchKey !== 'string' || ! hasOwnKey( branches, branchKey ) ) {
406
+ return createDefaultValue( attribute );
407
+ }
408
+
409
+ return coerceValueFromManifest( branches[ branchKey ], value );
410
+ }
411
+
412
+ function isValidString(
413
+ attribute: ManifestAttribute,
414
+ value: unknown
415
+ ): value is string {
416
+ if ( typeof value !== 'string' ) {
417
+ return false;
418
+ }
419
+
420
+ const { enum: enumValues } = attribute.wp;
421
+ const { format, maxLength, minLength, pattern } =
422
+ attribute.typia.constraints;
423
+
424
+ if ( enumValues && ! enumValues.includes( value ) ) {
425
+ return false;
426
+ }
427
+ if ( typeof minLength === 'number' && value.length < minLength ) {
428
+ return false;
429
+ }
430
+ if ( typeof maxLength === 'number' && value.length > maxLength ) {
431
+ return false;
432
+ }
433
+ if ( pattern ) {
434
+ try {
435
+ // Typia manifests are generated from developer-authored block types, not end-user input.
436
+ const regex = new RegExp( pattern );
437
+ if ( ! regex.test( value ) ) {
438
+ return false;
439
+ }
440
+ } catch {
441
+ return false;
442
+ }
443
+ }
444
+ if (
445
+ format === 'uuid' &&
446
+ ! /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
447
+ value
448
+ )
449
+ ) {
450
+ return false;
451
+ }
452
+
453
+ return true;
454
+ }
455
+
456
+ function isValidNumber(
457
+ attribute: ManifestAttribute,
458
+ value: unknown
459
+ ): value is number {
460
+ if ( typeof value !== 'number' || Number.isNaN( value ) ) {
461
+ return false;
462
+ }
463
+
464
+ const { enum: enumValues } = attribute.wp;
465
+ const { maximum, minimum, typeTag } = attribute.typia.constraints;
466
+
467
+ if ( enumValues && ! enumValues.includes( value ) ) {
468
+ return false;
469
+ }
470
+ if ( typeof minimum === 'number' && value < minimum ) {
471
+ return false;
472
+ }
473
+ if ( typeof maximum === 'number' && value > maximum ) {
474
+ return false;
475
+ }
476
+ if (
477
+ typeTag === 'uint32' &&
478
+ ( ! Number.isInteger( value ) || value < 0 || value > 4294967295 )
479
+ ) {
480
+ return false;
481
+ }
482
+
483
+ return true;
484
+ }
485
+
486
+ function isPlainObject( value: unknown ): value is Record< string, unknown > {
487
+ return (
488
+ typeof value === 'object' && value !== null && ! Array.isArray( value )
489
+ );
490
+ }