@wp-typia/project-tools 0.20.2 → 0.21.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 (47) hide show
  1. package/dist/runtime/cli-add-shared.d.ts +73 -5
  2. package/dist/runtime/cli-add-shared.js +58 -11
  3. package/dist/runtime/cli-add-workspace-ability.js +11 -57
  4. package/dist/runtime/cli-add-workspace-admin-view.d.ts +23 -0
  5. package/dist/runtime/cli-add-workspace-admin-view.js +872 -0
  6. package/dist/runtime/cli-add-workspace-ai-anchors.js +2 -5
  7. package/dist/runtime/cli-add-workspace-ai-source-emitters.d.ts +0 -4
  8. package/dist/runtime/cli-add-workspace-ai-source-emitters.js +7 -17
  9. package/dist/runtime/cli-add-workspace-ai.js +4 -6
  10. package/dist/runtime/cli-add-workspace-assets.d.ts +13 -5
  11. package/dist/runtime/cli-add-workspace-assets.js +290 -106
  12. package/dist/runtime/cli-add-workspace-rest-anchors.js +2 -5
  13. package/dist/runtime/cli-add-workspace-rest-source-emitters.d.ts +0 -1
  14. package/dist/runtime/cli-add-workspace-rest-source-emitters.js +7 -14
  15. package/dist/runtime/cli-add-workspace-rest.js +4 -6
  16. package/dist/runtime/cli-add-workspace.d.ts +58 -1
  17. package/dist/runtime/cli-add-workspace.js +588 -18
  18. package/dist/runtime/cli-add.d.ts +1 -1
  19. package/dist/runtime/cli-add.js +1 -1
  20. package/dist/runtime/cli-core.d.ts +8 -5
  21. package/dist/runtime/cli-core.js +7 -4
  22. package/dist/runtime/cli-diagnostics.d.ts +83 -1
  23. package/dist/runtime/cli-diagnostics.js +85 -2
  24. package/dist/runtime/cli-doctor-workspace.js +552 -13
  25. package/dist/runtime/cli-doctor.d.ts +4 -2
  26. package/dist/runtime/cli-doctor.js +2 -1
  27. package/dist/runtime/cli-help.js +19 -9
  28. package/dist/runtime/cli-init.d.ts +67 -3
  29. package/dist/runtime/cli-init.js +603 -64
  30. package/dist/runtime/cli-validation.js +4 -3
  31. package/dist/runtime/index.d.ts +9 -4
  32. package/dist/runtime/index.js +7 -3
  33. package/dist/runtime/package-json-types.d.ts +12 -0
  34. package/dist/runtime/package-json-types.js +1 -0
  35. package/dist/runtime/package-versions.d.ts +17 -2
  36. package/dist/runtime/package-versions.js +46 -1
  37. package/dist/runtime/php-utils.d.ts +16 -0
  38. package/dist/runtime/php-utils.js +59 -0
  39. package/dist/runtime/scaffold-answer-resolution.js +7 -6
  40. package/dist/runtime/scaffold-apply-utils.d.ts +2 -3
  41. package/dist/runtime/scaffold-apply-utils.js +3 -43
  42. package/dist/runtime/template-source-cache.d.ts +112 -0
  43. package/dist/runtime/template-source-cache.js +434 -0
  44. package/dist/runtime/template-source-seeds.js +319 -53
  45. package/dist/runtime/workspace-inventory.d.ts +43 -2
  46. package/dist/runtime/workspace-inventory.js +138 -5
  47. package/package.json +2 -2
@@ -3,10 +3,178 @@ import { promises as fsp } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { resolveWorkspaceProject } from "./workspace-project.js";
5
5
  import { appendWorkspaceInventoryEntries, readWorkspaceInventory } from "./workspace-inventory.js";
6
- import { toKebabCase, toTitleCase } from "./string-case.js";
6
+ import { toKebabCase, toSnakeCase, toTitleCase } from "./string-case.js";
7
7
  import { assertValidGeneratedSlug, assertValidHookAnchor, assertValidHookedBlockPosition, assertVariationDoesNotExist, getMutableBlockHooks, normalizeBlockSlug, patchFile, quoteTsString, readWorkspaceBlockJson, resolveWorkspaceBlock, rollbackWorkspaceMutation, snapshotWorkspaceFiles, } from "./cli-add-shared.js";
8
8
  const VARIATIONS_IMPORT_LINE = "import { registerWorkspaceVariations } from './variations';";
