@wp-typia/project-tools 0.17.0 → 0.18.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 (60) hide show
  1. package/dist/runtime/block-generator-service-core.d.ts +1 -1
  2. package/dist/runtime/block-generator-service-core.js +2 -1
  3. package/dist/runtime/block-generator-service-spec.d.ts +2 -1
  4. package/dist/runtime/built-in-block-artifacts.js +1 -0
  5. package/dist/runtime/built-in-block-code-templates/compound-child.d.ts +2 -2
  6. package/dist/runtime/built-in-block-code-templates/compound-child.js +30 -2
  7. package/dist/runtime/built-in-block-code-templates/compound-parent.d.ts +1 -1
  8. package/dist/runtime/built-in-block-code-templates/compound-parent.js +139 -19
  9. package/dist/runtime/cli-add-block.d.ts +2 -1
  10. package/dist/runtime/cli-add-block.js +19 -1
  11. package/dist/runtime/cli-add-shared.d.ts +55 -1
  12. package/dist/runtime/cli-add-shared.js +101 -2
  13. package/dist/runtime/cli-add-workspace-assets.d.ts +21 -1
  14. package/dist/runtime/cli-add-workspace-assets.js +417 -1
  15. package/dist/runtime/cli-add-workspace-rest.d.ts +14 -0
  16. package/dist/runtime/cli-add-workspace-rest.js +1060 -0
  17. package/dist/runtime/cli-add-workspace.d.ts +10 -1
  18. package/dist/runtime/cli-add-workspace.js +10 -1
  19. package/dist/runtime/cli-add.d.ts +3 -3
  20. package/dist/runtime/cli-add.js +2 -2
  21. package/dist/runtime/cli-core.d.ts +3 -1
  22. package/dist/runtime/cli-core.js +2 -1
  23. package/dist/runtime/cli-doctor-workspace.js +135 -1
  24. package/dist/runtime/cli-help.js +10 -6
  25. package/dist/runtime/cli-scaffold.d.ts +10 -2
  26. package/dist/runtime/cli-scaffold.js +136 -36
  27. package/dist/runtime/cli-templates.d.ts +4 -4
  28. package/dist/runtime/cli-templates.js +79 -39
  29. package/dist/runtime/index.d.ts +4 -3
  30. package/dist/runtime/index.js +3 -2
  31. package/dist/runtime/local-dev-presets.js +7 -2
  32. package/dist/runtime/rest-resource-artifacts.d.ts +35 -0
  33. package/dist/runtime/rest-resource-artifacts.js +158 -0
  34. package/dist/runtime/scaffold-answer-resolution.js +68 -2
  35. package/dist/runtime/scaffold-apply-utils.d.ts +4 -3
  36. package/dist/runtime/scaffold-apply-utils.js +34 -17
  37. package/dist/runtime/scaffold-bootstrap.d.ts +15 -0
  38. package/dist/runtime/scaffold-bootstrap.js +29 -7
  39. package/dist/runtime/scaffold-documents.js +2 -1
  40. package/dist/runtime/scaffold-onboarding.js +7 -3
  41. package/dist/runtime/scaffold-package-manager-files.js +6 -1
  42. package/dist/runtime/scaffold.d.ts +7 -1
  43. package/dist/runtime/scaffold.js +50 -8
  44. package/dist/runtime/template-render.d.ts +5 -2
  45. package/dist/runtime/template-render.js +9 -3
  46. package/dist/runtime/template-source-contracts.d.ts +11 -0
  47. package/dist/runtime/template-source-external.d.ts +1 -1
  48. package/dist/runtime/template-source-external.js +45 -13
  49. package/dist/runtime/template-source-normalization.d.ts +1 -1
  50. package/dist/runtime/template-source-normalization.js +5 -1
  51. package/dist/runtime/template-source-remote.d.ts +5 -0
  52. package/dist/runtime/template-source-remote.js +33 -0
  53. package/dist/runtime/template-source.js +30 -1
  54. package/dist/runtime/workspace-inventory.d.ts +43 -1
  55. package/dist/runtime/workspace-inventory.js +132 -1
  56. package/dist/runtime/workspace-project.d.ts +1 -1
  57. package/dist/runtime/workspace-project.js +2 -2
  58. package/package.json +3 -3
  59. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +428 -49
  60. package/templates/query-loop/src/validator-toolkit.ts.mustache +0 -1
@@ -4,5 +4,5 @@ export declare class BlockGeneratorService {
4
4
  plan({ allowExistingDir, answers, cwd, dataStorageMode, externalLayerId, externalLayerSource, externalLayerSourceLabel, noInstall, packageManager, persistencePolicy, projectDir, repositoryReference, templateId, variant, withMigrationUi, withTestPreset, withWpEnv, }: PlanBlockInput): Promise<PlanBlockResult>;
5
5
  validate({ plan }: ValidateBlockInput): Promise<ValidateBlockResult>;
6
6
  render({ validated }: RenderBlockInput): Promise<RenderBlockResult>;
7
- apply({ rendered, installDependencies, }: ApplyBlockInput): Promise<ScaffoldProjectResult>;
7
+ apply({ rendered, installDependencies, onProgress, }: ApplyBlockInput): Promise<ScaffoldProjectResult>;
8
8
  }
@@ -222,7 +222,7 @@ export class BlockGeneratorService {
222
222
  });
223
223
  return rendered;
224
224
  }
