@wp-typia/project-tools 0.16.8 → 0.16.10

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 (50) hide show
  1. package/README.md +14 -4
  2. package/dist/runtime/block-generator-service.d.ts +5 -1
  3. package/dist/runtime/block-generator-service.js +7 -3
  4. package/dist/runtime/built-in-block-artifacts.js +388 -572
  5. package/dist/runtime/built-in-block-code-artifacts.js +96 -46
  6. package/dist/runtime/built-in-block-code-templates.d.ts +36 -0
  7. package/dist/runtime/built-in-block-code-templates.js +2234 -0
  8. package/dist/runtime/cli-add-block.d.ts +2 -1
  9. package/dist/runtime/cli-add-block.js +163 -25
  10. package/dist/runtime/cli-add-shared.d.ts +7 -0
  11. package/dist/runtime/cli-add-shared.js +4 -6
  12. package/dist/runtime/cli-add-workspace.js +56 -17
  13. package/dist/runtime/cli-core.d.ts +4 -0
  14. package/dist/runtime/cli-core.js +3 -0
  15. package/dist/runtime/cli-diagnostics.d.ts +58 -0
  16. package/dist/runtime/cli-diagnostics.js +101 -0
  17. package/dist/runtime/cli-doctor.d.ts +2 -1
  18. package/dist/runtime/cli-doctor.js +16 -5
  19. package/dist/runtime/cli-help.js +4 -4
  20. package/dist/runtime/cli-scaffold.d.ts +5 -1
  21. package/dist/runtime/cli-scaffold.js +138 -111
  22. package/dist/runtime/external-layer-selection.d.ts +14 -0
  23. package/dist/runtime/external-layer-selection.js +35 -0
  24. package/dist/runtime/index.d.ts +2 -2
  25. package/dist/runtime/index.js +1 -1
  26. package/dist/runtime/migration-render.d.ts +23 -1
  27. package/dist/runtime/migration-render.js +58 -10
  28. package/dist/runtime/migration-ui-capability.js +17 -8
  29. package/dist/runtime/migration-utils.d.ts +7 -6
  30. package/dist/runtime/migration-utils.js +76 -73
  31. package/dist/runtime/migrations.js +2 -2
  32. package/dist/runtime/object-utils.d.ts +8 -1
  33. package/dist/runtime/object-utils.js +21 -1
  34. package/dist/runtime/scaffold-apply-utils.d.ts +14 -2
  35. package/dist/runtime/scaffold-apply-utils.js +19 -6
  36. package/dist/runtime/scaffold-repository-reference.d.ts +22 -0
  37. package/dist/runtime/scaffold-repository-reference.js +119 -0
  38. package/dist/runtime/scaffold.d.ts +5 -1
  39. package/dist/runtime/scaffold.js +15 -37
  40. package/dist/runtime/template-layers.d.ts +6 -0
  41. package/dist/runtime/template-layers.js +20 -7
  42. package/dist/runtime/template-render.d.ts +13 -2
  43. package/dist/runtime/template-render.js +102 -71
  44. package/dist/runtime/template-source.d.ts +6 -5
  45. package/dist/runtime/template-source.js +284 -217
  46. package/package.json +8 -3
  47. package/templates/_shared/base/src/validator-toolkit.ts.mustache +2 -2
  48. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +61 -16
  49. package/templates/_shared/migration-ui/common/src/migrations/helpers.ts +19 -47
  50. package/templates/_shared/migration-ui/common/src/migrations/index.ts +40 -11