9
+ const VARIATIONS_IMPORT_PATTERN = /^\s*import\s*\{\s*registerWorkspaceVariations\s*\}\s*from\s*["']\.\/variations["']\s*;?\s*$/mu;
9
10
  const VARIATIONS_CALL_LINE = "registerWorkspaceVariations();";
11
+ const VARIATIONS_CALL_PATTERN = /registerWorkspaceVariations\s*\(\s*\)\s*;?/u;
12
+ const BLOCK_STYLES_IMPORT_LINE = "import { registerWorkspaceBlockStyles } from './styles';";
13
+ const BLOCK_STYLES_IMPORT_PATTERN = /^\s*import\s*\{\s*registerWorkspaceBlockStyles\s*\}\s*from\s*["']\.\/styles["']\s*;?\s*$/mu;
14
+ const BLOCK_STYLES_CALL_LINE = "registerWorkspaceBlockStyles();";
15
+ const BLOCK_STYLES_CALL_PATTERN = /registerWorkspaceBlockStyles\s*\(\s*\)\s*;?/u;
16
+ const BLOCK_TRANSFORMS_IMPORT_LINE = "import { applyWorkspaceBlockTransforms } from './transforms';";
17
+ const BLOCK_TRANSFORMS_IMPORT_PATTERN = /^\s*import\s*\{\s*applyWorkspaceBlockTransforms\s*\}\s*from\s*["']\.\/transforms["']\s*;?\s*$/mu;
18
+ const BLOCK_TRANSFORMS_CALL_LINE = "applyWorkspaceBlockTransforms(registration.settings);";
19
+ const BLOCK_TRANSFORMS_CALL_PATTERN = /applyWorkspaceBlockTransforms\s*\(\s*registration\s*\.\s*settings\s*\)\s*;?/u;
20
+ const SCAFFOLD_REGISTRATION_SETTINGS_CALL_PATTERN = /registerScaffoldBlockType\s*\(\s*registration\s*\.\s*name\s*,\s*registration\s*\.\s*settings\s*\)\s*;?/u;
21
+ const FULL_BLOCK_NAME_PATTERN = /^[a-z0-9-]+\/[a-z0-9-]+$/u;
22
+ function maskSourceSegment(segment) {
23
+ return segment.replace(/[^\n\r]/gu, " ");
24
+ }
25
+ function maskTypeScriptComments(source) {
26
+ return source
27
+ .replace(/\/\*[\s\S]*?\*\//gu, maskSourceSegment)
28
+ .replace(/\/\/[^\n\r]*/gu, maskSourceSegment);
29
+ }
30
+ // Preserve source offsets so executable-code match indexes still map to the original file.
31
+ function maskTypeScriptCommentsAndLiterals(source) {
32
+ let maskedSource = "";
33
+ let index = 0;
34
+ while (index < source.length) {
35
+ const current = source[index];
36
+ const next = source[index + 1];
37
+ if (current === "/" && next === "/") {
38
+ const start = index;
39
+ index += 2;
40
+ while (index < source.length &&
41
+ source[index] !== "\n" &&
42
+ source[index] !== "\r") {
43
+ index += 1;
44
+ }
45
+ maskedSource += maskSourceSegment(source.slice(start, index));
46
+ continue;
47
+ }
48
+ if (current === "/" && next === "*") {
49
+ const start = index;
50
+ index += 2;
51
+ while (index < source.length &&
52
+ !(source[index] === "*" && source[index + 1] === "/")) {
53
+ index += 1;
54
+ }
55
+ index = Math.min(index + 2, source.length);
56
+ maskedSource += maskSourceSegment(source.slice(start, index));
57
+ continue;
58
+ }
59
+ if (current === "'" || current === '"' || current === "`") {
60
+ const start = index;
61
+ const quote = current;
62
+ index += 1;
63
+ while (index < source.length) {
64
+ const char = source[index];
65
+ if (char === "\\") {
66
+ index += 2;
67
+ continue;
68
+ }
69
+ index += 1;
70
+ if (char === quote) {
71
+ break;
72
+ }
73
+ }
74
+ maskedSource += maskSourceSegment(source.slice(start, index));
75
+ continue;
76
+ }
77
+ maskedSource += current;
78
+ index += 1;
79
+ }
80
+ return maskedSource;
81
+ }
82
+ function hasExecutablePattern(source, pattern) {
83
+ return pattern.test(maskTypeScriptCommentsAndLiterals(source));
84
+ }
85
+ function hasUncommentedPattern(source, pattern) {
86
+ return pattern.test(maskTypeScriptComments(source));
87
+ }
88
+ function findExecutablePatternMatch(source, patterns) {
89
+ const maskedSource = maskTypeScriptCommentsAndLiterals(source);
90
+ for (const pattern of patterns) {
91
+ const match = pattern.exec(maskedSource);
92
+ if (match && match.index !== undefined) {
93
+ return {
94
+ end: match.index + match[0].length,
95
+ start: match.index,
96
+ };
97
+ }
98
+ }
99
+ return undefined;
100
+ }
101
+ function isIdentifierBoundary(source, index) {
102
+ if (index < 0 || index >= source.length) {
103
+ return true;
104
+ }
105
+ return !/[A-Za-z0-9_$]/u.test(source[index] ?? "");
106
+ }
107
+ function skipWhitespace(source, index) {
108
+ let cursor = index;
109
+ while (cursor < source.length && /\s/u.test(source[cursor] ?? "")) {
110
+ cursor += 1;
111
+ }
112
+ return cursor;
113
+ }
114
+ function findMatchingDelimiterEnd(source, openIndex, openDelimiter, closeDelimiter) {
115
+ let depth = 0;
116
+ for (let index = openIndex; index < source.length; index += 1) {
117
+ const char = source[index];
118
+ if (char === openDelimiter) {
119
+ depth += 1;
120
+ continue;
121
+ }
122
+ if (char === closeDelimiter) {
123
+ depth -= 1;
124
+ if (depth === 0) {
125
+ return index + 1;
126
+ }
127
+ }
128
+ }
129
+ return undefined;
130
+ }
131
+ function findExecutableCallRange(source, callName) {
132
+ const maskedSource = maskTypeScriptCommentsAndLiterals(source);
133
+ let searchIndex = 0;
134
+ while (searchIndex < maskedSource.length) {
135
+ const callNameIndex = maskedSource.indexOf(callName, searchIndex);
136
+ if (callNameIndex === -1) {
137
+ return undefined;
138
+ }
139
+ const callNameEnd = callNameIndex + callName.length;
140
+ if (!isIdentifierBoundary(maskedSource, callNameIndex - 1) ||
141
+ !isIdentifierBoundary(maskedSource, callNameEnd)) {
142
+ searchIndex = callNameEnd;
143
+ continue;
144
+ }
145
+ let cursor = skipWhitespace(maskedSource, callNameEnd);
146
+ if (maskedSource[cursor] === "<") {
147
+ const genericEnd = findMatchingDelimiterEnd(maskedSource, cursor, "<", ">");
148
+ if (genericEnd === undefined) {
149
+ searchIndex = callNameEnd;
150
+ continue;
151
+ }
152
+ cursor = skipWhitespace(maskedSource, genericEnd);
153
+ }
154
+ if (maskedSource[cursor] !== "(") {
155
+ searchIndex = callNameEnd;
156
+ continue;
157
+ }
158
+ const callEnd = findMatchingDelimiterEnd(maskedSource, cursor, "(", ")");
159
+ if (callEnd === undefined) {
160
+ searchIndex = callNameEnd;
161
+ continue;
162
+ }
163
+ let end = skipWhitespace(maskedSource, callEnd);
164
+ if (maskedSource[end] === ";") {
165
+ end += 1;
166
+ }
167
+ return {
168
+ end,
169
+ start: callNameIndex,
170
+ };
171
+ }
172
+ return undefined;
173
+ }
174
+ function findBlockRegistrationCallRange(source) {
175
+ return (findExecutableCallRange(source, "registerScaffoldBlockType") ??
176
+ findExecutableCallRange(source, "registerBlockType"));
177
+ }
10
178
  function buildVariationConfigEntry(blockSlug, variationSlug) {
11
179
  return [
12
180
  "\t{",
@@ -78,36 +246,215 @@ export function registerWorkspaceVariations() {
78
246
  }
79
247
  `;
80
248
  }
249
+ function buildWorkspaceConstName(prefix, slug) {
250
+ return `workspace${prefix}_${toSnakeCase(slug)}`;
251
+ }
252
+ function buildBlockStyleConfigEntry(blockSlug, styleSlug) {
253
+ return [
254
+ "\t{",
255
+ `\t\tblock: ${quoteTsString(blockSlug)},`,
256
+ `\t\tfile: ${quoteTsString(`src/blocks/${blockSlug}/styles/${styleSlug}.ts`)},`,
257
+ `\t\tslug: ${quoteTsString(styleSlug)},`,
258
+ "\t},",
259
+ ].join("\n");
260
+ }
261
+ function buildBlockTransformConfigEntry(options) {
262
+ return [
263
+ "\t{",
264
+ `\t\tblock: ${quoteTsString(options.blockSlug)},`,
265
+ `\t\tfile: ${quoteTsString(`src/blocks/${options.blockSlug}/transforms/${options.transformSlug}.ts`)},`,
266
+ `\t\tfrom: ${quoteTsString(options.fromBlockName)},`,
267
+ `\t\tslug: ${quoteTsString(options.transformSlug)},`,
268
+ `\t\tto: ${quoteTsString(options.toBlockName)},`,
269
+ "\t},",
270
+ ].join("\n");
271
+ }
272
+ function getBlockStyleConstBindings(styleSlugs) {
273
+ const seenConstNames = new Map();
274
+ return styleSlugs.map((styleSlug) => {
275
+ const constName = buildWorkspaceConstName("BlockStyle", styleSlug);
276
+ const previousSlug = seenConstNames.get(constName);
277
+ if (previousSlug && previousSlug !== styleSlug) {
278
+ throw new Error(`Style slugs "${previousSlug}" and "${styleSlug}" generate the same registry identifier "${constName}". Rename one of the styles.`);
279
+ }
280
+ seenConstNames.set(constName, styleSlug);
281
+ return { constName, styleSlug };
282
+ });
283
+ }
284
+ function getBlockTransformConstBindings(transformSlugs) {
285
+ const seenConstNames = new Map();
286
+ return transformSlugs.map((transformSlug) => {
287
+ const constName = buildWorkspaceConstName("BlockTransform", transformSlug);
288
+ const previousSlug = seenConstNames.get(constName);
289
+ if (previousSlug && previousSlug !== transformSlug) {
290
+ throw new Error(`Transform slugs "${previousSlug}" and "${transformSlug}" generate the same registry identifier "${constName}". Rename one of the transforms.`);
291
+ }
292
+ seenConstNames.set(constName, transformSlug);
293
+ return { constName, transformSlug };
294
+ });
295
+ }
296
+ function buildBlockStyleSource(styleSlug, textDomain) {
297
+ const styleTitle = toTitleCase(styleSlug);
298
+ const styleConstName = buildWorkspaceConstName("BlockStyle", styleSlug);
299
+ return `import { __ } from '@wordpress/i18n';
300
+
301
+ export const ${styleConstName} = {
302
+ \tname: ${quoteTsString(styleSlug)},
303
+ \tlabel: __( ${quoteTsString(styleTitle)}, ${quoteTsString(textDomain)} ),
304
+ } as const;
305
+ `;
306
+ }
307
+ function buildBlockStyleIndexSource(styleSlugs) {
308
+ const styleBindings = getBlockStyleConstBindings(styleSlugs);
309
+ const importLines = styleBindings
310
+ .map(({ constName, styleSlug }) => `import { ${constName} } from './${styleSlug}';`)
311
+ .join("\n");
312
+ const styleConstNames = styleBindings.map(({ constName }) => constName).join(",\n\t");
313
+ return `import { registerBlockStyle } from '@wordpress/blocks';
314
+ import metadata from '../block.json';
315
+ ${importLines ? `\n${importLines}` : ""}
316
+
317
+ const WORKSPACE_BLOCK_STYLES = [
318
+ \t${styleConstNames}
319
+ \t// wp-typia add style entries
320
+ ] as const;
321
+
322
+ export function registerWorkspaceBlockStyles() {
323
+ \tfor (const style of WORKSPACE_BLOCK_STYLES) {
324
+ \t\tregisterBlockStyle(metadata.name, style);
325
+ \t}
326
+ }
327
+ `;
328
+ }
329
+ function buildBlockTransformSource(options) {
330
+ const transformTitle = toTitleCase(options.transformSlug);
331
+ const transformConstName = buildWorkspaceConstName("BlockTransform", options.transformSlug);
332
+ return `import { createBlock } from '@wordpress/blocks';
333
+ import { __ } from '@wordpress/i18n';
334
+ import metadata from '../block.json';
335
+
336
+ type TransformAttributes = Record<string, unknown>;
337
+ type TransformInnerBlock = ReturnType<typeof createBlock>;
338
+
339
+ function mapTransformAttributes(attributes: TransformAttributes): TransformAttributes {
340
+ \tconst content = attributes.content;
341
+
342
+ \treturn typeof content === 'string' ? { content } : {};
343
+ }
344
+
345
+ export const ${transformConstName} = {
346
+ \ttype: 'block',
347
+ \tblocks: [${quoteTsString(options.fromBlockName)}],
348
+ \ttitle: __( ${quoteTsString(transformTitle)}, ${quoteTsString(options.textDomain)} ),
349
+ \ttransform: (
350
+ \t\tattributes: TransformAttributes,
351
+ \t\tinnerBlocks: TransformInnerBlock[] = [],
352
+ \t) => createBlock(metadata.name, mapTransformAttributes(attributes), innerBlocks),
353
+ } as const;
354
+ `;
355
+ }
356
+ function buildBlockTransformIndexSource(transformSlugs) {
357
+ const transformBindings = getBlockTransformConstBindings(transformSlugs);
358
+ const importLines = transformBindings
359
+ .map(({ constName, transformSlug }) => `import { ${constName} } from './${transformSlug}';`)
360
+ .join("\n");
361
+ const transformConstNames = transformBindings
362
+ .map(({ constName }) => constName)
363
+ .join(",\n\t");
364
+ return `${importLines ? `${importLines}\n\n` : ""}type BlockSettingsWithTransforms = {
365
+ \ttransforms?: {
366
+ \t\tfrom?: unknown[];
367
+ \t\tto?: unknown[];
368
+ \t};
369
+ };
370
+
371
+ const WORKSPACE_BLOCK_TRANSFORMS = [
372
+ \t${transformConstNames}
373
+ \t// wp-typia add transform entries
374
+ ] as const;
375
+
376
+ export function applyWorkspaceBlockTransforms(settings: BlockSettingsWithTransforms) {
377
+ \tconst transforms = settings.transforms ?? {};
378
+
379
+ \tsettings.transforms = {
380
+ \t\t...transforms,
381
+ \t\tfrom: [...(transforms.from ?? []), ...WORKSPACE_BLOCK_TRANSFORMS],
382
+ \t};
383
+ }
384
+ `;
385
+ }
81
386
  async function ensureVariationRegistrationHook(blockIndexPath) {
82
387
  await patchFile(blockIndexPath, (source) => {
83
388
  let nextSource = source;
84
- if (!nextSource.includes(VARIATIONS_IMPORT_LINE)) {
389
+ if (!hasUncommentedPattern(nextSource, VARIATIONS_IMPORT_PATTERN)) {
85
390
  nextSource = `${VARIATIONS_IMPORT_LINE}\n${nextSource}`;
86
391
  }
87
- if (!nextSource.includes(VARIATIONS_CALL_LINE)) {
88
- const callInsertionPatterns = [
89
- /(registerBlockType<[\s\S]*?\);\s*)/u,
90
- /(registerBlockType\([\s\S]*?\);\s*)/u,
91
- ];
92
- let inserted = false;
93
- for (const pattern of callInsertionPatterns) {
94
- const candidate = nextSource.replace(pattern, (match) => `${match}\n${VARIATIONS_CALL_LINE}\n`);
95
- if (candidate !== nextSource) {
96
- nextSource = candidate;
97
- inserted = true;
98
- break;
99
- }
392
+ if (!hasExecutablePattern(nextSource, VARIATIONS_CALL_PATTERN)) {
393
+ const callRange = findBlockRegistrationCallRange(nextSource);
394
+ if (callRange) {
395
+ nextSource = [
396
+ nextSource.slice(0, callRange.end),
397
+ `\n${VARIATIONS_CALL_LINE}\n`,
398
+ nextSource.slice(callRange.end),
399
+ ].join("");
100
400
  }
101
- if (!inserted) {
401
+ else {
102
402
  nextSource = `${nextSource.trimEnd()}\n\n${VARIATIONS_CALL_LINE}\n`;
103
403
  }
104
404
  }
105
- if (!nextSource.includes(VARIATIONS_CALL_LINE)) {
405
+ if (!hasExecutablePattern(nextSource, VARIATIONS_CALL_PATTERN)) {
106
406
  throw new Error(`Unable to inject ${VARIATIONS_CALL_LINE} into ${path.basename(blockIndexPath)}.`);
107
407
  }
108
408
  return nextSource;
109
409
  });
110
410
  }
411
+ async function ensureBlockStyleRegistrationHook(blockIndexPath) {
412
+ await patchFile(blockIndexPath, (source) => {
413
+ let nextSource = source;
414
+ if (!hasUncommentedPattern(nextSource, BLOCK_STYLES_IMPORT_PATTERN)) {
415
+ nextSource = `${BLOCK_STYLES_IMPORT_LINE}\n${nextSource}`;
416
+ }
417
+ if (!hasExecutablePattern(nextSource, BLOCK_STYLES_CALL_PATTERN)) {
418
+ const callRange = findBlockRegistrationCallRange(nextSource);
419
+ if (callRange) {
420
+ nextSource = [
421
+ nextSource.slice(0, callRange.end),
422
+ `\n${BLOCK_STYLES_CALL_LINE}\n`,
423
+ nextSource.slice(callRange.end),
424
+ ].join("");
425
+ }
426
+ else {
427
+ nextSource = `${nextSource.trimEnd()}\n\n${BLOCK_STYLES_CALL_LINE}\n`;
428
+ }
429
+ }
430
+ if (!hasExecutablePattern(nextSource, BLOCK_STYLES_CALL_PATTERN)) {
431
+ throw new Error(`Unable to inject ${BLOCK_STYLES_CALL_LINE} into ${path.basename(blockIndexPath)}.`);
432
+ }
433
+ return nextSource;
434
+ });
435
+ }
436
+ async function ensureBlockTransformRegistrationHook(blockIndexPath) {
437
+ await patchFile(blockIndexPath, (source) => {
438
+ let nextSource = source;
439
+ if (!hasUncommentedPattern(nextSource, BLOCK_TRANSFORMS_IMPORT_PATTERN)) {
440
+ nextSource = `${BLOCK_TRANSFORMS_IMPORT_LINE}\n${nextSource}`;
441
+ }
442
+ if (!hasExecutablePattern(nextSource, BLOCK_TRANSFORMS_CALL_PATTERN)) {
443
+ const callRange = findExecutablePatternMatch(nextSource, [
444
+ SCAFFOLD_REGISTRATION_SETTINGS_CALL_PATTERN,
445
+ ]);
446
+ if (!callRange) {
447
+ throw new Error(`Unable to inject ${BLOCK_TRANSFORMS_CALL_LINE} into ${path.basename(blockIndexPath)} because it does not expose a scaffold registration settings object.`);
448
+ }
449
+ nextSource = [
450
+ nextSource.slice(0, callRange.start),
451
+ `${BLOCK_TRANSFORMS_CALL_LINE}\n`,
452
+ nextSource.slice(callRange.start),
453
+ ].join("");
454
+ }
455
+ return nextSource;
456
+ });
457
+ }
111
458
  async function writeVariationRegistry(projectDir, blockSlug, variationSlug) {
112
459
  const variationsDir = path.join(projectDir, "src", "blocks", blockSlug, "variations");
113
460
  const variationsIndexPath = path.join(variationsDir, "index.ts");
@@ -119,6 +466,83 @@ async function writeVariationRegistry(projectDir, blockSlug, variationSlug) {
119
466
  const nextVariationSlugs = Array.from(new Set([...existingVariationSlugs, variationSlug])).sort();
120
467
  await fsp.writeFile(variationsIndexPath, buildVariationIndexSource(nextVariationSlugs), "utf8");
121
468
  }
469
+ async function writeBlockStyleRegistry(projectDir, blockSlug, styleSlug) {
470
+ const stylesDir = path.join(projectDir, "src", "blocks", blockSlug, "styles");
471
+ const stylesIndexPath = path.join(stylesDir, "index.ts");
472
+ await fsp.mkdir(stylesDir, { recursive: true });
473
+ const existingStyleSlugs = fs
474
+ .readdirSync(stylesDir)
475
+ .filter((entry) => entry.endsWith(".ts") && entry !== "index.ts")
476
+ .map((entry) => entry.replace(/\.ts$/u, ""));
477
+ const nextStyleSlugs = Array.from(new Set([...existingStyleSlugs, styleSlug])).sort();
478
+ await fsp.writeFile(stylesIndexPath, buildBlockStyleIndexSource(nextStyleSlugs), "utf8");
479
+ }
480
+ async function writeBlockTransformRegistry(projectDir, blockSlug, transformSlug) {
481
+ const transformsDir = path.join(projectDir, "src", "blocks", blockSlug, "transforms");
482
+ const transformsIndexPath = path.join(transformsDir, "index.ts");
483
+ await fsp.mkdir(transformsDir, { recursive: true });
484
+ const existingTransformSlugs = fs
485
+ .readdirSync(transformsDir)
486
+ .filter((entry) => entry.endsWith(".ts") && entry !== "index.ts")
487
+ .map((entry) => entry.replace(/\.ts$/u, ""));
488
+ const nextTransformSlugs = Array.from(new Set([...existingTransformSlugs, transformSlug])).sort();
489
+ await fsp.writeFile(transformsIndexPath, buildBlockTransformIndexSource(nextTransformSlugs), "utf8");
490
+ }
491
+ function assertBlockStyleDoesNotExist(projectDir, blockSlug, styleSlug, inventory) {
492
+ const stylePath = path.join(projectDir, "src", "blocks", blockSlug, "styles", `${styleSlug}.ts`);
493
+ if (fs.existsSync(stylePath)) {
494
+ throw new Error(`A block style already exists at ${path.relative(projectDir, stylePath)}. Choose a different name.`);
495
+ }
496
+ if (inventory.blockStyles.some((entry) => entry.block === blockSlug && entry.slug === styleSlug)) {
497
+ throw new Error(`A block style inventory entry already exists for ${blockSlug}/${styleSlug}. Choose a different name.`);
498
+ }
499
+ }
500
+ function assertBlockTransformDoesNotExist(projectDir, blockSlug, transformSlug, inventory) {
501
+ const transformPath = path.join(projectDir, "src", "blocks", blockSlug, "transforms", `${transformSlug}.ts`);
502
+ if (fs.existsSync(transformPath)) {
503
+ throw new Error(`A block transform already exists at ${path.relative(projectDir, transformPath)}. Choose a different name.`);
504
+ }
505
+ if (inventory.blockTransforms.some((entry) => entry.block === blockSlug && entry.slug === transformSlug)) {
506
+ throw new Error(`A block transform inventory entry already exists for ${blockSlug}/${transformSlug}. Choose a different name.`);
507
+ }
508
+ }
509
+ function assertFullBlockName(blockName, flagName) {
510
+ const trimmed = blockName.trim();
511
+ if (!trimmed) {
512
+ throw new Error(`\`${flagName}\` requires a block name.`);
513
+ }
514
+ if (!FULL_BLOCK_NAME_PATTERN.test(trimmed)) {
515
+ throw new Error(`\`${flagName}\` must use <namespace/block-slug> format.`);
516
+ }
517
+ return trimmed;
518
+ }
519
+ function resolveWorkspaceTargetBlockName(blockName, namespace, flagName) {
520
+ const trimmed = blockName.trim();
521
+ if (!trimmed) {
522
+ throw new Error(`\`${flagName}\` requires <block-slug|namespace/block-slug>.`);
523
+ }
524
+ const blockNameSegments = trimmed.split("/");
525
+ if (blockNameSegments.length > 2 ||
526
+ blockNameSegments.some((segment) => segment.trim() === "")) {
527
+ throw new Error(`\`${flagName}\` must use <block-slug|namespace/block-slug> format.`);
528
+ }
529
+ const [maybeNamespace, maybeSlug] = blockNameSegments.length === 2
530
+ ? blockNameSegments
531
+ : [undefined, blockNameSegments[0]];
532
+ if (maybeNamespace && maybeNamespace !== namespace) {
533
+ throw new Error(`\`${flagName}\` references namespace "${maybeNamespace}". Expected "${namespace}".`);
534
+ }
535
+ const blockSlug = normalizeBlockSlug(maybeSlug ?? "");
536
+ return {
537
+ blockName: `${namespace}/${blockSlug}`,
538
+ blockSlug,
539
+ };
540
+ }
541
+ /**
542
+ * Re-export the DataViews admin screen scaffold workflow from the focused
543
+ * admin-view runtime helper module.
544
+ */
545
+ export { runAddAdminViewCommand, } from "./cli-add-workspace-admin-view.js";
122
546
  /**
123
547
  * Re-export focused workspace asset scaffold commands from the companion
124
548
  * `cli-add-workspace-assets` module.
@@ -167,6 +591,7 @@ export async function runAddVariationCommand({ blockName, cwd = process.cwd(), v
167
591
  const variationsDir = path.join(workspace.projectDir, "src", "blocks", blockSlug, "variations");
168
592
  const variationFilePath = path.join(variationsDir, `${variationSlug}.ts`);
169
593
  const variationsIndexPath = path.join(variationsDir, "index.ts");
594
+ const shouldRemoveVariationsDirOnRollback = !fs.existsSync(variationsDir);
170
595
  const mutationSnapshot = {
171
596
  fileSources: await snapshotWorkspaceFiles([
172
597
  blockConfigPath,
@@ -174,7 +599,10 @@ export async function runAddVariationCommand({ blockName, cwd = process.cwd(), v
174
599
  variationsIndexPath,
175
600
  ]),
176
601
  snapshotDirs: [],
177
- targetPaths: [variationFilePath],
602
+ targetPaths: [
603
+ variationFilePath,
604
+ ...(shouldRemoveVariationsDirOnRollback ? [variationsDir] : []),
605
+ ],
178
606
  };
179
607
  try {
180
608
  await fsp.mkdir(variationsDir, { recursive: true });
@@ -195,6 +623,148 @@ export async function runAddVariationCommand({ blockName, cwd = process.cwd(), v
195
623
  throw error;
196
624
  }
197
625
  }
626
+ /**
627
+ * Add one Block Styles registration to an existing workspace block.
628
+ *
629
+ * @param options Command options for the Block Styles scaffold workflow.
630
+ * @param options.blockName Target workspace block slug that will own the style.
631
+ * @param options.cwd Working directory used to resolve the nearest official workspace.
632
+ * Defaults to `process.cwd()`.
633
+ * @param options.styleName Human-entered style name that will be normalized and
634
+ * validated before files are written.
635
+ * @returns A promise that resolves with the normalized `blockSlug`, `styleSlug`,
636
+ * and owning `projectDir` after the style module, style registry, entrypoint
637
+ * hook, and inventory entry have been written successfully.
638
+ * @throws {Error} When the command is run outside an official workspace, when
639
+ * the target block is unknown, when the style slug is invalid, or when a
640
+ * conflicting file or inventory entry already exists.
641
+ */
642
+ export async function runAddBlockStyleCommand({ blockName, cwd = process.cwd(), styleName, }) {
643
+ const workspace = resolveWorkspaceProject(cwd);
644
+ const blockSlug = normalizeBlockSlug(blockName);
645
+ const styleSlug = assertValidGeneratedSlug("Style name", normalizeBlockSlug(styleName), "wp-typia add style <name> --block <block-slug>");
646
+ const inventory = readWorkspaceInventory(workspace.projectDir);
647
+ resolveWorkspaceBlock(inventory, blockSlug);
648
+ assertBlockStyleDoesNotExist(workspace.projectDir, blockSlug, styleSlug, inventory);
649
+ const blockConfigPath = path.join(workspace.projectDir, "scripts", "block-config.ts");
650
+ const blockIndexPath = path.join(workspace.projectDir, "src", "blocks", blockSlug, "index.tsx");
651
+ const stylesDir = path.join(workspace.projectDir, "src", "blocks", blockSlug, "styles");
652
+ const styleFilePath = path.join(stylesDir, `${styleSlug}.ts`);
653
+ const stylesIndexPath = path.join(stylesDir, "index.ts");
654
+ const shouldRemoveStylesDirOnRollback = !fs.existsSync(stylesDir);
655
+ const mutationSnapshot = {
656
+ fileSources: await snapshotWorkspaceFiles([
657
+ blockConfigPath,
658
+ blockIndexPath,
659
+ stylesIndexPath,
660
+ ]),
661
+ snapshotDirs: [],
662
+ targetPaths: [
663
+ styleFilePath,
664
+ ...(shouldRemoveStylesDirOnRollback ? [stylesDir] : []),
665
+ ],
666
+ };
667
+ try {
668
+ await fsp.mkdir(stylesDir, { recursive: true });
669
+ await fsp.writeFile(styleFilePath, buildBlockStyleSource(styleSlug, workspace.workspace.textDomain), "utf8");
670
+ await writeBlockStyleRegistry(workspace.projectDir, blockSlug, styleSlug);
671
+ await ensureBlockStyleRegistrationHook(blockIndexPath);
672
+ await appendWorkspaceInventoryEntries(workspace.projectDir, {
673
+ blockStyleEntries: [buildBlockStyleConfigEntry(blockSlug, styleSlug)],
674
+ });
675
+ return {
676
+ blockSlug,
677
+ projectDir: workspace.projectDir,
678
+ styleSlug,
679
+ };
680
+ }
681
+ catch (error) {
682
+ await rollbackWorkspaceMutation(mutationSnapshot);
683
+ throw error;
684
+ }
685
+ }
686
+ /**
687
+ * Add one block-to-block transform registration to an existing workspace block.
688
+ *
689
+ * @param options Command options for the block transform scaffold workflow.
690
+ * @param options.cwd Working directory used to resolve the nearest official workspace.
691
+ * Defaults to `process.cwd()`.
692
+ * @param options.fromBlockName Source block name for `--from`. This must be the
693
+ * full `namespace/block` form because transforms may originate from WordPress
694
+ * core or third-party blocks outside the workspace.
695
+ * @param options.toBlockName Target block for `--to`. A workspace block slug is
696
+ * resolved against the workspace namespace, while a full `namespace/block` name
697
+ * must still point at an existing workspace block.
698
+ * @param options.transformName Human-entered transform name that will be
699
+ * normalized and validated before files are written.
700
+ * @returns A promise that resolves with the normalized target `blockSlug`,
701
+ * resolved `fromBlockName`, resolved `toBlockName`, `transformSlug`, and owning
702
+ * `projectDir` after the transform module, transform registry, entrypoint hook,
703
+ * and inventory entry have been written successfully.
704
+ * @throws {Error} When the command is run outside an official workspace, when
705
+ * the target block is unknown, when `--from` is not a full block name, when
706
+ * `--to` uses a non-workspace namespace, when the target block entrypoint does
707
+ * not expose `registration.settings`, when the transform slug is invalid, or
708
+ * when a conflicting file or inventory entry already exists.
709
+ */
710
+ export async function runAddBlockTransformCommand({ cwd = process.cwd(), fromBlockName, toBlockName, transformName, }) {
711
+ const workspace = resolveWorkspaceProject(cwd);
712
+ const transformSlug = assertValidGeneratedSlug("Transform name", normalizeBlockSlug(transformName), "wp-typia add transform <name> --from <namespace/block> --to <block-slug|namespace/block-slug>");
713
+ const resolvedFromBlockName = assertFullBlockName(fromBlockName, "--from");
714
+ const target = resolveWorkspaceTargetBlockName(toBlockName, workspace.workspace.namespace, "--to");
715
+ const inventory = readWorkspaceInventory(workspace.projectDir);
716
+ resolveWorkspaceBlock(inventory, target.blockSlug);
717
+ assertBlockTransformDoesNotExist(workspace.projectDir, target.blockSlug, transformSlug, inventory);
718
+ const blockConfigPath = path.join(workspace.projectDir, "scripts", "block-config.ts");
719
+ const blockIndexPath = path.join(workspace.projectDir, "src", "blocks", target.blockSlug, "index.tsx");
720
+ const transformsDir = path.join(workspace.projectDir, "src", "blocks", target.blockSlug, "transforms");
721
+ const transformFilePath = path.join(transformsDir, `${transformSlug}.ts`);
722
+ const transformsIndexPath = path.join(transformsDir, "index.ts");
723
+ const shouldRemoveTransformsDirOnRollback = !fs.existsSync(transformsDir);
724
+ const mutationSnapshot = {
725
+ fileSources: await snapshotWorkspaceFiles([
726
+ blockConfigPath,
727
+ blockIndexPath,
728
+ transformsIndexPath,
729
+ ]),
730
+ snapshotDirs: [],
731
+ targetPaths: [
732
+ transformFilePath,
733
+ ...(shouldRemoveTransformsDirOnRollback ? [transformsDir] : []),
734
+ ],
735
+ };
736
+ try {
737
+ await fsp.mkdir(transformsDir, { recursive: true });
738
+ await fsp.writeFile(transformFilePath, buildBlockTransformSource({
739
+ fromBlockName: resolvedFromBlockName,
740
+ textDomain: workspace.workspace.textDomain,
741
+ transformSlug,
742
+ }), "utf8");
743
+ await writeBlockTransformRegistry(workspace.projectDir, target.blockSlug, transformSlug);
744
+ await ensureBlockTransformRegistrationHook(blockIndexPath);
745
+ await appendWorkspaceInventoryEntries(workspace.projectDir, {
746
+ blockTransformEntries: [
747
+ buildBlockTransformConfigEntry({
748
+ blockSlug: target.blockSlug,
749
+ fromBlockName: resolvedFromBlockName,
750
+ toBlockName: target.blockName,
751
+ transformSlug,
752
+ }),
753
+ ],
754
+ });
755
+ return {
756
+ blockSlug: target.blockSlug,
757
+ fromBlockName: resolvedFromBlockName,
758
+ projectDir: workspace.projectDir,
759
+ toBlockName: target.blockName,
760
+ transformSlug,
761
+ };
762
+ }
763
+ catch (error) {
764
+ await rollbackWorkspaceMutation(mutationSnapshot);
765
+ throw error;
766
+ }
767
+ }
198
768
  /**
199
769
  * Add one `blockHooks` entry to an existing official workspace block.
200
770
  *
@@ -10,5 +10,5 @@
10
10
  export { ADD_BLOCK_TEMPLATE_IDS, ADD_KIND_IDS, EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, } from "./cli-add-shared.js";
11
11
  export type { AddBlockTemplateId, AddKindId, EditorPluginSlotId, } from "./cli-add-shared.js";
12
12
  export { runAddBlockCommand, seedWorkspaceMigrationProject, } from "./cli-add-block.js";
13
- export { runAddBindingSourceCommand, runAddAbilityCommand, runAddAiFeatureCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddRestResourceCommand, runAddVariationCommand, } from "./cli-add-workspace.js";
13
+ export { runAddAdminViewCommand, runAddAbilityCommand, runAddAiFeatureCommand, runAddBindingSourceCommand, runAddBlockStyleCommand, runAddBlockTransformCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddRestResourceCommand, runAddVariationCommand, } from "./cli-add-workspace.js";
14
14
  export { getWorkspaceBlockSelectOptions } from "./workspace-inventory.js";
@@ -9,5 +9,5 @@
9
9
  */
10
10
  export { ADD_BLOCK_TEMPLATE_IDS, ADD_KIND_IDS, EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, } from "./cli-add-shared.js";
11
11
  export { runAddBlockCommand, seedWorkspaceMigrationProject, } from "./cli-add-block.js";
12
- export { runAddBindingSourceCommand, runAddAbilityCommand, runAddAiFeatureCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddRestResourceCommand, runAddVariationCommand, } from "./cli-add-workspace.js";
12
+ export { runAddAdminViewCommand, runAddAbilityCommand, runAddAiFeatureCommand, runAddBindingSourceCommand, runAddBlockStyleCommand, runAddBlockTransformCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddRestResourceCommand, runAddVariationCommand, } from "./cli-add-workspace.js";
13
13
  export { getWorkspaceBlockSelectOptions } from "./workspace-inventory.js";