225
- async apply({ rendered, installDependencies, }) {
225
+ async apply({ rendered, installDependencies, onProgress, }) {
226
226
  const cachedArtifacts = renderedArtifactCache.get(rendered);
227
227
  const currentVariablesFingerprint = createVariablesFingerprint(rendered.variables);
228
228
  const artifacts = cachedArtifacts &&
@@ -246,6 +246,7 @@ export class BlockGeneratorService {
246
246
  codeArtifacts,
247
247
  installDependencies,
248
248
  noInstall: rendered.target.noInstall,
249
+ onProgress,
249
250
  packageManager: rendered.target.packageManager,
250
251
  projectDir: rendered.target.projectDir,
251
252
  repositoryReference: rendered.target.repositoryReference,
@@ -1,7 +1,7 @@
1
1
  import type { PackageManagerId } from "./package-managers.js";
2
2
  import { type BuiltInTemplateId } from "./template-registry.js";
3
3
  import type { InstallDependenciesOptions } from "./scaffold-apply-utils.js";
4
- import type { DataStorageMode, PersistencePolicy, ScaffoldAnswers, ScaffoldTemplateVariables } from "./scaffold.js";
4
+ import type { DataStorageMode, PersistencePolicy, ScaffoldAnswers, ScaffoldProgressEvent, ScaffoldTemplateVariables } from "./scaffold.js";
5
5
  export interface BlockSpec {
6
6
  block: {
7
7
  namespace: string;
@@ -105,6 +105,7 @@ export interface RenderBlockResult extends ValidateBlockResult {
105
105
  export interface ApplyBlockInput {
106
106
  rendered: RenderBlockResult;
107
107
  installDependencies?: ((options: InstallDependenciesOptions) => Promise<void>) | undefined;
108
+ onProgress?: ((event: ScaffoldProgressEvent) => void | Promise<void>) | undefined;
108
109
  }
109
110
  export declare function createBuiltInBlockSpec({ answers, dataStorageMode, persistencePolicy, templateId, withMigrationUi, withTestPreset, withWpEnv, }: Omit<PlanBlockInput, "allowExistingDir" | "cwd" | "noInstall" | "packageManager" | "projectDir" | "variant">): BlockSpec;
110
111
  export declare function buildTemplateVariablesFromBlockSpec(spec: BlockSpec): ScaffoldTemplateVariables;
@@ -115,6 +115,7 @@ function buildCompoundParentArtifact(variables) {
115
115
  icon: variables.icon,
116
116
  description: variables.description,
117
117
  example: {},
118
+ allowedBlocks: [`${variables.namespace}/${variables.slugKebabCase}-item`],
118
119
  supports: persistenceEnabled
119
120
  ? {
120
121
  html: false,
@@ -1,4 +1,4 @@
1
- export declare const COMPOUND_CHILD_EDIT_TEMPLATE = "import type { BlockEditProps } from '@wp-typia/block-types/blocks/registration';\nimport { RichText, useBlockProps } from '@wordpress/block-editor';\nimport { Notice } from '@wordpress/components';\nimport { __ } from '@wordpress/i18n';\n\nimport { useTypiaValidation } from './hooks';\nimport type { {{pascalCase}}ItemAttributes } from './types';\nimport {\n\tcreateAttributeUpdater,\n\tvalidate{{pascalCase}}ItemAttributes,\n} from './validators';\n\ntype EditProps = BlockEditProps< {{pascalCase}}ItemAttributes >;\n\nexport default function Edit( {\n\tattributes,\n\tsetAttributes,\n}: EditProps ) {\n\tconst updateAttribute = createAttributeUpdater( attributes, setAttributes );\n\tconst { errorMessages, isValid } = useTypiaValidation(\n\t\tattributes,\n\t\tvalidate{{pascalCase}}ItemAttributes\n\t);\n\n\treturn (\n\t\t<div { ...useBlockProps( { className: '{{compoundChildCssClassName}}' } ) }>\n\t\t\t<RichText\n\t\t\t\ttagName=\"h4\"\n\t\t\t\tclassName=\"{{compoundChildCssClassName}}__title\"\n\t\t\t\tvalue={ attributes.title ?? '' }\n\t\t\t\tonChange={ ( title ) => updateAttribute( 'title', title ) }\n\t\t\t\tplaceholder={ __( {{compoundChildTitleJson}}, '{{textDomain}}' ) }\n\t\t\t/>\n\t\t\t<RichText\n\t\t\t\ttagName=\"p\"\n\t\t\t\tclassName=\"{{compoundChildCssClassName}}__body\"\n\t\t\t\tvalue={ attributes.body ?? '' }\n\t\t\t\tonChange={ ( body ) => updateAttribute( 'body', body ) }\n\t\t\t\tplaceholder={ __( 'Add supporting details for this internal item.', '{{textDomain}}' ) }\n\t\t\t/>\n\t\t\t{ ! isValid && (\n\t\t\t\t<Notice status=\"error\" isDismissible={ false }>\n\t\t\t\t\t<ul>\n\t\t\t\t\t\t{ errorMessages.map( ( error, index ) => <li key={ index }>{ error }</li> ) }\n\t\t\t\t\t</ul>\n\t\t\t\t</Notice>\n\t\t\t) }\n\t\t</div>\n\t);\n}\n";
2
- export declare const COMPOUND_CHILD_SAVE_TEMPLATE = "import { RichText, useBlockProps } from '@wordpress/block-editor';\n\nimport type { {{pascalCase}}ItemAttributes } from './types';\n\nexport default function Save( {\n\tattributes,\n}: {\n\tattributes: {{pascalCase}}ItemAttributes;\n} ) {\n\treturn (\n\t\t<div { ...useBlockProps.save( { className: '{{compoundChildCssClassName}}' } ) }>\n\t\t\t<RichText.Content\n\t\t\t\ttagName=\"h4\"\n\t\t\t\tclassName=\"{{compoundChildCssClassName}}__title\"\n\t\t\t\tvalue={ attributes.title }\n\t\t\t/>\n\t\t\t<RichText.Content\n\t\t\t\ttagName=\"p\"\n\t\t\t\tclassName=\"{{compoundChildCssClassName}}__body\"\n\t\t\t\tvalue={ attributes.body }\n\t\t\t/>\n\t\t</div>\n\t);\n}\n";
1
+ export declare const COMPOUND_CHILD_EDIT_TEMPLATE = "import type { BlockEditProps } from '@wp-typia/block-types/blocks/registration';\nimport { InnerBlocks, RichText, useBlockProps } from '@wordpress/block-editor';\nimport { Notice } from '@wordpress/components';\nimport { __ } from '@wordpress/i18n';\n\nimport metadata from './block-metadata';\nimport {\n\tgetChildAllowedBlocks,\n\tgetChildTemplate,\n\thasNestedChildBlocks,\n} from '../{{slugKebabCase}}/children';\nimport { useTypiaValidation } from './hooks';\nimport type { {{pascalCase}}ItemAttributes } from './types';\nimport {\n\tcreateAttributeUpdater,\n\tvalidate{{pascalCase}}ItemAttributes,\n} from './validators';\n\ntype EditProps = BlockEditProps< {{pascalCase}}ItemAttributes >;\n\nexport default function Edit( {\n\tattributes,\n\tsetAttributes,\n}: EditProps ) {\n\tconst updateAttribute = createAttributeUpdater( attributes, setAttributes );\n\tconst { errorMessages, isValid } = useTypiaValidation(\n\t\tattributes,\n\t\tvalidate{{pascalCase}}ItemAttributes\n\t);\n\tconst nestedAllowedBlocks = getChildAllowedBlocks( metadata.name );\n\tconst nestedTemplate = getChildTemplate( metadata.name );\n\tconst showsNestedChildren = hasNestedChildBlocks( metadata.name );\n\n\treturn (\n\t\t<div { ...useBlockProps( { className: '{{compoundChildCssClassName}}' } ) }>\n\t\t\t<RichText\n\t\t\t\ttagName=\"h4\"\n\t\t\t\tclassName=\"{{compoundChildCssClassName}}__title\"\n\t\t\t\tvalue={ attributes.title ?? '' }\n\t\t\t\tonChange={ ( title ) => updateAttribute( 'title', title ) }\n\t\t\t\tplaceholder={ __( {{compoundChildTitleJson}}, '{{textDomain}}' ) }\n\t\t\t/>\n\t\t\t<RichText\n\t\t\t\ttagName=\"p\"\n\t\t\t\tclassName=\"{{compoundChildCssClassName}}__body\"\n\t\t\t\tvalue={ attributes.body ?? '' }\n\t\t\t\tonChange={ ( body ) => updateAttribute( 'body', body ) }\n\t\t\t\tplaceholder={ __( 'Add supporting details for this internal item.', '{{textDomain}}' ) }\n\t\t\t/>\n\t\t\t{ ! isValid && (\n\t\t\t\t<Notice status=\"error\" isDismissible={ false }>\n\t\t\t\t\t<ul>\n\t\t\t\t\t\t{ errorMessages.map( ( error, index ) => <li key={ index }>{ error }</li> ) }\n\t\t\t\t\t</ul>\n\t\t\t\t</Notice>\n\t\t\t) }\n\t\t\t{ showsNestedChildren && (\n\t\t\t\t<div className=\"{{compoundChildCssClassName}}__children\">\n\t\t\t\t\t<InnerBlocks\n\t\t\t\t\t\tallowedBlocks={ nestedAllowedBlocks }\n\t\t\t\t\t\trenderAppender={ InnerBlocks.ButtonBlockAppender }\n\t\t\t\t\t\ttemplate={ nestedTemplate }\n\t\t\t\t\t\ttemplateLock={ false }\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t) }\n\t\t</div>\n\t);\n}\n";
2
+ export declare const COMPOUND_CHILD_SAVE_TEMPLATE = "import { InnerBlocks, RichText, useBlockProps } from '@wordpress/block-editor';\n\nimport metadata from './block-metadata';\nimport { hasNestedChildBlocks } from '../{{slugKebabCase}}/children';\nimport type { {{pascalCase}}ItemAttributes } from './types';\n\nexport default function Save( {\n\tattributes,\n}: {\n\tattributes: {{pascalCase}}ItemAttributes;\n} ) {\n\tconst showsNestedChildren = hasNestedChildBlocks( metadata.name );\n\n\treturn (\n\t\t<div { ...useBlockProps.save( { className: '{{compoundChildCssClassName}}' } ) }>\n\t\t\t<RichText.Content\n\t\t\t\ttagName=\"h4\"\n\t\t\t\tclassName=\"{{compoundChildCssClassName}}__title\"\n\t\t\t\tvalue={ attributes.title }\n\t\t\t/>\n\t\t\t<RichText.Content\n\t\t\t\ttagName=\"p\"\n\t\t\t\tclassName=\"{{compoundChildCssClassName}}__body\"\n\t\t\t\tvalue={ attributes.body }\n\t\t\t/>\n\t\t\t{ showsNestedChildren && (\n\t\t\t\t<div className=\"{{compoundChildCssClassName}}__children\">\n\t\t\t\t\t<InnerBlocks.Content />\n\t\t\t\t</div>\n\t\t\t) }\n\t\t</div>\n\t);\n}\n";
3
3
  export declare const COMPOUND_CHILD_INDEX_TEMPLATE = "import {\n\tregisterScaffoldBlockType,\n\ttype BlockConfiguration,\n} from '@wp-typia/block-types/blocks/registration';\nimport {\n\tbuildScaffoldBlockRegistration,\n\tparseScaffoldBlockMetadata,\n} from '@wp-typia/block-runtime/blocks';\n\nimport Edit from './edit';\nimport Save from './save';\nimport metadata from './block-metadata';\nimport '../{{slugKebabCase}}/style.scss';\n\nimport type { {{pascalCase}}ItemAttributes } from './types';\n\nconst registration = buildScaffoldBlockRegistration(\n\tparseScaffoldBlockMetadata<BlockConfiguration< {{pascalCase}}ItemAttributes >>( metadata ),\n\t{\n\t\tedit: Edit,\n\t\tsave: Save,\n\t}\n);\n\nregisterScaffoldBlockType(registration.name, registration.settings);\n";
4
4
  export declare const COMPOUND_CHILD_VALIDATORS_TEMPLATE = "import typia from 'typia';\nimport currentManifest from './manifest-defaults-document';\nimport type {\n\t{{pascalCase}}ItemAttributes,\n\t{{pascalCase}}ItemValidationResult,\n} from './types';\nimport { createTemplateValidatorToolkit } from '../../validator-toolkit';\n\nconst scaffoldValidators = createTemplateValidatorToolkit< {{pascalCase}}ItemAttributes >( {\n\tassert: typia.createAssert< {{pascalCase}}ItemAttributes >(),\n\tclone: typia.misc.createClone< {{pascalCase}}ItemAttributes >() as (\n\t\tvalue: {{pascalCase}}ItemAttributes,\n\t) => {{pascalCase}}ItemAttributes,\n\tis: typia.createIs< {{pascalCase}}ItemAttributes >(),\n\tmanifest: currentManifest,\n\tprune: typia.misc.createPrune< {{pascalCase}}ItemAttributes >(),\n\trandom: typia.createRandom< {{pascalCase}}ItemAttributes >() as (\n\t\t...args: unknown[]\n\t) => {{pascalCase}}ItemAttributes,\n\tvalidate: typia.createValidate< {{pascalCase}}ItemAttributes >(),\n} );\n\nexport const validate{{pascalCase}}ItemAttributes =\n\tscaffoldValidators.validateAttributes as (\n\t\tattributes: unknown\n\t) => {{pascalCase}}ItemValidationResult;\n\nexport const validators = scaffoldValidators.validators;\n\nexport const sanitize{{pascalCase}}ItemAttributes =\n\tscaffoldValidators.sanitizeAttributes as (\n\t\tattributes: Partial< {{pascalCase}}ItemAttributes >\n\t) => {{pascalCase}}ItemAttributes;\n\nexport const createAttributeUpdater = scaffoldValidators.createAttributeUpdater;\n";
@@ -1,8 +1,14 @@
1
1
  export const COMPOUND_CHILD_EDIT_TEMPLATE = `import type { BlockEditProps } from '@wp-typia/block-types/blocks/registration';
2
- import { RichText, useBlockProps } from '@wordpress/block-editor';
2
+ import { InnerBlocks, RichText, useBlockProps } from '@wordpress/block-editor';
3
3
  import { Notice } from '@wordpress/components';
4
4
  import { __ } from '@wordpress/i18n';
5
5
 
6
+ import metadata from './block-metadata';
7
+ import {
8
+ \tgetChildAllowedBlocks,
9
+ \tgetChildTemplate,
10
+ \thasNestedChildBlocks,
11
+ } from '../{{slugKebabCase}}/children';
6
12
  import { useTypiaValidation } from './hooks';
7
13
  import type { {{pascalCase}}ItemAttributes } from './types';
8
14
  import {
@@ -21,6 +27,9 @@ export default function Edit( {
21
27
  \t\tattributes,
22
28
  \t\tvalidate{{pascalCase}}ItemAttributes
23
29
  \t);
30
+ \tconst nestedAllowedBlocks = getChildAllowedBlocks( metadata.name );
31
+ \tconst nestedTemplate = getChildTemplate( metadata.name );
32
+ \tconst showsNestedChildren = hasNestedChildBlocks( metadata.name );
24
33
 
25
34
  \treturn (
26
35
  \t\t<div { ...useBlockProps( { className: '{{compoundChildCssClassName}}' } ) }>
@@ -45,12 +54,24 @@ export default function Edit( {
45
54
  \t\t\t\t\t</ul>
46
55
  \t\t\t\t</Notice>
47
56
  \t\t\t) }
57
+ \t\t\t{ showsNestedChildren && (
58
+ \t\t\t\t<div className="{{compoundChildCssClassName}}__children">
59
+ \t\t\t\t\t<InnerBlocks
60
+ \t\t\t\t\t\tallowedBlocks={ nestedAllowedBlocks }
61
+ \t\t\t\t\t\trenderAppender={ InnerBlocks.ButtonBlockAppender }
62
+ \t\t\t\t\t\ttemplate={ nestedTemplate }
63
+ \t\t\t\t\t\ttemplateLock={ false }
64
+ \t\t\t\t\t/>
65
+ \t\t\t\t</div>
66
+ \t\t\t) }
48
67
  \t\t</div>
49
68
  \t);
50
69
  }
51
70
  `;
52
- export const COMPOUND_CHILD_SAVE_TEMPLATE = `import { RichText, useBlockProps } from '@wordpress/block-editor';
71
+ export const COMPOUND_CHILD_SAVE_TEMPLATE = `import { InnerBlocks, RichText, useBlockProps } from '@wordpress/block-editor';
53
72
 
73
+ import metadata from './block-metadata';
74
+ import { hasNestedChildBlocks } from '../{{slugKebabCase}}/children';
54
75
  import type { {{pascalCase}}ItemAttributes } from './types';
55
76
 
56
77
  export default function Save( {
@@ -58,6 +79,8 @@ export default function Save( {
58
79
  }: {
59
80
  \tattributes: {{pascalCase}}ItemAttributes;
60
81
  } ) {
82
+ \tconst showsNestedChildren = hasNestedChildBlocks( metadata.name );
83
+
61
84
  \treturn (
62
85
  \t\t<div { ...useBlockProps.save( { className: '{{compoundChildCssClassName}}' } ) }>
63
86
  \t\t\t<RichText.Content
@@ -70,6 +93,11 @@ export default function Save( {
70
93
  \t\t\t\tclassName="{{compoundChildCssClassName}}__body"
71
94
  \t\t\t\tvalue={ attributes.body }
72
95
  \t\t\t/>
96
+ \t\t\t{ showsNestedChildren && (
97
+ \t\t\t\t<div className="{{compoundChildCssClassName}}__children">
98
+ \t\t\t\t\t<InnerBlocks.Content />
99
+ \t\t\t\t</div>
100
+ \t\t\t) }
73
101
  \t\t</div>
74
102
  \t);
75
103
  }
@@ -3,4 +3,4 @@ export declare const COMPOUND_PARENT_SAVE_TEMPLATE = "import { InnerBlocks, Rich
3
3
  export declare const COMPOUND_PARENT_INDEX_TEMPLATE = "import {\n\tregisterScaffoldBlockType,\n\ttype BlockConfiguration,\n} from '@wp-typia/block-types/blocks/registration';\nimport {\n\tbuildScaffoldBlockRegistration,\n\tparseScaffoldBlockMetadata,\n} from '@wp-typia/block-runtime/blocks';\n\nimport Edit from './edit';\nimport Save from './save';\nimport metadata from './block-metadata';\nimport './style.scss';\n\nimport type { {{pascalCase}}Attributes } from './types';\n\nconst registration = buildScaffoldBlockRegistration(\n\tparseScaffoldBlockMetadata<BlockConfiguration< {{pascalCase}}Attributes >>( metadata ),\n\t{\n\t\tedit: Edit,\n\t\tsave: Save,\n\t}\n);\n\nregisterScaffoldBlockType(registration.name, registration.settings);\n";
4
4
  export declare const COMPOUND_LOCAL_HOOKS_TEMPLATE = "export {\n\tformatValidationError,\n\tformatValidationErrors,\n\tuseTypiaValidation,\n} from '../../hooks';\n\nexport type {\n\tTypiaValidationError,\n\tValidationResult,\n\tValidationState,\n} from '../../hooks';\n";
5
5
  export declare const COMPOUND_PARENT_VALIDATORS_TEMPLATE = "import typia from 'typia';\nimport currentManifest from './manifest-defaults-document';\nimport type {\n\t{{pascalCase}}Attributes,\n\t{{pascalCase}}ValidationResult,\n} from './types';\nimport { createTemplateValidatorToolkit } from '../../validator-toolkit';\n\nconst scaffoldValidators = createTemplateValidatorToolkit< {{pascalCase}}Attributes >( {\n\tassert: typia.createAssert< {{pascalCase}}Attributes >(),\n\tclone: typia.misc.createClone< {{pascalCase}}Attributes >() as (\n\t\tvalue: {{pascalCase}}Attributes,\n\t) => {{pascalCase}}Attributes,\n\tis: typia.createIs< {{pascalCase}}Attributes >(),\n\tmanifest: currentManifest,\n\tprune: typia.misc.createPrune< {{pascalCase}}Attributes >(),\n\trandom: typia.createRandom< {{pascalCase}}Attributes >() as (\n\t\t...args: unknown[]\n\t) => {{pascalCase}}Attributes,\n\tvalidate: typia.createValidate< {{pascalCase}}Attributes >(),\n} );\n\nexport const validate{{pascalCase}}Attributes =\n\tscaffoldValidators.validateAttributes as (\n\t\tattributes: unknown\n\t) => {{pascalCase}}ValidationResult;\n\nexport const validators = scaffoldValidators.validators;\n\nexport const sanitize{{pascalCase}}Attributes =\n\tscaffoldValidators.sanitizeAttributes as (\n\t\tattributes: Partial< {{pascalCase}}Attributes >\n\t) => {{pascalCase}}Attributes;\n\nexport const createAttributeUpdater = scaffoldValidators.createAttributeUpdater;\n";
6
- export declare const COMPOUND_CHILDREN_TEMPLATE = "import type { BlockTemplate } from '@wp-typia/block-types/blocks/registration';\n\nexport const DEFAULT_CHILD_BLOCK_NAME = '{{namespace}}/{{slugKebabCase}}-item';\n\nexport const ALLOWED_CHILD_BLOCKS = [\n\tDEFAULT_CHILD_BLOCK_NAME,\n\t// add-child: insert new allowed child block names here\n];\n\nexport const DEFAULT_CHILD_TEMPLATE: BlockTemplate = [\n\t[\n\t\tDEFAULT_CHILD_BLOCK_NAME,\n\t\t{\n\t\t\tbody: 'Add supporting details for the first internal item.',\n\t\t\ttitle: 'First Item',\n\t\t},\n\t],\n\t[\n\t\tDEFAULT_CHILD_BLOCK_NAME,\n\t\t{\n\t\t\tbody: 'Add supporting details for the second internal item.',\n\t\t\ttitle: 'Second Item',\n\t\t},\n\t],\n];\n";
6
+ export declare const COMPOUND_CHILDREN_TEMPLATE = "import type { BlockTemplate } from '@wp-typia/block-types/blocks/registration';\n\nexport const DEFAULT_CHILD_BLOCK_NAME = '{{namespace}}/{{slugKebabCase}}-item';\n\nexport interface CompoundChildSpec {\n\tancestorKeys: string[];\n\tblockName: string;\n\tbodyPlaceholder: string;\n\tcontainer: boolean;\n\tfolderSlug: string;\n\tkey: string;\n\tplacement: 'nested' | 'root';\n\tsupportsInserter: boolean;\n\ttemplateInstances: Array< Record< string, unknown > >;\n\ttitle: string;\n}\n\nconst ROOT_BLOCK_NAME = '{{namespace}}/{{slugKebabCase}}';\n\nexport const COMPOUND_CHILD_SPECS: CompoundChildSpec[] = [\n\t{\n\t\tancestorKeys: [],\n\t\tblockName: DEFAULT_CHILD_BLOCK_NAME,\n\t\tbodyPlaceholder: 'Add supporting details for this internal item.',\n\t\tcontainer: false,\n\t\tfolderSlug: '{{slugKebabCase}}-item',\n\t\tkey: 'item',\n\t\tplacement: 'root',\n\t\tsupportsInserter: false,\n\t\ttemplateInstances: [\n\t\t\t{\n\t\t\t\tbody: 'Add supporting details for the first internal item.',\n\t\t\t\ttitle: 'First Item',\n\t\t\t},\n\t\t\t{\n\t\t\t\tbody: 'Add supporting details for the second internal item.',\n\t\t\t\ttitle: 'Second Item',\n\t\t\t},\n\t\t],\n\t\ttitle: '{{compoundChildTitle}}',\n\t},\n\t// add-child: insert new child specs here\n];\n\nfunction getChildSpecByKey( key: string ): CompoundChildSpec | undefined {\n\treturn COMPOUND_CHILD_SPECS.find( ( spec ) => spec.key === key );\n}\n\nfunction buildTemplateEntriesForSpec( spec: CompoundChildSpec ): BlockTemplate {\n\tconst nestedTemplate = buildNestedTemplateForKey( spec.key );\n\n\treturn spec.templateInstances.map( ( attributes ) =>\n\t\tnestedTemplate.length > 0\n\t\t\t? [ spec.blockName, attributes, nestedTemplate ]\n\t\t\t: [ spec.blockName, attributes ]\n\t);\n}\n\nfunction buildNestedTemplateForKey( key: string ): BlockTemplate {\n\treturn COMPOUND_CHILD_SPECS.filter(\n\t\t( spec ) =>\n\t\t\tspec.placement === 'nested' &&\n\t\t\tspec.ancestorKeys[ spec.ancestorKeys.length - 1 ] === key\n\t).flatMap( ( spec ) => buildTemplateEntriesForSpec( spec ) );\n}\n\nexport const ALLOWED_CHILD_BLOCKS = COMPOUND_CHILD_SPECS.filter(\n\t( spec ) => spec.placement === 'root'\n).map( ( spec ) => spec.blockName );\n\nexport const DEFAULT_CHILD_TEMPLATE: BlockTemplate =\n\tCOMPOUND_CHILD_SPECS.filter( ( spec ) => spec.placement === 'root' ).flatMap(\n\t\t( spec ) => buildTemplateEntriesForSpec( spec )\n\t);\n\nexport function getChildSpec( blockName: string ): CompoundChildSpec | undefined {\n\treturn COMPOUND_CHILD_SPECS.find( ( spec ) => spec.blockName === blockName );\n}\n\nexport function getChildAllowedBlocks(\n\tblockName: string\n): string[] | undefined {\n\tconst childSpec = getChildSpec( blockName );\n\tif ( ! childSpec ) {\n\t\treturn undefined;\n\t}\n\n\tconst directDescendants = COMPOUND_CHILD_SPECS.filter(\n\t\t( spec ) =>\n\t\t\tspec.placement === 'nested' &&\n\t\t\tspec.ancestorKeys[ spec.ancestorKeys.length - 1 ] === childSpec.key\n\t).map( ( spec ) => spec.blockName );\n\n\tif ( directDescendants.length > 0 ) {\n\t\treturn directDescendants;\n\t}\n\n\treturn childSpec.container ? [] : undefined;\n}\n\nexport function getChildAncestorBlockNames(\n\tblockName: string\n): string[] | undefined {\n\tconst childSpec = getChildSpec( blockName );\n\tif ( ! childSpec || childSpec.ancestorKeys.length === 0 ) {\n\t\treturn undefined;\n\t}\n\n\treturn childSpec.ancestorKeys.flatMap( ( key ) => {\n\t\tconst ancestorSpec = getChildSpecByKey( key );\n\t\treturn ancestorSpec ? [ ancestorSpec.blockName ] : [];\n\t} );\n}\n\nexport function getChildTemplate(\n\tblockName: string\n): BlockTemplate | undefined {\n\tconst childSpec = getChildSpec( blockName );\n\tif ( ! childSpec ) {\n\t\treturn undefined;\n\t}\n\n\tconst nestedTemplate = buildNestedTemplateForKey( childSpec.key );\n\tif ( nestedTemplate.length > 0 ) {\n\t\treturn nestedTemplate;\n\t}\n\n\treturn childSpec.container ? [] : undefined;\n}\n\nexport function hasNestedChildBlocks( blockName: string ): boolean {\n\tconst childSpec = getChildSpec( blockName );\n\tif ( ! childSpec ) {\n\t\treturn false;\n\t}\n\n\treturn childSpec.container || buildNestedTemplateForKey( childSpec.key ).length > 0;\n}\n\nexport function isRootCompoundChildBlock( blockName: string ): boolean {\n\tconst childSpec = getChildSpec( blockName );\n\treturn childSpec?.placement === 'root';\n}\n\nexport { ROOT_BLOCK_NAME };\n";
@@ -203,25 +203,145 @@ export const COMPOUND_CHILDREN_TEMPLATE = `import type { BlockTemplate } from '@
203
203
 
204
204
  export const DEFAULT_CHILD_BLOCK_NAME = '{{namespace}}/{{slugKebabCase}}-item';
205
205
 
206
- export const ALLOWED_CHILD_BLOCKS = [
207
- \tDEFAULT_CHILD_BLOCK_NAME,
208
- \t// add-child: insert new allowed child block names here
209
- ];
206
+ export interface CompoundChildSpec {
207
+ \tancestorKeys: string[];
208
+ \tblockName: string;
209
+ \tbodyPlaceholder: string;
210
+ \tcontainer: boolean;
211
+ \tfolderSlug: string;
212
+ \tkey: string;
213
+ \tplacement: 'nested' | 'root';
214
+ \tsupportsInserter: boolean;
215
+ \ttemplateInstances: Array< Record< string, unknown > >;
216
+ \ttitle: string;
217
+ }
218
+
219
+ const ROOT_BLOCK_NAME = '{{namespace}}/{{slugKebabCase}}';
210
220
 
211
- export const DEFAULT_CHILD_TEMPLATE: BlockTemplate = [
212
- \t[
213
- \t\tDEFAULT_CHILD_BLOCK_NAME,
214
- \t\t{
215
- \t\t\tbody: 'Add supporting details for the first internal item.',
216
- \t\t\ttitle: 'First Item',
217
- \t\t},
218
- \t],
219
- \t[
220
- \t\tDEFAULT_CHILD_BLOCK_NAME,
221
- \t\t{
222
- \t\t\tbody: 'Add supporting details for the second internal item.',
223
- \t\t\ttitle: 'Second Item',
224
- \t\t},
225
- \t],
221
+ export const COMPOUND_CHILD_SPECS: CompoundChildSpec[] = [
222
+ \t{
223
+ \t\tancestorKeys: [],
224
+ \t\tblockName: DEFAULT_CHILD_BLOCK_NAME,
225
+ \t\tbodyPlaceholder: 'Add supporting details for this internal item.',
226
+ \t\tcontainer: false,
227
+ \t\tfolderSlug: '{{slugKebabCase}}-item',
228
+ \t\tkey: 'item',
229
+ \t\tplacement: 'root',
230
+ \t\tsupportsInserter: false,
231
+ \t\ttemplateInstances: [
232
+ \t\t\t{
233
+ \t\t\t\tbody: 'Add supporting details for the first internal item.',
234
+ \t\t\t\ttitle: 'First Item',
235
+ \t\t\t},
236
+ \t\t\t{
237
+ \t\t\t\tbody: 'Add supporting details for the second internal item.',
238
+ \t\t\t\ttitle: 'Second Item',
239
+ \t\t\t},
240
+ \t\t],
241
+ \t\ttitle: '{{compoundChildTitle}}',
242
+ \t},
243
+ \t// add-child: insert new child specs here
226
244
  ];
245
+
246
+ function getChildSpecByKey( key: string ): CompoundChildSpec | undefined {
247
+ \treturn COMPOUND_CHILD_SPECS.find( ( spec ) => spec.key === key );
248
+ }
249
+
250
+ function buildTemplateEntriesForSpec( spec: CompoundChildSpec ): BlockTemplate {
251
+ \tconst nestedTemplate = buildNestedTemplateForKey( spec.key );
252
+
253
+ \treturn spec.templateInstances.map( ( attributes ) =>
254
+ \t\tnestedTemplate.length > 0
255
+ \t\t\t? [ spec.blockName, attributes, nestedTemplate ]
256
+ \t\t\t: [ spec.blockName, attributes ]
257
+ \t);
258
+ }
259
+
260
+ function buildNestedTemplateForKey( key: string ): BlockTemplate {
261
+ \treturn COMPOUND_CHILD_SPECS.filter(
262
+ \t\t( spec ) =>
263
+ \t\t\tspec.placement === 'nested' &&
264
+ \t\t\tspec.ancestorKeys[ spec.ancestorKeys.length - 1 ] === key
265
+ \t).flatMap( ( spec ) => buildTemplateEntriesForSpec( spec ) );
266
+ }
267
+
268
+ export const ALLOWED_CHILD_BLOCKS = COMPOUND_CHILD_SPECS.filter(
269
+ \t( spec ) => spec.placement === 'root'
270
+ ).map( ( spec ) => spec.blockName );
271
+
272
+ export const DEFAULT_CHILD_TEMPLATE: BlockTemplate =
273
+ \tCOMPOUND_CHILD_SPECS.filter( ( spec ) => spec.placement === 'root' ).flatMap(
274
+ \t\t( spec ) => buildTemplateEntriesForSpec( spec )
275
+ \t);
276
+
277
+ export function getChildSpec( blockName: string ): CompoundChildSpec | undefined {
278
+ \treturn COMPOUND_CHILD_SPECS.find( ( spec ) => spec.blockName === blockName );
279
+ }
280
+
281
+ export function getChildAllowedBlocks(
282
+ \tblockName: string
283
+ ): string[] | undefined {
284
+ \tconst childSpec = getChildSpec( blockName );
285
+ \tif ( ! childSpec ) {
286
+ \t\treturn undefined;
287
+ \t}
288
+
289
+ \tconst directDescendants = COMPOUND_CHILD_SPECS.filter(
290
+ \t\t( spec ) =>
291
+ \t\t\tspec.placement === 'nested' &&
292
+ \t\t\tspec.ancestorKeys[ spec.ancestorKeys.length - 1 ] === childSpec.key
293
+ \t).map( ( spec ) => spec.blockName );
294
+
295
+ \tif ( directDescendants.length > 0 ) {
296
+ \t\treturn directDescendants;
297
+ \t}
298
+
299
+ \treturn childSpec.container ? [] : undefined;
300
+ }
301
+
302
+ export function getChildAncestorBlockNames(
303
+ \tblockName: string
304
+ ): string[] | undefined {
305
+ \tconst childSpec = getChildSpec( blockName );
306
+ \tif ( ! childSpec || childSpec.ancestorKeys.length === 0 ) {
307
+ \t\treturn undefined;
308
+ \t}
309
+
310
+ \treturn childSpec.ancestorKeys.flatMap( ( key ) => {
311
+ \t\tconst ancestorSpec = getChildSpecByKey( key );
312
+ \t\treturn ancestorSpec ? [ ancestorSpec.blockName ] : [];
313
+ \t} );
314
+ }
315
+
316
+ export function getChildTemplate(
317
+ \tblockName: string
318
+ ): BlockTemplate | undefined {
319
+ \tconst childSpec = getChildSpec( blockName );
320
+ \tif ( ! childSpec ) {
321
+ \t\treturn undefined;
322
+ \t}
323
+
324
+ \tconst nestedTemplate = buildNestedTemplateForKey( childSpec.key );
325
+ \tif ( nestedTemplate.length > 0 ) {
326
+ \t\treturn nestedTemplate;
327
+ \t}
328
+
329
+ \treturn childSpec.container ? [] : undefined;
330
+ }
331
+
332
+ export function hasNestedChildBlocks( blockName: string ): boolean {
333
+ \tconst childSpec = getChildSpec( blockName );
334
+ \tif ( ! childSpec ) {
335
+ \t\treturn false;
336
+ \t}
337
+
338
+ \treturn childSpec.container || buildNestedTemplateForKey( childSpec.key ).length > 0;
339
+ }
340
+
341
+ export function isRootCompoundChildBlock( blockName: string ): boolean {
342
+ \tconst childSpec = getChildSpec( blockName );
343
+ \treturn childSpec?.placement === 'root';
344
+ }
345
+
346
+ export { ROOT_BLOCK_NAME };
227
347
  `;
@@ -27,7 +27,8 @@ export declare function seedWorkspaceMigrationProject(projectDir: string, curren
27
27
  * succeeds.
28
28
  * @throws {Error} When the template id is unknown, persistence flags are used
29
29
  * with unsupported templates, the command runs outside an official workspace,
30
- * or target block paths already exist.
30
+ * workspace dependencies have not been installed yet, or target block paths
31
+ * already exist.
31
32
  */
32
33
  export declare function runAddBlockCommand({ blockName, cwd, dataStorageMode, externalLayerId, externalLayerSource, persistencePolicy, selectExternalLayerId, templateId, }: RunAddBlockCommandOptions): Promise<{
33
34
  blockSlugs: string[];
@@ -15,8 +15,15 @@ import { buildConfigEntries, buildMigrationBlocks, buildServerTemplateRoot, } fr
15
15
  import { COMPOUND_SHARED_SUPPORT_FILES, collectLegacyCompoundValidatorPaths, ensureBlockConfigCanAddRestManifests, ensureCompoundWorkspaceSupportFiles, } from "./cli-add-block-legacy-validator.js";
16
16
  import { parseTemplateLocator, resolveTemplateSeed, } from "./template-source.js";
17
17
  import { resolveExternalTemplateLayers, } from "./template-layers.js";
18
+ import { formatInstallCommand, } from "./package-managers.js";
18
19
  import { resolveOptionalInteractiveExternalLayerId, } from "./external-layer-selection.js";
19
20
  const COLLECTION_IMPORT_LINE = "import '../../collection';";
21
+ // This is a lightweight preflight heuristic for the common install layouts:
22
+ // node_modules for npm/pnpm/bun/yarn-classic and Yarn PnP marker files.
23
+ // It intentionally favors fast user guidance over exhaustive detection, so
24
+ // stale node_modules or hoisted installs outside the workspace root can still
25
+ // fall through to deeper resolver errors.
26
+ const WORKSPACE_INSTALL_MARKERS = ["node_modules", ".pnp.cjs", ".pnp.loader.mjs"];
20
27
  async function ensureCollectionImport(filePath) {
21
28
  await patchFile(filePath, (source) => {
22
29
  if (source.includes(COLLECTION_IMPORT_LINE)) {
@@ -69,6 +76,15 @@ function resolveExternalLayerSourceFromCaller(source, callerCwd) {
69
76
  }
70
77
  return path.resolve(callerCwd, source);
71
78
  }
79
+ function hasInstalledWorkspaceDependencies(projectDir) {
80
+ return WORKSPACE_INSTALL_MARKERS.some((marker) => fs.existsSync(path.join(projectDir, marker)));
81
+ }
82
+ function assertWorkspaceDependenciesInstalled(workspace) {
83
+ if (hasInstalledWorkspaceDependencies(workspace.projectDir)) {
84
+ return;
85
+ }
86
+ throw new Error(`Workspace dependencies have not been installed yet. Run \`${formatInstallCommand(workspace.packageManager)}\` from the workspace root before using \`wp-typia add block ...\`.`);
87
+ }
72
88
  async function copyScaffoldedBlockSlice(projectDir, templateId, tempProjectDir, variables, legacyValidatorPaths = []) {
73
89
  if (templateId === "compound") {
74
90
  await ensureCompoundWorkspaceSupportFiles(projectDir, tempProjectDir, legacyValidatorPaths);
@@ -248,7 +264,8 @@ export async function seedWorkspaceMigrationProject(projectDir, currentMigration
248
264
  * succeeds.
249
265
  * @throws {Error} When the template id is unknown, persistence flags are used
250
266
  * with unsupported templates, the command runs outside an official workspace,
251
- * or target block paths already exist.
267
+ * workspace dependencies have not been installed yet, or target block paths
268
+ * already exist.
252
269
  */
253
270
  export async function runAddBlockCommand({ blockName, cwd = process.cwd(), dataStorageMode, externalLayerId, externalLayerSource, persistencePolicy, selectExternalLayerId, templateId = "basic", }) {
254
271
  if (!isAddBlockTemplateId(templateId)) {
@@ -257,6 +274,7 @@ export async function runAddBlockCommand({ blockName, cwd = process.cwd(), dataS
257
274
  const resolvedTemplateId = templateId;
258
275
  assertPersistenceFlagsAllowed(resolvedTemplateId, { dataStorageMode, persistencePolicy });
259
276
  const workspace = resolveWorkspaceProject(cwd);
277
+ assertWorkspaceDependenciesInstalled(workspace);
260
278
  const normalizedExternalLayerId = normalizeExternalLayerOption(externalLayerId);
261
279
  const normalizedExternalLayerSource = resolveExternalLayerSourceFromCaller(normalizeExternalLayerOption(externalLayerSource), cwd);
262
280
  const resolvedExternalLayerSelection = await resolveOptionalInteractiveExternalLayerId({
@@ -4,13 +4,25 @@ import { type WorkspaceProject } from "./workspace-project.js";
4
4
  /**
5
5
  * Supported top-level `wp-typia add` kinds exposed by the canonical CLI.
6
6
  */
7
- export declare const ADD_KIND_IDS: readonly ["block", "variation", "pattern", "binding-source", "hooked-block"];
7
+ export declare const ADD_KIND_IDS: readonly ["block", "variation", "pattern", "binding-source", "rest-resource", "hooked-block", "editor-plugin"];
8
8
  export type AddKindId = (typeof ADD_KIND_IDS)[number];
9
+ /**
10
+ * Supported plugin-level REST resource methods accepted by
11
+ * `wp-typia add rest-resource --methods`.
12
+ */
13
+ export declare const REST_RESOURCE_METHOD_IDS: readonly ["list", "read", "create", "update", "delete"];
14
+ export type RestResourceMethodId = (typeof REST_RESOURCE_METHOD_IDS)[number];
15
+ /**
16
+ * Supported editor-plugin shell slots accepted by `wp-typia add editor-plugin --slot`.
17
+ */
18
+ export declare const EDITOR_PLUGIN_SLOT_IDS: readonly ["PluginSidebar"];
19
+ export type EditorPluginSlotId = (typeof EDITOR_PLUGIN_SLOT_IDS)[number];
9
20
  /**
10
21
  * Supported built-in block families accepted by `wp-typia add block --template`.
11
22
  */
12
23
  export declare const ADD_BLOCK_TEMPLATE_IDS: readonly ["basic", "interactivity", "persistence", "compound"];
13
24
  export type AddBlockTemplateId = (typeof ADD_BLOCK_TEMPLATE_IDS)[number];
25
+ export declare const REST_RESOURCE_NAMESPACE_PATTERN: RegExp;
14
26
  export interface RunAddVariationCommandOptions {
15
27
  blockName: string;
16
28
  cwd?: string;
@@ -24,12 +36,32 @@ export interface RunAddBindingSourceCommandOptions {
24
36
  bindingSourceName: string;
25
37
  cwd?: string;
26
38
  }
39
+ export interface RunAddRestResourceCommandOptions {
40
+ cwd?: string;
41
+ methods?: string;
42
+ namespace?: string;
43
+ restResourceName: string;
44
+ }
27
45
  export interface RunAddHookedBlockCommandOptions {
28
46
  anchorBlockName: string;
29
47
  blockName: string;
30
48
  cwd?: string;
31
49
  position: string;
32
50
  }
51
+ /**
52
+ * Options for `wp-typia add editor-plugin`.
53
+ *
54
+ * @property cwd Working directory used to resolve the nearest official workspace.
55
+ * Defaults to `process.cwd()`.
56
+ * @property editorPluginName Human-entered editor plugin name that will be
57
+ * normalized into the generated slug.
58
+ * @property slot Optional editor shell slot. Defaults to `PluginSidebar`.
59
+ */
60
+ export interface RunAddEditorPluginCommandOptions {
61
+ cwd?: string;
62
+ editorPluginName: string;
63
+ slot?: string;
64
+ }
33
65
  export interface RunAddBlockCommandOptions {
34
66
  blockName: string;
35
67
  cwd?: string;
@@ -59,6 +91,9 @@ export interface WorkspaceMutationSnapshot {
59
91
  }
60
92
  export declare function normalizeBlockSlug(input: string): string;
61
93
  export declare function assertValidGeneratedSlug(label: string, slug: string, usage: string): string;
94
+ export declare function assertValidRestResourceNamespace(namespace: string): string;
95
+ export declare function resolveRestResourceNamespace(workspaceNamespace: string, namespace?: string): string;
96
+ export declare function assertValidRestResourceMethods(methods?: string): RestResourceMethodId[];
62
97
  export declare function assertValidHookedBlockPosition(position: string): HookedBlockPositionId;
63
98
  export declare function getWorkspaceBootstrapPath(workspace: WorkspaceProject): string;
64
99
  export declare function buildWorkspacePhpPrefix(workspacePhpPrefix: string, slug: string): string;
@@ -86,6 +121,14 @@ export declare function snapshotWorkspaceFiles(filePaths: string[]): Promise<Wor
86
121
  export declare function rollbackWorkspaceMutation(snapshot: WorkspaceMutationSnapshot): Promise<void>;
87
122
  export declare function resolveWorkspaceBlock(inventory: WorkspaceInventory, blockSlug: string): WorkspaceInventory["blocks"][number];
88
123
  export declare function assertValidHookAnchor(anchorBlockName: string): string;
124
+ /**
125
+ * Validate and normalize the editor plugin shell slot.
126
+ *
127
+ * @param slot Optional shell slot. Defaults to `PluginSidebar`.
128
+ * @returns The canonical editor plugin slot id.
129
+ * @throws {Error} When the slot is not supported by the workspace scaffold.
130
+ */
131
+ export declare function assertValidEditorPluginSlot(slot?: string): EditorPluginSlotId;
89
132
  export declare function readWorkspaceBlockJson(projectDir: string, blockSlug: string): {
90
133
  blockJson: Record<string, unknown>;
91
134
  blockJsonPath: string;
@@ -94,6 +137,17 @@ export declare function getMutableBlockHooks(blockJson: Record<string, unknown>,
94
137
  export declare function assertVariationDoesNotExist(projectDir: string, blockSlug: string, variationSlug: string, inventory: WorkspaceInventory): void;
95
138
  export declare function assertPatternDoesNotExist(projectDir: string, patternSlug: string, inventory: WorkspaceInventory): void;
96
139
  export declare function assertBindingSourceDoesNotExist(projectDir: string, bindingSourceSlug: string, inventory: WorkspaceInventory): void;
140
+ export declare function assertRestResourceDoesNotExist(projectDir: string, restResourceSlug: string, inventory: WorkspaceInventory): void;
141
+ /**
142
+ * Ensure an editor plugin scaffold does not already exist on disk or in the
143
+ * workspace inventory.
144
+ *
145
+ * @param projectDir Workspace root directory.
146
+ * @param editorPluginSlug Normalized editor plugin slug.
147
+ * @param inventory Parsed workspace inventory.
148
+ * @throws {Error} When the directory or inventory entry already exists.
149
+ */
150
+ export declare function assertEditorPluginDoesNotExist(projectDir: string, editorPluginSlug: string, inventory: WorkspaceInventory): void;
97
151
  /**
98
152
  * Returns help text for the canonical `wp-typia add` subcommands.
99
153
  */
@@ -8,7 +8,30 @@ import { WORKSPACE_TEMPLATE_PACKAGE, } from "./workspace-project.js";
8
8
  /**
9
9
  * Supported top-level `wp-typia add` kinds exposed by the canonical CLI.
10
10
  */
11
- export const ADD_KIND_IDS = ["block", "variation", "pattern", "binding-source", "hooked-block"];
11
+ export const ADD_KIND_IDS = [
12
+ "block",
13
+ "variation",
14
+ "pattern",
15
+ "binding-source",
16
+ "rest-resource",
17
+ "hooked-block",
18
+ "editor-plugin",
19
+ ];
20
+ /**
21
+ * Supported plugin-level REST resource methods accepted by
22
+ * `wp-typia add rest-resource --methods`.
23
+ */
24
+ export const REST_RESOURCE_METHOD_IDS = [
25
+ "list",
26
+ "read",
27
+ "create",
28
+ "update",
29
+ "delete",
30
+ ];
31
+ /**
32
+ * Supported editor-plugin shell slots accepted by `wp-typia add editor-plugin --slot`.
33
+ */
34
+ export const EDITOR_PLUGIN_SLOT_IDS = ["PluginSidebar"];
12
35
  /**
13
36
  * Supported built-in block families accepted by `wp-typia add block --template`.
14
37
  */
@@ -19,6 +42,7 @@ export const ADD_BLOCK_TEMPLATE_IDS = [
19
42
  "compound",
20
43
  ];
21
44
  const WORKSPACE_GENERATED_SLUG_PATTERN = /^[a-z][a-z0-9-]*$/;
45
+ export const REST_RESOURCE_NAMESPACE_PATTERN = /^[a-z][a-z0-9-]*(?:\/[a-z0-9-]+)+$/u;
22
46
  export function normalizeBlockSlug(input) {
23
47
  return toKebabCase(input);
24
48
  }
@@ -31,6 +55,33 @@ export function assertValidGeneratedSlug(label, slug, usage) {
31
55
  }
32
56
  return slug;
33
57
  }
58
+ export function assertValidRestResourceNamespace(namespace) {
59
+ const trimmed = namespace.trim();
60
+ if (!trimmed) {
61
+ throw new Error("REST resource namespace is required. Use `--namespace <vendor/v1>` or let the workspace default apply.");
62
+ }
63
+ if (!REST_RESOURCE_NAMESPACE_PATTERN.test(trimmed)) {
64
+ throw new Error("REST resource namespace must use lowercase slash-separated segments like `demo-space/v1`.");
65
+ }
66
+ return trimmed;
67
+ }
68
+ export function resolveRestResourceNamespace(workspaceNamespace, namespace) {
69
+ return assertValidRestResourceNamespace(namespace ?? `${workspaceNamespace}/v1`);
70
+ }
71
+ export function assertValidRestResourceMethods(methods) {
72
+ const rawMethods = typeof methods === "string" && methods.trim().length > 0
73
+ ? methods.split(",").map((value) => value.trim()).filter(Boolean)
74
+ : ["list", "read", "create"];
75
+ const normalizedMethods = Array.from(new Set(rawMethods));
76
+ const invalidMethods = normalizedMethods.filter((method) => !REST_RESOURCE_METHOD_IDS.includes(method));
77
+ if (invalidMethods.length > 0) {
78
+ throw new Error(`REST resource methods must be a comma-separated list of: ${REST_RESOURCE_METHOD_IDS.join(", ")}.`);
79
+ }
80
+ if (normalizedMethods.length === 0) {
81
+ throw new Error("REST resource methods must include at least one of: list, read, create, update, delete.");
82
+ }
83
+ return normalizedMethods;
84
+ }
34
85
  export function assertValidHookedBlockPosition(position) {
35
86
  if (HOOKED_BLOCK_POSITION_IDS.includes(position)) {
36
87
  return position;
@@ -121,6 +172,19 @@ export function assertValidHookAnchor(anchorBlockName) {
121
172
  }
122
173
  return trimmed;
123
174
  }
175
+ /**
176
+ * Validate and normalize the editor plugin shell slot.
177
+ *
178
+ * @param slot Optional shell slot. Defaults to `PluginSidebar`.
179
+ * @returns The canonical editor plugin slot id.
180
+ * @throws {Error} When the slot is not supported by the workspace scaffold.
181
+ */
182
+ export function assertValidEditorPluginSlot(slot = "PluginSidebar") {
183
+ if (EDITOR_PLUGIN_SLOT_IDS.includes(slot)) {
184
+ return slot;
185
+ }
186
+ throw new Error(`Editor plugin slot must be one of: ${EDITOR_PLUGIN_SLOT_IDS.join(", ")}.`);
187
+ }
124
188
  export function readWorkspaceBlockJson(projectDir, blockSlug) {
125
189
  const blockJsonPath = path.join(projectDir, "src", "blocks", blockSlug, "block.json");
126
190
  if (!fs.existsSync(blockJsonPath)) {
@@ -179,6 +243,37 @@ export function assertBindingSourceDoesNotExist(projectDir, bindingSourceSlug, i
179
243
  throw new Error(`A binding source inventory entry already exists for ${bindingSourceSlug}. Choose a different name.`);
180
244
  }
181
245
  }
246
+ export function assertRestResourceDoesNotExist(projectDir, restResourceSlug, inventory) {
247
+ const restResourceDir = path.join(projectDir, "src", "rest", restResourceSlug);
248
+ const restResourcePhpPath = path.join(projectDir, "inc", "rest", `${restResourceSlug}.php`);
249
+ if (fs.existsSync(restResourceDir)) {
250
+ throw new Error(`A REST resource already exists at ${path.relative(projectDir, restResourceDir)}. Choose a different name.`);
251
+ }
252
+ if (fs.existsSync(restResourcePhpPath)) {
253
+ throw new Error(`A REST resource bootstrap already exists at ${path.relative(projectDir, restResourcePhpPath)}. Choose a different name.`);
254
+ }
255
+ if (inventory.restResources.some((entry) => entry.slug === restResourceSlug)) {
256
+ throw new Error(`A REST resource inventory entry already exists for ${restResourceSlug}. Choose a different name.`);
257
+ }
258
+ }
259
+ /**
260
+ * Ensure an editor plugin scaffold does not already exist on disk or in the
261
+ * workspace inventory.
262
+ *
263
+ * @param projectDir Workspace root directory.
264
+ * @param editorPluginSlug Normalized editor plugin slug.
265
+ * @param inventory Parsed workspace inventory.
266
+ * @throws {Error} When the directory or inventory entry already exists.
267
+ */
268
+ export function assertEditorPluginDoesNotExist(projectDir, editorPluginSlug, inventory) {
269
+ const editorPluginDir = path.join(projectDir, "src", "editor-plugins", editorPluginSlug);
270
+ if (fs.existsSync(editorPluginDir)) {
271
+ throw new Error(`An editor plugin already exists at ${path.relative(projectDir, editorPluginDir)}. Choose a different name.`);
272
+ }
273
+ if (inventory.editorPlugins.some((entry) => entry.slug === editorPluginSlug)) {
274
+ throw new Error(`An editor plugin inventory entry already exists for ${editorPluginSlug}. Choose a different name.`);
275
+ }
276
+ }
182
277
  /**
183
278
  * Returns help text for the canonical `wp-typia add` subcommands.
184
279
  */
@@ -188,12 +283,16 @@ export function formatAddHelpText() {
188
283
  wp-typia add variation <name> --block <block-slug>
189
284
  wp-typia add pattern <name>
190
285
  wp-typia add binding-source <name>
286
+ wp-typia add rest-resource <name> [--namespace <vendor/v1>] [--methods <list,read,create,update,delete>]
191
287
  wp-typia add hooked-block <block-slug> --anchor <anchor-block-name> --position <${HOOKED_BLOCK_POSITION_IDS.join("|")}>
288
+ wp-typia add editor-plugin <name> [--slot <${EDITOR_PLUGIN_SLOT_IDS.join("|")}>]
192
289
 
193
290
  Notes:
194
291
  \`wp-typia add\` runs only inside official ${WORKSPACE_TEMPLATE_PACKAGE} workspaces.
195
292
  \`add variation\` targets an existing block slug from \`scripts/block-config.ts\`.
196
293
  \`add pattern\` scaffolds a namespaced PHP pattern shell under \`src/patterns/\`.
197
294
  \`add binding-source\` scaffolds shared PHP and editor registration under \`src/bindings/\`.
198
- \`add hooked-block\` patches an existing workspace block's \`block.json\` \`blockHooks\` metadata.`;
295
+ \`add rest-resource\` scaffolds plugin-level TypeScript REST contracts under \`src/rest/\` and PHP route glue under \`inc/rest/\`.
296
+ \`add hooked-block\` patches an existing workspace block's \`block.json\` \`blockHooks\` metadata.
297
+ \`add editor-plugin\` scaffolds a document-level editor extension under \`src/editor-plugins/\`.`;
199
298
  }