@@ -29,8 +29,9 @@ export declare function seedWorkspaceMigrationProject(projectDir: string, curren
29
29
  * with unsupported templates, the command runs outside an official workspace,
30
30
  * or target block paths already exist.
31
31
  */
32
- export declare function runAddBlockCommand({ blockName, cwd, dataStorageMode, persistencePolicy, templateId, }: RunAddBlockCommandOptions): Promise<{
32
+ export declare function runAddBlockCommand({ blockName, cwd, dataStorageMode, externalLayerId, externalLayerSource, persistencePolicy, selectExternalLayerId, templateId, }: RunAddBlockCommandOptions): Promise<{
33
33
  blockSlugs: string[];
34
34
  projectDir: string;
35
35
  templateId: AddBlockTemplateId;
36
+ warnings: string[];
36
37
  }>;
@@ -7,11 +7,14 @@ import { ensureMigrationDirectories, parseMigrationConfig, writeInitialMigration
7
7
  import { syncPersistenceRestArtifacts, } from "./persistence-rest-artifacts.js";
8
8
  import { snapshotProjectVersion } from "./migrations.js";
9
9
  import { getDefaultAnswers, scaffoldProject } from "./scaffold.js";
10
- import { copyInterpolatedDirectory, } from "./template-render.js";
10
+ import { copyInterpolatedDirectory, listInterpolatedDirectoryOutputs, } from "./template-render.js";
11
11
  import { SHARED_WORKSPACE_TEMPLATE_ROOT, } from "./template-registry.js";
12
12
  import { appendWorkspaceInventoryEntries, } from "./workspace-inventory.js";
13
13
  import { resolveWorkspaceProject, } from "./workspace-project.js";
14
14
  import { ADD_BLOCK_TEMPLATE_IDS, buildWorkspacePhpPrefix, isAddBlockTemplateId, normalizeBlockSlug, patchFile, quoteTsString, readOptionalFile, rollbackWorkspaceMutation, snapshotWorkspaceFiles, } from "./cli-add-shared.js";
15
+ import { parseTemplateLocator, resolveTemplateSeed, } from "./template-source.js";
16
+ import { resolveExternalTemplateLayers, } from "./template-layers.js";
17
+ import { resolveOptionalInteractiveExternalLayerId, } from "./external-layer-selection.js";
15
18
  const COLLECTION_IMPORT_LINE = "import '../../collection';";
16
19
  const REST_MANIFEST_IMPORT_PATTERN = /import\s*\{[^}]*\bdefineEndpointManifest\b[^}]*\}\s*from\s*["']@wp-typia\/block-runtime\/metadata-core["'];?/m;
17
20
  function buildServerTemplateRoot(persistencePolicy) {
@@ -159,6 +162,9 @@ async function ensureCollectionImport(filePath) {
159
162
  if (source.includes(COLLECTION_IMPORT_LINE)) {
160
163
  return source;
161
164
  }
165
+ if (source.includes("import metadata from './block-metadata';")) {
166
+ return source.replace("import metadata from './block-metadata';", `${COLLECTION_IMPORT_LINE}\nimport metadata from './block-metadata';`);
167
+ }
162
168
  if (source.includes("import metadata from './block.json';")) {
163
169
  return source.replace("import metadata from './block.json';", `${COLLECTION_IMPORT_LINE}\nimport metadata from './block.json';`);
164
170
  }
@@ -198,6 +204,7 @@ async function renderWorkspacePersistenceServerModule(projectDir, variables) {
198
204
  const COMPOUND_SHARED_SUPPORT_FILES = ["hooks.ts", "validator-toolkit.ts"];
199
205
  const LEGACY_ASSERT_PATTERN = /assert:\s*typia\.createAssert</u;
200
206
  const LEGACY_MANIFEST_PATTERN = /\r?\n[ \t]*manifest:\s*currentManifest,/u;
207
+ const LEGACY_VALIDATOR_MANIFEST_IMPORT_PATTERN = /^[\uFEFF \t]*import\s+currentManifest\s+from\s*["']\.\/typia\.manifest\.json["'];?$/u;
201
208
  const LEGACY_TOOLKIT_CALL_PATTERN = /createTemplateValidatorToolkit<\s*(?<typeName>[A-Za-z0-9_]+)\s*>\s*\(\s*\{/u;
202
209
  const LEGACY_VALIDATOR_TOOLKIT_IMPORT_PATTERN = /from\s*["']\.\.\/\.\.\/validator-toolkit["']/u;
203
210
  const TYPIA_IMPORT_PATTERN = /^[\uFEFF \t]*import\s+typia\s+from\s*["']typia["'];?/mu;
@@ -211,6 +218,21 @@ const COMPATIBLE_COMPOUND_TOOLKIT_PATTERNS = [
211
218
  /\bvalidate\s*:\s*ScaffoldValidatorToolkitOptions\s*<\s*T\s*>\s*\[\s*["']validate["']\s*\]/u,
212
219
  /createTemplateValidatorToolkit\s*<\s*T\s+extends\s+object\s*>\s*\(\s*\{/u,
213
220
  ];
221
+ function normalizeExternalLayerOption(value) {
222
+ if (typeof value !== "string") {
223
+ return undefined;
224
+ }
225
+ const trimmed = value.trim();
226
+ return trimmed.length > 0 ? trimmed : undefined;
227
+ }
228
+ function resolveExternalLayerSourceFromCaller(source, callerCwd) {
229
+ if (typeof source !== "string" ||
230
+ source.length === 0 ||
231
+ !(path.isAbsolute(source) || source.startsWith("./") || source.startsWith("../"))) {
232
+ return source;
233
+ }
234
+ return path.resolve(callerCwd, source);
235
+ }
214
236
  function shouldRefreshCompoundValidatorToolkit(source) {
215
237
  return (source === null ||
216
238
  !COMPATIBLE_COMPOUND_TOOLKIT_PATTERNS.every((pattern) => pattern.test(source)));
@@ -223,6 +245,36 @@ function isLegacyCompoundValidatorSource(source) {
223
245
  function hasTypiaImport(source) {
224
246
  return TYPIA_IMPORT_PATTERN.test(source.replace(/\/\*[\s\S]*?\*\//gu, ""));
225
247
  }
248
+ function replaceFirstNonCommentLine(source, pattern, replacement) {
249
+ const lineEnding = source.includes("\r\n") ? "\r\n" : "\n";
250
+ const lines = source.split(/\r?\n/);
251
+ let inBlockComment = false;
252
+ for (let index = 0; index < lines.length; index += 1) {
253
+ const line = lines[index] ?? "";
254
+ const trimmed = line.trimStart();
255
+ if (inBlockComment) {
256
+ if (trimmed.includes("*/")) {
257
+ inBlockComment = false;
258
+ }
259
+ continue;
260
+ }
261
+ if (trimmed.startsWith("//")) {
262
+ continue;
263
+ }
264
+ if (trimmed.startsWith("/*")) {
265
+ if (!trimmed.includes("*/")) {
266
+ inBlockComment = true;
267
+ }
268
+ continue;
269
+ }
270
+ if (!pattern.test(line)) {
271
+ continue;
272
+ }
273
+ lines[index] = replacement;
274
+ return lines.join(lineEnding);
275
+ }
276
+ return source;
277
+ }
226
278
  function upgradeLegacyCompoundValidatorSource(source) {
227
279
  const typeNameMatch = source.match(LEGACY_TOOLKIT_CALL_PATTERN);
228
280
  const typeName = typeNameMatch?.groups?.typeName;
@@ -233,6 +285,7 @@ function upgradeLegacyCompoundValidatorSource(source) {
233
285
  if (!hasTypiaImport(nextSource)) {
234
286
  nextSource = `import typia from 'typia';\n${nextSource}`;
235
287
  }
288
+ nextSource = replaceFirstNonCommentLine(nextSource, LEGACY_VALIDATOR_MANIFEST_IMPORT_PATTERN, "import currentManifest from './manifest-defaults-document';");
236
289
  nextSource = nextSource.replace(LEGACY_TOOLKIT_CALL_PATTERN, [
237
290
  `createTemplateValidatorToolkit< ${typeName} >( {`,
238
291
  `\tassert: typia.createAssert< ${typeName} >(),`,
@@ -255,6 +308,26 @@ function upgradeLegacyCompoundValidatorSource(source) {
255
308
  }
256
309
  return replacedManifest;
257
310
  }
311
+ function renderLegacyManifestDefaultsWrapperSource() {
312
+ return [
313
+ "import rawCurrentManifest from './typia.manifest.json';",
314
+ "import { defineManifestDefaultsDocument } from '@wp-typia/block-runtime/defaults';",
315
+ "",
316
+ "const currentManifest = defineManifestDefaultsDocument( rawCurrentManifest );",
317
+ "",
318
+ "export default currentManifest;",
319
+ "",
320
+ ].join("\n");
321
+ }
322
+ async function ensureLegacyCompoundValidatorManifestDefaultsWrapper(validatorPath) {
323
+ const validatorDir = path.dirname(validatorPath);
324
+ const wrapperPath = path.join(validatorDir, "manifest-defaults-document.ts");
325
+ const manifestPath = path.join(validatorDir, "typia.manifest.json");
326
+ if (fs.existsSync(wrapperPath) || !fs.existsSync(manifestPath)) {
327
+ return;
328
+ }
329
+ await fsp.writeFile(wrapperPath, renderLegacyManifestDefaultsWrapperSource(), "utf8");
330
+ }
258
331
  async function collectLegacyCompoundValidatorPaths(projectDir) {
259
332
  const blocksDir = path.join(projectDir, "src", "blocks");
260
333
  if (!fs.existsSync(blocksDir)) {
@@ -290,6 +363,7 @@ async function ensureCompoundWorkspaceSupportFiles(projectDir, tempProjectDir, l
290
363
  if (!isLegacyCompoundValidatorSource(currentSource)) {
291
364
  continue;
292
365
  }
366
+ await ensureLegacyCompoundValidatorManifestDefaultsWrapper(validatorPath);
293
367
  await fsp.writeFile(validatorPath, upgradeLegacyCompoundValidatorSource(currentSource), "utf8");
294
368
  }
295
369
  }
@@ -308,6 +382,47 @@ async function copyScaffoldedBlockSlice(projectDir, templateId, tempProjectDir,
308
382
  await renderWorkspacePersistenceServerModule(projectDir, variables);
309
383
  }
310
384
  }
385
+ function isSupportedAddBlockLayerOutput(options) {
386
+ const { relativePath, templateId, variables } = options;
387
+ if (templateId === "compound") {
388
+ return (relativePath === "src/hooks.ts" ||
389
+ relativePath === "src/validator-toolkit.ts" ||
390
+ relativePath.startsWith(`src/blocks/${variables.slugKebabCase}/`) ||
391
+ relativePath.startsWith(`src/blocks/${variables.slugKebabCase}-item/`));
392
+ }
393
+ return relativePath.startsWith("src/");
394
+ }
395
+ async function assertAddBlockSupportsExternalLayerOutputs(options) {
396
+ const { callerCwd, externalLayerId, externalLayerSource, templateId, variables, } = options;
397
+ if (!externalLayerSource) {
398
+ return;
399
+ }
400
+ const layerSeed = await resolveTemplateSeed(parseTemplateLocator(externalLayerSource), callerCwd);
401
+ try {
402
+ const resolvedLayers = await resolveExternalTemplateLayers({
403
+ externalLayerId,
404
+ sourceRoot: layerSeed.rootDir,
405
+ });
406
+ for (const entry of resolvedLayers.entries) {
407
+ if (entry.kind !== "external") {
408
+ continue;
409
+ }
410
+ for (const relativePath of await listInterpolatedDirectoryOutputs(entry.dir, variables)) {
411
+ if (isSupportedAddBlockLayerOutput({
412
+ relativePath,
413
+ templateId,
414
+ variables,
415
+ })) {
416
+ continue;
417
+ }
418
+ throw new Error(`External layer "${entry.id}" writes workspace-level output "${relativePath}", which \`wp-typia add block\` cannot merge safely. Restrict the layer to block-local files or scaffold it through \`wp-typia create\` instead.`);
419
+ }
420
+ }
421
+ }
422
+ finally {
423
+ await layerSeed.cleanup?.();
424
+ }
425
+ }
311
426
  function collectWorkspaceBlockPaths(projectDir, templateId, variables) {
312
427
  if (templateId === "compound") {
313
428
  return [
@@ -433,20 +548,28 @@ export async function seedWorkspaceMigrationProject(projectDir, currentMigration
433
548
  * with unsupported templates, the command runs outside an official workspace,
434
549
  * or target block paths already exist.
435
550
  */
436
- export async function runAddBlockCommand({ blockName, cwd = process.cwd(), dataStorageMode, persistencePolicy, templateId = "basic", }) {
551
+ export async function runAddBlockCommand({ blockName, cwd = process.cwd(), dataStorageMode, externalLayerId, externalLayerSource, persistencePolicy, selectExternalLayerId, templateId = "basic", }) {
437
552
  if (!isAddBlockTemplateId(templateId)) {
438
553
  throw new Error(`Unknown add-block template "${templateId}". Expected one of: ${ADD_BLOCK_TEMPLATE_IDS.join(", ")}`);
439
554
  }
440
555
  const resolvedTemplateId = templateId;
441
556
  assertPersistenceFlagsAllowed(resolvedTemplateId, { dataStorageMode, persistencePolicy });
442
557
  const workspace = resolveWorkspaceProject(cwd);
443
- const normalizedSlug = normalizeBlockSlug(blockName);
444
- if (!normalizedSlug) {
445
- throw new Error("Block name is required. Use `wp-typia add block <name> --template <family>`.");
446
- }
447
- const defaults = getDefaultAnswers(normalizedSlug, resolvedTemplateId);
558
+ const normalizedExternalLayerId = normalizeExternalLayerOption(externalLayerId);
559
+ const normalizedExternalLayerSource = resolveExternalLayerSourceFromCaller(normalizeExternalLayerOption(externalLayerSource), cwd);
560
+ const resolvedExternalLayerSelection = await resolveOptionalInteractiveExternalLayerId({
561
+ callerCwd: cwd,
562
+ externalLayerId: normalizedExternalLayerId,
563
+ externalLayerSource: normalizedExternalLayerSource,
564
+ selectExternalLayerId,
565
+ });
448
566
  let tempRoot = "";
449
567
  try {
568
+ const normalizedSlug = normalizeBlockSlug(blockName);
569
+ if (!normalizedSlug) {
570
+ throw new Error("Block name is required. Use `wp-typia add block <name> --template <family>`.");
571
+ }
572
+ const defaults = getDefaultAnswers(normalizedSlug, resolvedTemplateId);
450
573
  tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "wp-typia-add-block-"));
451
574
  const tempProjectDir = path.join(tempRoot, normalizedSlug);
452
575
  const blockConfigPath = path.join(workspace.projectDir, "scripts", "block-config.ts");
@@ -460,24 +583,37 @@ export async function runAddBlockCommand({ blockName, cwd = process.cwd(), dataS
460
583
  const legacyCompoundValidatorPaths = resolvedTemplateId === "compound"
461
584
  ? await collectLegacyCompoundValidatorPaths(workspace.projectDir)
462
585
  : [];
463
- const result = await scaffoldProject({
464
- answers: {
465
- ...defaults,
466
- author: workspace.author,
467
- namespace: workspace.workspace.namespace,
468
- phpPrefix: blockPhpPrefix,
469
- slug: normalizedSlug,
470
- textDomain: workspace.workspace.textDomain,
471
- title: defaults.title,
472
- },
473
- cwd: workspace.projectDir,
474
- dataStorageMode: dataStorageMode,
475
- noInstall: true,
476
- packageManager: workspace.packageManager,
477
- persistencePolicy: persistencePolicy,
478
- projectDir: tempProjectDir,
479
- templateId: resolvedTemplateId,
480
- });
586
+ const result = await (async () => {
587
+ const scaffoldResult = await scaffoldProject({
588
+ answers: {
589
+ ...defaults,
590
+ author: workspace.author,
591
+ namespace: workspace.workspace.namespace,
592
+ phpPrefix: blockPhpPrefix,
593
+ slug: normalizedSlug,
594
+ textDomain: workspace.workspace.textDomain,
595
+ title: defaults.title,
596
+ },
597
+ cwd: workspace.projectDir,
598
+ dataStorageMode: dataStorageMode,
599
+ externalLayerId: resolvedExternalLayerSelection.externalLayerId,
600
+ externalLayerSource: resolvedExternalLayerSelection.externalLayerSource,
601
+ externalLayerSourceLabel: normalizedExternalLayerSource,
602
+ noInstall: true,
603
+ packageManager: workspace.packageManager,
604
+ persistencePolicy: persistencePolicy,
605
+ projectDir: tempProjectDir,
606
+ templateId: resolvedTemplateId,
607
+ });
608
+ await assertAddBlockSupportsExternalLayerOutputs({
609
+ callerCwd: cwd,
610
+ externalLayerId: resolvedExternalLayerSelection.externalLayerId,
611
+ externalLayerSource: resolvedExternalLayerSelection.externalLayerSource,
612
+ templateId: resolvedTemplateId,
613
+ variables: scaffoldResult.variables,
614
+ });
615
+ return scaffoldResult;
616
+ })();
481
617
  assertBlockTargetsDoNotExist(workspace.projectDir, resolvedTemplateId, result.variables);
482
618
  const mutationSnapshot = {
483
619
  fileSources: await snapshotWorkspaceFiles([
@@ -503,6 +639,7 @@ export async function runAddBlockCommand({ blockName, cwd = process.cwd(), dataS
503
639
  blockSlugs: collectWorkspaceBlockPaths(workspace.projectDir, resolvedTemplateId, result.variables).map((targetPath) => path.basename(targetPath)),
504
640
  projectDir: workspace.projectDir,
505
641
  templateId: resolvedTemplateId,
642
+ warnings: result.warnings,
506
643
  };
507
644
  }
508
645
  catch (error) {
@@ -511,6 +648,7 @@ export async function runAddBlockCommand({ blockName, cwd = process.cwd(), dataS
511
648
  }
512
649
  }
513
650
  finally {
651
+ await resolvedExternalLayerSelection.cleanup?.();
514
652
  if (tempRoot) {
515
653
  await fsp.rm(tempRoot, { force: true, recursive: true });
516
654
  }
@@ -34,7 +34,14 @@ export interface RunAddBlockCommandOptions {
34
34
  blockName: string;
35
35
  cwd?: string;
36
36
  dataStorageMode?: string;
37
+ externalLayerId?: string;
38
+ externalLayerSource?: string;
37
39
  persistencePolicy?: string;
40
+ selectExternalLayerId?: (options: Array<{
41
+ description?: string;
42
+ extends: string[];
43
+ id: string;
44
+ }>) => Promise<string>;
38
45
  templateId?: string;
39
46
  }
40
47
  export interface WorkspaceMutationSnapshot {
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import { promises as fsp } from "node:fs";
3
3
  import path from "node:path";
4
+ import { parseScaffoldBlockMetadata } from "@wp-typia/block-runtime/blocks";
4
5
  import { HOOKED_BLOCK_ANCHOR_PATTERN, HOOKED_BLOCK_POSITION_IDS, } from "./hooked-blocks.js";
5
6
  import { toKebabCase, toSnakeCase, } from "./string-case.js";
6
7
  import { WORKSPACE_TEMPLATE_PACKAGE, } from "./workspace-project.js";
@@ -127,18 +128,15 @@ export function readWorkspaceBlockJson(projectDir, blockSlug) {
127
128
  }
128
129
  let blockJson;
129
130
  try {
130
- blockJson = JSON.parse(fs.readFileSync(blockJsonPath, "utf8"));
131
+ blockJson = parseScaffoldBlockMetadata(JSON.parse(fs.readFileSync(blockJsonPath, "utf8")));
131
132
  }
132
133
  catch (error) {
133
134
  throw new Error(error instanceof Error
134
135
  ? `Failed to parse ${path.relative(projectDir, blockJsonPath)}: ${error.message}`
135
136
  : `Failed to parse ${path.relative(projectDir, blockJsonPath)}.`);
136
137
  }
137
- if (!blockJson || typeof blockJson !== "object" || Array.isArray(blockJson)) {
138
- throw new Error(`${path.relative(projectDir, blockJsonPath)} must contain a JSON object.`);
139
- }
140
138
  return {
141
- blockJson: blockJson,
139
+ blockJson,
142
140
  blockJsonPath,
143
141
  };
144
142
  }
@@ -186,7 +184,7 @@ export function assertBindingSourceDoesNotExist(projectDir, bindingSourceSlug, i
186
184
  */
187
185
  export function formatAddHelpText() {
188
186
  return `Usage:
189
- wp-typia add block <name> --template <${ADD_BLOCK_TEMPLATE_IDS.join("|")}> [--data-storage <post-meta|custom-table>] [--persistence-policy <authenticated|public>]
187
+ wp-typia add block <name> --template <${ADD_BLOCK_TEMPLATE_IDS.join("|")}> [--external-layer-source <./path|github:owner/repo/path[#ref]|npm-package>] [--external-layer-id <layer-id>] [--data-storage <post-meta|custom-table>] [--persistence-policy <authenticated|public>]
190
188
  wp-typia add variation <name> --block <block-slug>
191
189
  wp-typia add pattern <name>
192
190
  wp-typia add binding-source <name>
@@ -14,6 +14,9 @@ const BINDING_SOURCE_EDITOR_ASSET = "build/bindings/index.asset.php";
14
14
  function escapeRegex(value) {
15
15
  return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
16
16
  }
17
+ function quotePhpString(value) {
18
+ return `'${value.replace(/\\/gu, "\\\\").replace(/'/gu, "\\'")}'`;
19
+ }
17
20
  function buildVariationConfigEntry(blockSlug, variationSlug) {
18
21
  return [
19
22
  "\t{",
@@ -61,7 +64,7 @@ function getVariationConstBindings(variationSlugs) {
61
64
  function buildVariationSource(variationSlug, textDomain) {
62
65
  const variationTitle = toTitleCase(variationSlug);
63
66
  const variationConstName = buildVariationConstName(variationSlug);
64
- return `import type { BlockVariation } from '@wordpress/blocks';
67
+ return `import type { BlockVariation } from '@wp-typia/block-types/blocks/registration';
65
68
  import { __ } from '@wordpress/i18n';
66
69
 
67
70
  export const ${variationConstName} = {
@@ -120,8 +123,12 @@ register_block_pattern(
120
123
  );
121
124
  `;
122
125
  }
123
- function buildBindingSourceServerSource(bindingSourceSlug, namespace, textDomain) {
126
+ function buildBindingSourceServerSource(bindingSourceSlug, phpPrefix, namespace, textDomain) {
124
127
  const bindingSourceTitle = toTitleCase(bindingSourceSlug);
128
+ const bindingSourcePhpId = bindingSourceSlug.replace(/-/g, "_");
129
+ const bindingSourceValueFunctionName = `${phpPrefix}_${bindingSourcePhpId}_binding_source_values`;
130
+ const bindingSourceResolveFunctionName = `${phpPrefix}_${bindingSourcePhpId}_resolve_binding_source_value`;
131
+ const starterValue = `${bindingSourceTitle} starter value`;
125
132
  return `<?php
126
133
  if ( ! defined( 'ABSPATH' ) ) {
127
134
  \treturn;
@@ -131,29 +138,55 @@ if ( ! function_exists( 'register_block_bindings_source' ) ) {
131
138
  \treturn;
132
139
  }
133
140
 
141
+ if ( ! function_exists( '${bindingSourceValueFunctionName}' ) ) {
142
+ \tfunction ${bindingSourceValueFunctionName}() : array {
143
+ \t\treturn array(
144
+ \t\t\t${quotePhpString(bindingSourceSlug)} => ${quotePhpString(starterValue)},
145
+ \t\t);
146
+ \t}
147
+ }
148
+
149
+ if ( ! function_exists( '${bindingSourceResolveFunctionName}' ) ) {
150
+ \tfunction ${bindingSourceResolveFunctionName}( array $source_args ) : string {
151
+ \t\t$field = isset( $source_args['field'] ) && is_string( $source_args['field'] )
152
+ \t\t\t? $source_args['field']
153
+ \t\t\t: '${bindingSourceSlug}';
154
+ \t\t$binding_source_values = ${bindingSourceValueFunctionName}();
155
+ \t\t$value = $binding_source_values[ $field ] ?? '';
156
+
157
+ \t\treturn is_string( $value ) ? $value : '';
158
+ \t}
159
+ }
160
+
134
161
  register_block_bindings_source(
135
- \t'${namespace}/${bindingSourceSlug}',
162
+ \t${quotePhpString(`${namespace}/${bindingSourceSlug}`)},
136
163
  \tarray(
137
- \t\t'label' => __( ${JSON.stringify(bindingSourceTitle)}, '${textDomain}' ),
138
- \t\t'get_value_callback' => static function( array $source_args ) : string {
139
- \t\t\t$field = isset( $source_args['field'] ) && is_string( $source_args['field'] )
140
- \t\t\t\t? $source_args['field']
141
- \t\t\t\t: '${bindingSourceSlug}';
142
-
143
- \t\t\treturn sprintf(
144
- \t\t\t\t__( 'Replace %s with real binding source data.', '${textDomain}' ),
145
- \t\t\t\t$field
146
- \t\t\t);
147
- \t\t},
164
+ \t\t'label' => __( ${quotePhpString(bindingSourceTitle)}, ${quotePhpString(textDomain)} ),
165
+ \t\t'get_value_callback' => ${quotePhpString(bindingSourceResolveFunctionName)},
148
166
  \t)
149
167
  );
150
168
  `;
151
169
  }
152
170
  function buildBindingSourceEditorSource(bindingSourceSlug, namespace, textDomain) {
153
171
  const bindingSourceTitle = toTitleCase(bindingSourceSlug);
172
+ const starterValue = `${bindingSourceTitle} starter value`;
154
173
  return `import { registerBlockBindingsSource } from '@wordpress/blocks';
155
174
  import { __ } from '@wordpress/i18n';
156
175
 
176
+ interface BindingSourceRegistration {
177
+ \targs?: {
178
+ \t\tfield?: string;
179
+ \t};
180
+ }
181
+
182
+ const BINDING_SOURCE_VALUES: Record<string, string> = {
183
+ \t${quoteTsString(bindingSourceSlug)}: ${quoteTsString(starterValue)},
184
+ };
185
+
186
+ function resolveBindingSourceValue( field: string ): string {
187
+ \treturn BINDING_SOURCE_VALUES[ field ] ?? '';
188
+ }
189
+
157
190
  registerBlockBindingsSource( {
158
191
  \tname: ${quoteTsString(`${namespace}/${bindingSourceSlug}`)},
159
192
  \tlabel: __( ${quoteTsString(bindingSourceTitle)}, ${quoteTsString(textDomain)} ),
@@ -170,8 +203,14 @@ registerBlockBindingsSource( {
170
203
  \t},
171
204
  \tgetValues( { bindings } ) {
172
205
  \t\tconst values: Record<string, string> = {};
173
- \t\tfor ( const attributeName of Object.keys( bindings ) ) {
174
- \t\t\tvalues[ attributeName ] = ${quoteTsString(`TODO: replace ${bindingSourceSlug} with real editor-side values.`)};
206
+ \t\tfor ( const [ attributeName, binding ] of Object.entries(
207
+ \t\t\tbindings as Record<string, BindingSourceRegistration>
208
+ \t\t) ) {
209
+ \t\t\tconst field =
210
+ \t\t\t\ttypeof binding?.args?.field === 'string'
211
+ \t\t\t\t\t? binding.args.field
212
+ \t\t\t\t\t: ${quoteTsString(bindingSourceSlug)};
213
+ \t\t\tvalues[ attributeName ] = resolveBindingSourceValue( field );
175
214
  \t\t}
176
215
  \t\treturn values;
177
216
  \t},
@@ -512,7 +551,7 @@ export async function runAddBindingSourceCommand({ bindingSourceName, cwd = proc
512
551
  try {
513
552
  await fsp.mkdir(bindingSourceDir, { recursive: true });
514
553
  await ensureBindingSourceBootstrapAnchors(workspace);
515
- await fsp.writeFile(serverFilePath, buildBindingSourceServerSource(bindingSourceSlug, workspace.workspace.namespace, workspace.workspace.textDomain), "utf8");
554
+ await fsp.writeFile(serverFilePath, buildBindingSourceServerSource(bindingSourceSlug, workspace.workspace.phpPrefix, workspace.workspace.namespace, workspace.workspace.textDomain), "utf8");
516
555
  await fsp.writeFile(editorFilePath, buildBindingSourceEditorSource(bindingSourceSlug, workspace.workspace.namespace, workspace.workspace.textDomain), "utf8");
517
556
  await writeBindingSourceRegistry(workspace.projectDir, bindingSourceSlug);
518
557
  await appendWorkspaceInventoryEntries(workspace.projectDir, {
@@ -12,6 +12,8 @@
12
12
  * `getWorkspaceBlockSelectOptions`, and `seedWorkspaceMigrationProject` for
13
13
  * explicit `wp-typia add` flows,
14
14
  * `getDoctorChecks`, `runDoctor`, and `DoctorCheck` for diagnostics,
15
+ * `createCliCommandError` and `formatCliDiagnosticError` for shared
16
+ * non-interactive failure rendering,
15
17
  * `formatHelpText` for top-level CLI usage output, scaffold helpers such as
16
18
  * `createReadlinePrompt`, `getNextSteps`, `getOptionalOnboarding`,
17
19
  * `runScaffoldFlow`, and `ReadlinePrompt` for interactive project creation,
@@ -21,6 +23,8 @@
21
23
  * template inspection flows.
22
24
  */
23
25
  export { getDoctorChecks, runDoctor, type DoctorCheck } from "./cli-doctor.js";
26
+ export { createCliCommandError, CliDiagnosticError, formatCliDiagnosticError, formatDoctorCheckLine, formatDoctorSummaryLine, getDoctorFailureDetailLines, getFailingDoctorChecks, isCliDiagnosticError, } from "./cli-diagnostics.js";
27
+ export type { CliDiagnosticMessage } from "./cli-diagnostics.js";
24
28
  export { formatAddHelpText, getWorkspaceBlockSelectOptions, runAddBindingSourceCommand, runAddBlockCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddVariationCommand, seedWorkspaceMigrationProject, } from "./cli-add.js";
25
29
  export { HOOKED_BLOCK_POSITION_IDS } from "./hooked-blocks.js";
26
30
  export type { HookedBlockPositionId } from "./hooked-blocks.js";
@@ -12,6 +12,8 @@
12
12
  * `getWorkspaceBlockSelectOptions`, and `seedWorkspaceMigrationProject` for
13
13
  * explicit `wp-typia add` flows,
14
14
  * `getDoctorChecks`, `runDoctor`, and `DoctorCheck` for diagnostics,
15
+ * `createCliCommandError` and `formatCliDiagnosticError` for shared
16
+ * non-interactive failure rendering,
15
17
  * `formatHelpText` for top-level CLI usage output, scaffold helpers such as
16
18
  * `createReadlinePrompt`, `getNextSteps`, `getOptionalOnboarding`,
17
19
  * `runScaffoldFlow`, and `ReadlinePrompt` for interactive project creation,
@@ -21,6 +23,7 @@
21
23
  * template inspection flows.
22
24
  */
23
25
  export { getDoctorChecks, runDoctor } from "./cli-doctor.js";
26
+ export { createCliCommandError, CliDiagnosticError, formatCliDiagnosticError, formatDoctorCheckLine, formatDoctorSummaryLine, getDoctorFailureDetailLines, getFailingDoctorChecks, isCliDiagnosticError, } from "./cli-diagnostics.js";
24
27
  export { formatAddHelpText, getWorkspaceBlockSelectOptions, runAddBindingSourceCommand, runAddBlockCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddVariationCommand, seedWorkspaceMigrationProject, } from "./cli-add.js";
25
28
  export { HOOKED_BLOCK_POSITION_IDS } from "./hooked-blocks.js";
26
29
  export { formatHelpText } from "./cli-help.js";
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Shared human-readable diagnostics for non-interactive `wp-typia` CLI flows.
3
+ */
4
+ export interface CliDiagnosticMessage {
5
+ command: string;
6
+ detailLines: string[];
7
+ summary: string;
8
+ }
9
+ type DoctorCheckLike = {
10
+ detail: string;
11
+ label: string;
12
+ status: "pass" | "fail";
13
+ };
14
+ /**
15
+ * Structured CLI failure carrying a stable summary/detail layout.
16
+ */
17
+ export declare class CliDiagnosticError extends Error {
18
+ readonly command: string;
19
+ readonly detailLines: string[];
20
+ readonly summary: string;
21
+ constructor(message: CliDiagnosticMessage, options?: ErrorOptions);
22
+ }
23
+ /**
24
+ * Narrow an unknown error to the shared CLI diagnostic error shape.
25
+ */
26
+ export declare function isCliDiagnosticError(error: unknown): error is CliDiagnosticError;
27
+ /**
28
+ * Build a shared diagnostic error for one CLI command failure.
29
+ */
30
+ export declare function createCliCommandError(options: {
31
+ command: string;
32
+ detailLines?: string[];
33
+ error?: unknown;
34
+ summary?: string;
35
+ }): CliDiagnosticError;
36
+ /**
37
+ * Render a CLI diagnostic block. Non-diagnostic errors fall back to their
38
+ * plain message so existing non-command failures keep working.
39
+ */
40
+ export declare function formatCliDiagnosticError(error: unknown): string;
41
+ /**
42
+ * Format one human-readable doctor check row.
43
+ */
44
+ export declare function formatDoctorCheckLine(check: DoctorCheckLike): string;
45
+ /**
46
+ * Return the failing doctor checks from one doctor run.
47
+ */
48
+ export declare function getFailingDoctorChecks<TCheck extends DoctorCheckLike>(checks: readonly TCheck[]): TCheck[];
49
+ /**
50
+ * Format the final doctor summary row.
51
+ */
52
+ export declare function formatDoctorSummaryLine(checks: readonly DoctorCheckLike[]): string;
53
+ /**
54
+ * Build detail lines for doctor failures so the non-interactive formatter can
55
+ * restate the failed checks after the streaming rows.
56
+ */
57
+ export declare function getDoctorFailureDetailLines(checks: readonly DoctorCheckLike[]): string[];
58
+ export {};