@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,559 @@
1
+ /* eslint-disable no-console */
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ const PARENT_BLOCK_NAME = '{{namespace}}/{{slugKebabCase}}';
6
+ const PARENT_BLOCK_NAMESPACE = '{{namespace}}';
7
+ const PARENT_BLOCK_SLUG = '{{slugKebabCase}}';
8
+ const PARENT_BLOCK_TITLE = {{titleJson}};
9
+ const PARENT_TYPE_NAME = '{{pascalCase}}';
10
+ const PARENT_STYLE_IMPORT = '../{{slugKebabCase}}/style.scss';
11
+ const PROJECT_ROOT = process.cwd();
12
+ const TEXT_DOMAIN = '{{textDomain}}';
13
+
14
+ const ALLOWED_CHILD_MARKER = '// add-child: insert new allowed child block names here';
15
+ const BLOCK_CONFIG_MARKER = '// add-child: insert new block config entries here';
16
+ const CHILD_PLACEHOLDER = 'Add supporting details for this internal item.';
17
+ const BLOCK_SLUG_PATTERN = /^[a-z][a-z0-9-]*$/;
18
+
19
+ type StarterManifestDocument = {
20
+ attributes: Record< string, unknown >;
21
+ manifestVersion: 2;
22
+ sourceType: string;
23
+ };
24
+
25
+ function parseArgs() {
26
+ const args = process.argv.slice( 2 );
27
+ const parsed: {
28
+ slug?: string;
29
+ title?: string;
30
+ } = {};
31
+
32
+ for ( let index = 0; index < args.length; index += 1 ) {
33
+ const arg = args[ index ];
34
+ if ( arg === '--slug' ) {
35
+ const value = args[ index + 1 ];
36
+ if ( ! value || value.startsWith( '--' ) ) {
37
+ throw new Error( '--slug requires a value.' );
38
+ }
39
+ parsed.slug = value;
40
+ index += 1;
41
+ continue;
42
+ }
43
+
44
+ if ( arg === '--title' ) {
45
+ const value = args[ index + 1 ];
46
+ if ( ! value || value.startsWith( '--' ) ) {
47
+ throw new Error( '--title requires a value.' );
48
+ }
49
+ parsed.title = value;
50
+ index += 1;
51
+ continue;
52
+ }
53
+ }
54
+
55
+ return parsed;
56
+ }
57
+
58
+ function toKebabCase( input: string ): string {
59
+ return input
60
+ .trim()
61
+ .replace( /([a-z0-9])([A-Z])/g, '$1-$2' )
62
+ .replace( /[^A-Za-z0-9]+/g, '-' )
63
+ .replace( /^-+|-+$/g, '' )
64
+ .replace( /-{2,}/g, '-' )
65
+ .toLowerCase();
66
+ }
67
+
68
+ function toPascalCase( input: string ): string {
69
+ return toKebabCase( input )
70
+ .split( '-' )
71
+ .filter( Boolean )
72
+ .map( ( segment ) => segment.charAt( 0 ).toUpperCase() + segment.slice( 1 ) )
73
+ .join( '' );
74
+ }
75
+
76
+ function toTitleCase( input: string ): string {
77
+ return toKebabCase( input )
78
+ .split( '-' )
79
+ .filter( Boolean )
80
+ .map( ( segment ) => segment.charAt( 0 ).toUpperCase() + segment.slice( 1 ) )
81
+ .join( ' ' );
82
+ }
83
+
84
+ function resolveValidatedNamespace( value: string ): string {
85
+ const normalizedNamespace = toKebabCase( value );
86
+
87
+ if ( ! BLOCK_SLUG_PATTERN.test( normalizedNamespace ) ) {
88
+ throw new Error( 'Use a block namespace with lowercase letters, numbers, and hyphens only.' );
89
+ }
90
+
91
+ return normalizedNamespace;
92
+ }
93
+
94
+ function resolveValidatedBlockSlug( value: string ): string {
95
+ const normalizedSlug = toKebabCase( value );
96
+
97
+ if ( ! BLOCK_SLUG_PATTERN.test( normalizedSlug ) ) {
98
+ throw new Error( 'Use a child slug with lowercase letters, numbers, and hyphens only.' );
99
+ }
100
+
101
+ return normalizedSlug;
102
+ }
103
+
104
+ function buildBlockCssClassName( namespace: string, slug: string ): string {
105
+ const normalizedSlug = resolveValidatedBlockSlug( slug );
106
+ const normalizedNamespace =
107
+ namespace.trim().length > 0
108
+ ? resolveValidatedNamespace( namespace )
109
+ : '';
110
+
111
+ return normalizedNamespace.length > 0
112
+ ? `wp-block-${ normalizedNamespace }-${ normalizedSlug }`
113
+ : `wp-block-${ normalizedSlug }`;
114
+ }
115
+
116
+ function escapeForRegExp( value: string ): string {
117
+ return value.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
118
+ }
119
+
120
+ function createStringStarterAttribute(
121
+ defaultValue: string,
122
+ maxLength: number
123
+ ): Record< string, unknown > {
124
+ return {
125
+ ts: {
126
+ items: null,
127
+ kind: 'string',
128
+ properties: null,
129
+ required: true,
130
+ union: null,
131
+ },
132
+ typia: {
133
+ constraints: {
134
+ exclusiveMaximum: null,
135
+ exclusiveMinimum: null,
136
+ format: null,
137
+ maxItems: null,
138
+ maxLength,
139
+ maximum: null,
140
+ minItems: null,
141
+ minLength: 1,
142
+ minimum: null,
143
+ multipleOf: null,
144
+ pattern: null,
145
+ typeTag: null,
146
+ },
147
+ defaultValue,
148
+ hasDefault: true,
149
+ },
150
+ wp: {
151
+ defaultValue,
152
+ enum: null,
153
+ hasDefault: true,
154
+ type: 'string',
155
+ },
156
+ };
157
+ }
158
+
159
+ function createCompoundChildStarterManifest(
160
+ childTypeName: string,
161
+ childTitle: string,
162
+ bodyPlaceholder = CHILD_PLACEHOLDER
163
+ ): StarterManifestDocument {
164
+ return {
165
+ attributes: {
166
+ body: createStringStarterAttribute( bodyPlaceholder, 280 ),
167
+ title: createStringStarterAttribute( childTitle, 80 ),
168
+ },
169
+ manifestVersion: 2,
170
+ sourceType: childTypeName,
171
+ };
172
+ }
173
+
174
+ function stringifyStarterManifest( document: StarterManifestDocument ): string {
175
+ return `${ JSON.stringify( document, null, '\t' ) }\n`;
176
+ }
177
+
178
+ function insertBeforeMarker( filePath: string, marker: string, insertionLines: string[] ) {
179
+ const source = fs.readFileSync( filePath, 'utf8' );
180
+ const markerPattern = new RegExp(
181
+ `^(?<indent>\\s*)${ escapeForRegExp( marker ) }\\s*$`,
182
+ 'm'
183
+ );
184
+ const markerMatch = source.match( markerPattern );
185
+
186
+ if ( ! markerMatch ) {
187
+ throw new Error( `Unable to update ${ filePath }: marker not found.` );
188
+ }
189
+
190
+ const indent = markerMatch.groups?.indent ?? '';
191
+ const markerLine = markerMatch[ 0 ];
192
+ const insertion = insertionLines
193
+ .map( ( line ) => ( line.length > 0 ? `${ indent }${ line }` : line ) )
194
+ .join( '\n' );
195
+
196
+ fs.writeFileSync(
197
+ filePath,
198
+ source.replace( markerPattern, `${ insertion }\n${ markerLine }` ),
199
+ 'utf8'
200
+ );
201
+ }
202
+
203
+ function renderBlockJson(
204
+ childBlockName: string,
205
+ childFolderSlug: string,
206
+ childTitle: string
207
+ ): string {
208
+ const childCssClassName = buildBlockCssClassName( PARENT_BLOCK_NAMESPACE, childFolderSlug );
209
+
210
+ return `${ JSON.stringify(
211
+ {
212
+ $schema: 'https://schemas.wp.org/trunk/block.json',
213
+ apiVersion: 3,
214
+ name: childBlockName,
215
+ version: '{{blockMetadataVersion}}',
216
+ title: childTitle,
217
+ category: '{{compoundChildCategory}}',
218
+ icon: '{{compoundChildIcon}}',
219
+ description: `Internal item block used by ${ PARENT_BLOCK_TITLE }.`,
220
+ parent: [ PARENT_BLOCK_NAME ],
221
+ example: {},
222
+ supports: {
223
+ html: false,
224
+ inserter: false,
225
+ reusable: false,
226
+ },
227
+ attributes: {
228
+ title: {
229
+ type: 'string',
230
+ source: 'html',
231
+ selector: `.${ childCssClassName }__title`,
232
+ default: childTitle,
233
+ },
234
+ body: {
235
+ type: 'string',
236
+ source: 'html',
237
+ selector: `.${ childCssClassName }__body`,
238
+ default: CHILD_PLACEHOLDER,
239
+ },
240
+ },
241
+ textdomain: TEXT_DOMAIN,
242
+ editorScript: 'file:./index.js',
243
+ },
244
+ null,
245
+ '\t'
246
+ ) }\n`;
247
+ }
248
+
249
+ function renderTypesFile( childTypeName: string, childTitle: string ): string {
250
+ return `import { tags } from 'typia';
251
+
252
+ export interface ${ childTypeName } {
253
+ \ttitle: string &
254
+ \t\ttags.MinLength< 1 > &
255
+ \t\ttags.MaxLength< 80 > &
256
+ \t\ttags.Default< ${ JSON.stringify( childTitle ) } >;
257
+ \tbody: string &
258
+ \t\ttags.MinLength< 1 > &
259
+ \t\ttags.MaxLength< 280 > &
260
+ \t\ttags.Default< ${ JSON.stringify( CHILD_PLACEHOLDER ) } >;
261
+ }
262
+ `;
263
+ }
264
+
265
+ function renderStarterManifestFile(
266
+ childTypeName: string,
267
+ childTitle: string
268
+ ): string {
269
+ return stringifyStarterManifest(
270
+ createCompoundChildStarterManifest(
271
+ childTypeName,
272
+ childTitle,
273
+ CHILD_PLACEHOLDER
274
+ )
275
+ );
276
+ }
277
+
278
+ function renderHooksFile(): string {
279
+ return `import { useMemo } from '@wordpress/element';
280
+
281
+ import {
282
+ \tcreateUseTypiaValidationHook,
283
+ \tformatValidationError,
284
+ \tformatValidationErrors,
285
+ \ttype TypiaValidationError,
286
+ \ttype ValidationResult,
287
+ \ttype ValidationState,
288
+ } from '@wp-typia/block-runtime/validation';
289
+
290
+ export {
291
+ \tformatValidationError,
292
+ \tformatValidationErrors,
293
+ \ttype TypiaValidationError,
294
+ \ttype ValidationResult,
295
+ \ttype ValidationState,
296
+ } from '@wp-typia/block-runtime/validation';
297
+
298
+ export const useTypiaValidation = createUseTypiaValidationHook( {
299
+ \tuseMemo,
300
+ } );
301
+ `;
302
+ }
303
+
304
+ function renderValidatorsFile(
305
+ childTypeName: string,
306
+ childInterfaceName: string
307
+ ): string {
308
+ return `import typia from 'typia';
309
+ import currentManifest from './typia.manifest.json';
310
+ import {
311
+ \ttype ManifestDefaultsDocument,
312
+ } from '@wp-typia/block-runtime/defaults';
313
+ import {
314
+ \tcreateScaffoldValidatorToolkit,
315
+ } from '@wp-typia/block-runtime/validation';
316
+
317
+ import type { ${ childTypeName } } from './types';
318
+
319
+ const validate = typia.createValidate< ${ childTypeName } >();
320
+ const assert = typia.createAssert< ${ childTypeName } >();
321
+ const is = typia.createIs< ${ childTypeName } >();
322
+ const random = typia.createRandom< ${ childTypeName } >();
323
+ const clone = typia.misc.createClone< ${ childTypeName } >();
324
+ const prune = typia.misc.createPrune< ${ childTypeName } >();
325
+ const scaffoldValidators = createScaffoldValidatorToolkit< ${ childTypeName } >( {
326
+ \tmanifest: currentManifest as ManifestDefaultsDocument,
327
+ \tvalidate,
328
+ \tassert,
329
+ \tis,
330
+ \trandom,
331
+ \tclone,
332
+ \tprune,
333
+ } );
334
+
335
+ export const validate${ childInterfaceName } = scaffoldValidators.validateAttributes;
336
+
337
+ export const validators = scaffoldValidators.validators;
338
+
339
+ export const sanitize${ childInterfaceName } = scaffoldValidators.sanitizeAttributes;
340
+
341
+ export const createAttributeUpdater = scaffoldValidators.createAttributeUpdater;
342
+ `;
343
+ }
344
+
345
+ function renderEditFile(
346
+ childFolderSlug: string,
347
+ childTypeName: string,
348
+ childInterfaceName: string,
349
+ childTitle: string
350
+ ): string {
351
+ const childCssClassName = buildBlockCssClassName( PARENT_BLOCK_NAMESPACE, childFolderSlug );
352
+
353
+ return `import { RichText, useBlockProps } from '@wordpress/block-editor';
354
+ import { Notice } from '@wordpress/components';
355
+ import { __ } from '@wordpress/i18n';
356
+
357
+ import { useTypiaValidation } from './hooks';
358
+ import type { ${ childTypeName } } from './types';
359
+ import {
360
+ \tcreateAttributeUpdater,
361
+ \tvalidate${ childInterfaceName },
362
+ } from './validators';
363
+
364
+ export default function Edit( {
365
+ \tattributes,
366
+ \tsetAttributes,
367
+ }: {
368
+ \tattributes: ${ childTypeName };
369
+ \tsetAttributes: ( attrs: Partial< ${ childTypeName } > ) => void;
370
+ } ) {
371
+ \tconst updateAttribute = createAttributeUpdater( attributes, setAttributes );
372
+ \tconst { errorMessages, isValid } = useTypiaValidation(
373
+ \t\tattributes,
374
+ \t\tvalidate${ childInterfaceName }
375
+ \t);
376
+
377
+ \treturn (
378
+ \t\t<div { ...useBlockProps( { className: '${ childCssClassName }' } ) }>
379
+ \t\t\t<RichText
380
+ \t\t\t\ttagName="h4"
381
+ \t\t\t\tclassName="${ childCssClassName }__title"
382
+ \t\t\t\tvalue={ attributes.title ?? '' }
383
+ \t\t\t\tonChange={ ( title ) => updateAttribute( 'title', title ) }
384
+ \t\t\t\tplaceholder={ __( ${ JSON.stringify( childTitle ) }, '${ TEXT_DOMAIN }' ) }
385
+ \t\t\t/>
386
+ \t\t\t<RichText
387
+ \t\t\t\ttagName="p"
388
+ \t\t\t\tclassName="${ childCssClassName }__body"
389
+ \t\t\t\tvalue={ attributes.body ?? '' }
390
+ \t\t\t\tonChange={ ( body ) => updateAttribute( 'body', body ) }
391
+ \t\t\t\tplaceholder={ __( ${ JSON.stringify( CHILD_PLACEHOLDER ) }, '${ TEXT_DOMAIN }' ) }
392
+ \t\t\t/>
393
+ \t\t\t{ ! isValid && (
394
+ \t\t\t\t<Notice status="error" isDismissible={ false }>
395
+ \t\t\t\t\t<ul>
396
+ \t\t\t\t\t\t{ errorMessages.map( ( error, index ) => (
397
+ \t\t\t\t\t\t\t<li key={ index }>{ error }</li>
398
+ \t\t\t\t\t\t) ) }
399
+ \t\t\t\t\t</ul>
400
+ \t\t\t\t</Notice>
401
+ \t\t\t) }
402
+ \t\t</div>
403
+ \t);
404
+ }
405
+ `;
406
+ }
407
+
408
+ function renderSaveFile( childFolderSlug: string, childTypeName: string ): string {
409
+ const childCssClassName = buildBlockCssClassName( PARENT_BLOCK_NAMESPACE, childFolderSlug );
410
+
411
+ return `import { RichText, useBlockProps } from '@wordpress/block-editor';
412
+
413
+ import type { ${ childTypeName } } from './types';
414
+
415
+ export default function Save( {
416
+ \tattributes,
417
+ }: {
418
+ \tattributes: ${ childTypeName };
419
+ } ) {
420
+ \treturn (
421
+ \t\t<div { ...useBlockProps.save( { className: '${ childCssClassName }' } ) }>
422
+ \t\t\t<RichText.Content
423
+ \t\t\t\ttagName="h4"
424
+ \t\t\t\tclassName="${ childCssClassName }__title"
425
+ \t\t\t\tvalue={ attributes.title }
426
+ \t\t\t/>
427
+ \t\t\t<RichText.Content
428
+ \t\t\t\ttagName="p"
429
+ \t\t\t\tclassName="${ childCssClassName }__body"
430
+ \t\t\t\tvalue={ attributes.body }
431
+ \t\t\t/>
432
+ \t\t</div>
433
+ \t);
434
+ }
435
+ `;
436
+ }
437
+
438
+ function renderIndexFile( childTypeName: string, childFolderSlug: string ): string {
439
+ return `import { registerBlockType } from '@wordpress/blocks';
440
+ import type { BlockConfiguration } from '@wordpress/blocks';
441
+ import {
442
+ \tbuildScaffoldBlockRegistration,
443
+ \ttype ScaffoldBlockMetadata,
444
+ } from '@wp-typia/block-runtime/blocks';
445
+
446
+ import Edit from './edit';
447
+ import Save from './save';
448
+ import metadata from './block.json';
449
+ import '${ PARENT_STYLE_IMPORT }';
450
+
451
+ import type { ${ childTypeName } } from './types';
452
+
453
+ const registration = buildScaffoldBlockRegistration<
454
+ \tBlockConfiguration< ${ childTypeName } >
455
+ >( metadata as ScaffoldBlockMetadata, {
456
+ \tedit: Edit,
457
+ \tsave: Save,
458
+ } );
459
+
460
+ registerBlockType< ${ childTypeName } >(
461
+ \tregistration.name,
462
+ \tregistration.settings
463
+ );
464
+ `;
465
+ }
466
+
467
+ function main() {
468
+ const { slug, title } = parseArgs();
469
+ const normalizedSlug = slug ? resolveValidatedBlockSlug( slug ) : '';
470
+
471
+ if ( normalizedSlug.length === 0 ) {
472
+ throw new Error( 'Use a child slug with lowercase letters, numbers, and hyphens only.' );
473
+ }
474
+
475
+ const childTitle = title?.trim().length ? title.trim() : toTitleCase( normalizedSlug );
476
+ const childFolderSlug = `${ PARENT_BLOCK_SLUG }-${ normalizedSlug }`;
477
+ const childBlockName = `${ PARENT_BLOCK_NAME }-${ normalizedSlug }`;
478
+ const childTypeSuffix = toPascalCase( normalizedSlug );
479
+ const childTypeName = `${ PARENT_TYPE_NAME }${ childTypeSuffix }Attributes`;
480
+ const childInterfaceName = `${ PARENT_TYPE_NAME }${ childTypeSuffix }`;
481
+ const childDir = path.join( PROJECT_ROOT, 'src', 'blocks', childFolderSlug );
482
+ const childrenFile = path.join(
483
+ PROJECT_ROOT,
484
+ 'src',
485
+ 'blocks',
486
+ PARENT_BLOCK_SLUG,
487
+ 'children.ts'
488
+ );
489
+ const blockConfigFile = path.join( PROJECT_ROOT, 'scripts', 'block-config.ts' );
490
+
491
+ if ( fs.existsSync( childDir ) ) {
492
+ throw new Error( `Child block already exists: ${ childFolderSlug }` );
493
+ }
494
+
495
+ if ( ! fs.existsSync( childrenFile ) || ! fs.existsSync( blockConfigFile ) ) {
496
+ throw new Error(
497
+ 'This command expects a compound scaffold with src/blocks/<parent>/children.ts and scripts/block-config.ts.'
498
+ );
499
+ }
500
+
501
+ fs.mkdirSync( childDir, { recursive: true } );
502
+
503
+ fs.writeFileSync(
504
+ path.join( childDir, 'block.json' ),
505
+ renderBlockJson( childBlockName, childFolderSlug, childTitle ),
506
+ 'utf8'
507
+ );
508
+ fs.writeFileSync( path.join( childDir, 'types.ts' ), renderTypesFile( childTypeName, childTitle ), 'utf8' );
509
+ fs.writeFileSync(
510
+ path.join( childDir, 'typia.manifest.json' ),
511
+ renderStarterManifestFile( childTypeName, childTitle ),
512
+ 'utf8'
513
+ );
514
+ fs.writeFileSync( path.join( childDir, 'hooks.ts' ), renderHooksFile(), 'utf8' );
515
+ fs.writeFileSync(
516
+ path.join( childDir, 'validators.ts' ),
517
+ renderValidatorsFile( childTypeName, childInterfaceName ),
518
+ 'utf8'
519
+ );
520
+ fs.writeFileSync(
521
+ path.join( childDir, 'edit.tsx' ),
522
+ renderEditFile( childFolderSlug, childTypeName, childInterfaceName, childTitle ),
523
+ 'utf8'
524
+ );
525
+ fs.writeFileSync(
526
+ path.join( childDir, 'save.tsx' ),
527
+ renderSaveFile( childFolderSlug, childTypeName ),
528
+ 'utf8'
529
+ );
530
+ fs.writeFileSync(
531
+ path.join( childDir, 'index.tsx' ),
532
+ renderIndexFile( childTypeName, childFolderSlug ),
533
+ 'utf8'
534
+ );
535
+
536
+ insertBeforeMarker(
537
+ childrenFile,
538
+ ALLOWED_CHILD_MARKER,
539
+ [ `'${ childBlockName }',` ]
540
+ );
541
+ insertBeforeMarker(
542
+ blockConfigFile,
543
+ BLOCK_CONFIG_MARKER,
544
+ [
545
+ '{',
546
+ `\tslug: '${ childFolderSlug }',`,
547
+ `\tattributeTypeName: '${ childTypeName }',`,
548
+ `\ttypesFile: 'src/blocks/${ childFolderSlug }/types.ts',`,
549
+ '},',
550
+ ]
551
+ );
552
+
553
+ console.log( `✅ Added compound child block ${ childBlockName }` );
554
+ console.log(
555
+ 'Run `sync-types` next to generate block.json metadata, manifests, schemas, and PHP validators for the new child block.'
556
+ );
557
+ }
558
+
559
+ main();
@@ -0,0 +1,13 @@
1
+ export const BLOCKS = [
2
+ {
3
+ attributeTypeName: '{{pascalCase}}Attributes',
4
+ slug: '{{slugKebabCase}}',
5
+ typesFile: 'src/blocks/{{slugKebabCase}}/types.ts',
6
+ },
7
+ {
8
+ slug: '{{slugKebabCase}}-item',
9
+ attributeTypeName: '{{pascalCase}}ItemAttributes',
10
+ typesFile: 'src/blocks/{{slugKebabCase}}-item/types.ts',
11
+ },
12
+ // add-child: insert new block config entries here
13
+ ] as const;
@@ -0,0 +1,53 @@
1
+ /* eslint-disable no-console */
2
+ import path from 'node:path';
3
+
4
+ import { syncBlockMetadata } from '@wp-typia/block-runtime/metadata-core';
5
+
6
+ import { BLOCKS } from './block-config';
7
+
8
+ function parseCliOptions( argv: string[] ) {
9
+ const options = {
10
+ check: false,
11
+ };
12
+
13
+ for ( const argument of argv ) {
14
+ if ( argument === '--check' ) {
15
+ options.check = true;
16
+ continue;
17
+ }
18
+
19
+ throw new Error( `Unknown sync-types flag: ${ argument }` );
20
+ }
21
+
22
+ return options;
23
+ }
24
+
25
+ async function main() {
26
+ const options = parseCliOptions( process.argv.slice( 2 ) );
27
+
28
+ for ( const block of BLOCKS ) {
29
+ const baseDir = path.join( 'src', 'blocks', block.slug );
30
+ const result = await syncBlockMetadata( {
31
+ blockJsonFile: path.join( baseDir, 'block.json' ),
32
+ jsonSchemaFile: path.join( baseDir, 'typia.schema.json' ),
33
+ manifestFile: path.join( baseDir, 'typia.manifest.json' ),
34
+ openApiFile: path.join( baseDir, 'typia.openapi.json' ),
35
+ sourceTypeName: block.attributeTypeName,
36
+ typesFile: block.typesFile,
37
+ }, {
38
+ check: options.check,
39
+ } );
40
+
41
+ console.log(
42
+ options.check
43
+ ? `✅ ${ block.slug }: block.json, typia.manifest.json, typia-validator.php, typia.schema.json, and typia.openapi.json are already up to date with the TypeScript types!`
44
+ : `✅ ${ block.slug }: block.json, typia.manifest.json, typia-validator.php, typia.schema.json, and typia.openapi.json were generated from TypeScript types!`
45
+ );
46
+ console.log( '📝 Generated attributes:', result.attributeNames );
47
+ }
48
+ }
49
+
50
+ main().catch( ( error ) => {
51
+ console.error( '❌ Type sync failed:', error );
52
+ process.exit( 1 );
53
+ } );