@wp-typia/project-tools 0.17.0 → 0.19.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 (76) hide show
  1. package/dist/runtime/alternate-render-targets.d.ts +5 -0
  2. package/dist/runtime/alternate-render-targets.js +29 -0
  3. package/dist/runtime/block-generator-service-core.d.ts +2 -2
  4. package/dist/runtime/block-generator-service-core.js +13 -8
  5. package/dist/runtime/block-generator-service-spec.d.ts +10 -2
  6. package/dist/runtime/block-generator-service-spec.js +43 -1
  7. package/dist/runtime/built-in-block-artifacts.js +1 -0
  8. package/dist/runtime/built-in-block-code-templates/compound-child.d.ts +2 -2
  9. package/dist/runtime/built-in-block-code-templates/compound-child.js +35 -2
  10. package/dist/runtime/built-in-block-code-templates/compound-parent.d.ts +2 -2
  11. package/dist/runtime/built-in-block-code-templates/compound-parent.js +204 -27
  12. package/dist/runtime/built-in-block-code-templates/compound-persistence.d.ts +1 -1
  13. package/dist/runtime/built-in-block-code-templates/compound-persistence.js +11 -8
  14. package/dist/runtime/built-in-block-non-ts-artifacts.js +505 -2
  15. package/dist/runtime/cli-add-block.d.ts +6 -2
  16. package/dist/runtime/cli-add-block.js +71 -24
  17. package/dist/runtime/cli-add-shared.d.ts +58 -2
  18. package/dist/runtime/cli-add-shared.js +111 -12
  19. package/dist/runtime/cli-add-workspace-assets.d.ts +21 -1
  20. package/dist/runtime/cli-add-workspace-assets.js +417 -1
  21. package/dist/runtime/cli-add-workspace-rest.d.ts +14 -0
  22. package/dist/runtime/cli-add-workspace-rest.js +1060 -0
  23. package/dist/runtime/cli-add-workspace.d.ts +10 -1
  24. package/dist/runtime/cli-add-workspace.js +10 -1
  25. package/dist/runtime/cli-add.d.ts +3 -3
  26. package/dist/runtime/cli-add.js +2 -2
  27. package/dist/runtime/cli-core.d.ts +5 -1
  28. package/dist/runtime/cli-core.js +3 -1
  29. package/dist/runtime/cli-doctor-workspace.js +135 -1
  30. package/dist/runtime/cli-help.js +12 -7
  31. package/dist/runtime/cli-scaffold.d.ts +12 -2
  32. package/dist/runtime/cli-scaffold.js +222 -46
  33. package/dist/runtime/cli-templates.d.ts +4 -4
  34. package/dist/runtime/cli-templates.js +104 -39
  35. package/dist/runtime/cli-validation.d.ts +66 -0
  36. package/dist/runtime/cli-validation.js +92 -0
  37. package/dist/runtime/compound-inner-blocks.d.ts +78 -0
  38. package/dist/runtime/compound-inner-blocks.js +88 -0
  39. package/dist/runtime/index.d.ts +6 -3
  40. package/dist/runtime/index.js +4 -2
  41. package/dist/runtime/local-dev-presets.js +7 -2
  42. package/dist/runtime/migration-command-surface.js +2 -0
  43. package/dist/runtime/package-versions.d.ts +1 -0
  44. package/dist/runtime/package-versions.js +12 -0
  45. package/dist/runtime/rest-resource-artifacts.d.ts +35 -0
  46. package/dist/runtime/rest-resource-artifacts.js +158 -0
  47. package/dist/runtime/scaffold-answer-resolution.js +78 -8
  48. package/dist/runtime/scaffold-apply-utils.d.ts +4 -3
  49. package/dist/runtime/scaffold-apply-utils.js +34 -17
  50. package/dist/runtime/scaffold-bootstrap.d.ts +15 -0
  51. package/dist/runtime/scaffold-bootstrap.js +29 -7
  52. package/dist/runtime/scaffold-documents.js +24 -3
  53. package/dist/runtime/scaffold-identifiers.d.ts +17 -0
  54. package/dist/runtime/scaffold-identifiers.js +22 -0
  55. package/dist/runtime/scaffold-onboarding.js +25 -13
  56. package/dist/runtime/scaffold-package-manager-files.js +6 -1
  57. package/dist/runtime/scaffold-template-variables.js +22 -0
  58. package/dist/runtime/scaffold.d.ts +22 -1
  59. package/dist/runtime/scaffold.js +56 -11
  60. package/dist/runtime/template-render.d.ts +5 -2
  61. package/dist/runtime/template-render.js +9 -3
  62. package/dist/runtime/template-source-contracts.d.ts +11 -0
  63. package/dist/runtime/template-source-external.d.ts +1 -1
  64. package/dist/runtime/template-source-external.js +45 -13
  65. package/dist/runtime/template-source-normalization.d.ts +1 -1
  66. package/dist/runtime/template-source-normalization.js +5 -1
  67. package/dist/runtime/template-source-remote.d.ts +5 -0
  68. package/dist/runtime/template-source-remote.js +33 -0
  69. package/dist/runtime/template-source.js +35 -4
  70. package/dist/runtime/workspace-inventory.d.ts +43 -1
  71. package/dist/runtime/workspace-inventory.js +132 -1
  72. package/dist/runtime/workspace-project.d.ts +1 -1
  73. package/dist/runtime/workspace-project.js +3 -3
  74. package/package.json +9 -4
  75. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +728 -49
  76. package/templates/query-loop/src/validator-toolkit.ts.mustache +0 -1
@@ -10,13 +10,24 @@ import { getDefaultAnswers, scaffoldProject } from "./scaffold.js";
10
10
  import { copyInterpolatedDirectory, listInterpolatedDirectoryOutputs, } from "./template-render.js";
11
11
  import { appendWorkspaceInventoryEntries, } from "./workspace-inventory.js";
12
12
  import { resolveWorkspaceProject, } from "./workspace-project.js";
13
- import { ADD_BLOCK_TEMPLATE_IDS, buildWorkspacePhpPrefix, isAddBlockTemplateId, normalizeBlockSlug, patchFile, readOptionalFile, rollbackWorkspaceMutation, snapshotWorkspaceFiles, } from "./cli-add-shared.js";
13
+ import { ADD_BLOCK_TEMPLATE_IDS, buildWorkspacePhpPrefix, isAddBlockTemplateId, patchFile, readOptionalFile, rollbackWorkspaceMutation, snapshotWorkspaceFiles, } from "./cli-add-shared.js";
14
+ import { resolveNonEmptyNormalizedBlockSlug, } from "./scaffold-identifiers.js";
14
15
  import { buildConfigEntries, buildMigrationBlocks, buildServerTemplateRoot, } from "./cli-add-block-config.js";
15
16
  import { COMPOUND_SHARED_SUPPORT_FILES, collectLegacyCompoundValidatorPaths, ensureBlockConfigCanAddRestManifests, ensureCompoundWorkspaceSupportFiles, } from "./cli-add-block-legacy-validator.js";
16
17
  import { parseTemplateLocator, resolveTemplateSeed, } from "./template-source.js";
17
18
  import { resolveExternalTemplateLayers, } from "./template-layers.js";
19
+ import { formatInstallCommand, } from "./package-managers.js";
20
+ import { parseCompoundInnerBlocksPreset } from "./compound-inner-blocks.js";
18
21
  import { resolveOptionalInteractiveExternalLayerId, } from "./external-layer-selection.js";
22
+ import { parseAlternateRenderTargets } from "./alternate-render-targets.js";
23
+ import { assertExternalLayerCompositionOptions, normalizeOptionalCliString, resolveLocalCliPathOption, } from "./cli-validation.js";
19
24
  const COLLECTION_IMPORT_LINE = "import '../../collection';";
25
+ // This is a lightweight preflight heuristic for the common install layouts:
26
+ // node_modules for npm/pnpm/bun/yarn-classic and Yarn PnP marker files.
27
+ // It intentionally favors fast user guidance over exhaustive detection, so
28
+ // stale node_modules or hoisted installs outside the workspace root can still
29
+ // fall through to deeper resolver errors.
30
+ const WORKSPACE_INSTALL_MARKERS = ["node_modules", ".pnp.cjs", ".pnp.loader.mjs"];
20
31
  async function ensureCollectionImport(filePath) {
21
32
  await patchFile(filePath, (source) => {
22
33
  if (source.includes(COLLECTION_IMPORT_LINE)) {
@@ -54,20 +65,14 @@ async function renderWorkspacePersistenceServerModule(projectDir, variables) {
54
65
  const templateDir = buildServerTemplateRoot(variables.persistencePolicy);
55
66
  await copyInterpolatedDirectory(templateDir, targetDir, variables);
56
67
  }
57
- function normalizeExternalLayerOption(value) {
58
- if (typeof value !== "string") {
59
- return undefined;
60
- }
61
- const trimmed = value.trim();
62
- return trimmed.length > 0 ? trimmed : undefined;
68
+ function hasInstalledWorkspaceDependencies(projectDir) {
69
+ return WORKSPACE_INSTALL_MARKERS.some((marker) => fs.existsSync(path.join(projectDir, marker)));
63
70
  }
64
- function resolveExternalLayerSourceFromCaller(source, callerCwd) {
65
- if (typeof source !== "string" ||
66
- source.length === 0 ||
67
- !(path.isAbsolute(source) || source.startsWith("./") || source.startsWith("../"))) {
68
- return source;
71
+ function assertWorkspaceDependenciesInstalled(workspace) {
72
+ if (hasInstalledWorkspaceDependencies(workspace.projectDir)) {
73
+ return;
69
74
  }
70
- return path.resolve(callerCwd, source);
75
+ throw new Error(`Workspace dependencies have not been installed yet. Run \`${formatInstallCommand(workspace.packageManager)}\` from the workspace root before using \`wp-typia add block ...\`.`);
71
76
  }
72
77
  async function copyScaffoldedBlockSlice(projectDir, templateId, tempProjectDir, variables, legacyValidatorPaths = []) {
73
78
  if (templateId === "compound") {
@@ -191,12 +196,14 @@ async function syncWorkspaceAddedBlockArtifacts(projectDir, templateId, variable
191
196
  }
192
197
  }
193
198
  function assertPersistenceFlagsAllowed(templateId, options) {
194
- const hasPersistenceFlags = typeof options.dataStorageMode === "string" ||
199
+ const hasPersistenceFlags = typeof options.alternateRenderTargets === "string" ||
200
+ typeof options.dataStorageMode === "string" ||
195
201
  typeof options.persistencePolicy === "string";
196
202
  if (!hasPersistenceFlags) {
197
203
  return;
198
204
  }
199
205
  if (templateId === "persistence" || templateId === "compound") {
206
+ parseAlternateRenderTargets(options.alternateRenderTargets);
200
207
  if (typeof options.dataStorageMode === "string" &&
201
208
  options.dataStorageMode !== "custom-table" &&
202
209
  options.dataStorageMode !== "post-meta") {
@@ -207,9 +214,24 @@ function assertPersistenceFlagsAllowed(templateId, options) {
207
214
  options.persistencePolicy !== "public") {
208
215
  throw new Error(`Unsupported persistence policy "${options.persistencePolicy}". Expected one of: authenticated, public.`);
209
216
  }
217
+ if (templateId === "compound" &&
218
+ typeof options.alternateRenderTargets === "string" &&
219
+ !options.dataStorageMode &&
220
+ !options.persistencePolicy) {
221
+ throw new Error("`--alternate-render-targets` on `wp-typia add block --template compound` requires the persistence-enabled server render path. Add `--data-storage <post-meta|custom-table>` or `--persistence-policy <authenticated|public>` first.");
222
+ }
223
+ return;
224
+ }
225
+ throw new Error(`--data-storage, --persistence-policy, and --alternate-render-targets are supported only for \`wp-typia add block --template persistence\` or persistence-enabled \`--template compound\`.`);
226
+ }
227
+ function assertCompoundInnerBlocksPresetAllowed(templateId, innerBlocksPreset) {
228
+ if (!innerBlocksPreset) {
210
229
  return;
211
230
  }
212
- throw new Error(`--data-storage and --persistence-policy are supported only for \`wp-typia add block --template persistence\` or \`--template compound\`.`);
231
+ if (templateId !== "compound") {
232
+ throw new Error("`--inner-blocks-preset` is supported only for `wp-typia add block --template compound`.");
233
+ }
234
+ parseCompoundInnerBlocksPreset(innerBlocksPreset);
213
235
  }
214
236
  /**
215
237
  * Seeds an empty official workspace migration project before any blocks are added.
@@ -239,6 +261,9 @@ export async function seedWorkspaceMigrationProject(projectDir, currentMigration
239
261
  * workspace. Defaults to `process.cwd()`.
240
262
  * @param options.dataStorageMode Optional storage mode for persistence-capable
241
263
  * templates.
264
+ * @param options.innerBlocksPreset Optional compound-only InnerBlocks preset
265
+ * (`freeform`, `ordered`, `horizontal`, or `locked-structure`) that controls
266
+ * the generated authoring defaults for nested compound containers.
242
267
  * @param options.persistencePolicy Optional persistence policy for
243
268
  * persistence-capable templates.
244
269
  * @param options.templateId Built-in block family to scaffold. Defaults to
@@ -248,17 +273,36 @@ export async function seedWorkspaceMigrationProject(projectDir, currentMigration
248
273
  * succeeds.
249
274
  * @throws {Error} When the template id is unknown, persistence flags are used
250
275
  * with unsupported templates, the command runs outside an official workspace,
251
- * or target block paths already exist.
276
+ * workspace dependencies have not been installed yet, or target block paths
277
+ * already exist.
252
278
  */
253
- export async function runAddBlockCommand({ blockName, cwd = process.cwd(), dataStorageMode, externalLayerId, externalLayerSource, persistencePolicy, selectExternalLayerId, templateId = "basic", }) {
279
+ export async function runAddBlockCommand({ alternateRenderTargets, blockName, cwd = process.cwd(), dataStorageMode, externalLayerId, externalLayerSource, innerBlocksPreset, persistencePolicy, selectExternalLayerId, templateId = "basic", }) {
280
+ if (templateId === "query-loop") {
281
+ throw new Error("`wp-typia add block --template query-loop` is not supported. Query Loop is a create-time `core/query` variation scaffold, so use `wp-typia create <project-dir> --template query-loop` instead.");
282
+ }
254
283
  if (!isAddBlockTemplateId(templateId)) {
255
284
  throw new Error(`Unknown add-block template "${templateId}". Expected one of: ${ADD_BLOCK_TEMPLATE_IDS.join(", ")}`);
256
285
  }
257
286
  const resolvedTemplateId = templateId;
258
- assertPersistenceFlagsAllowed(resolvedTemplateId, { dataStorageMode, persistencePolicy });
287
+ assertPersistenceFlagsAllowed(resolvedTemplateId, {
288
+ alternateRenderTargets,
289
+ dataStorageMode,
290
+ persistencePolicy,
291
+ });
292
+ assertCompoundInnerBlocksPresetAllowed(resolvedTemplateId, innerBlocksPreset);
293
+ const resolvedInnerBlocksPreset = parseCompoundInnerBlocksPreset(innerBlocksPreset);
259
294
  const workspace = resolveWorkspaceProject(cwd);
260
- const normalizedExternalLayerId = normalizeExternalLayerOption(externalLayerId);
261
- const normalizedExternalLayerSource = resolveExternalLayerSourceFromCaller(normalizeExternalLayerOption(externalLayerSource), cwd);
295
+ assertWorkspaceDependenciesInstalled(workspace);
296
+ const normalizedExternalLayerId = normalizeOptionalCliString(externalLayerId);
297
+ const normalizedExternalLayerSource = resolveLocalCliPathOption({
298
+ cwd,
299
+ label: "--external-layer-source",
300
+ value: externalLayerSource,
301
+ });
302
+ assertExternalLayerCompositionOptions({
303
+ externalLayerId: normalizedExternalLayerId,
304
+ externalLayerSource: normalizedExternalLayerSource,
305
+ });
262
306
  const resolvedExternalLayerSelection = await resolveOptionalInteractiveExternalLayerId({
263
307
  callerCwd: cwd,
264
308
  externalLayerId: normalizedExternalLayerId,
@@ -267,10 +311,11 @@ export async function runAddBlockCommand({ blockName, cwd = process.cwd(), dataS
267
311
  });
268
312
  let tempRoot = "";
269
313
  try {
270
- const normalizedSlug = normalizeBlockSlug(blockName);
271
- if (!normalizedSlug) {
272
- throw new Error("Block name is required. Use `wp-typia add block <name> --template <family>`.");
273
- }
314
+ const normalizedSlug = resolveNonEmptyNormalizedBlockSlug({
315
+ input: blockName,
316
+ label: "Block name",
317
+ usage: "wp-typia add block <name> --template <family>",
318
+ });
274
319
  const defaults = getDefaultAnswers(normalizedSlug, resolvedTemplateId);
275
320
  tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "wp-typia-add-block-"));
276
321
  const tempProjectDir = path.join(tempRoot, normalizedSlug);
@@ -287,9 +332,11 @@ export async function runAddBlockCommand({ blockName, cwd = process.cwd(), dataS
287
332
  : [];
288
333
  const result = await (async () => {
289
334
  const scaffoldResult = await scaffoldProject({
335
+ alternateRenderTargets,
290
336
  answers: {
291
337
  ...defaults,
292
338
  author: workspace.author,
339
+ compoundInnerBlocksPreset: resolvedInnerBlocksPreset,
293
340
  namespace: workspace.workspace.namespace,
294
341
  phpPrefix: blockPhpPrefix,
295
342
  slug: normalizedSlug,
@@ -1,16 +1,29 @@
1
1
  import { type HookedBlockPositionId } from "./hooked-blocks.js";
2
2
  import { type WorkspaceInventory } from "./workspace-inventory.js";
3
3
  import { type WorkspaceProject } from "./workspace-project.js";
4
+ export { normalizeBlockSlug, } from "./scaffold-identifiers.js";
4
5
  /**
5
6
  * Supported top-level `wp-typia add` kinds exposed by the canonical CLI.
6
7
  */
7
- export declare const ADD_KIND_IDS: readonly ["block", "variation", "pattern", "binding-source", "hooked-block"];
8
+ export declare const ADD_KIND_IDS: readonly ["block", "variation", "pattern", "binding-source", "rest-resource", "hooked-block", "editor-plugin"];
8
9
  export type AddKindId = (typeof ADD_KIND_IDS)[number];
10
+ /**
11
+ * Supported plugin-level REST resource methods accepted by
12
+ * `wp-typia add rest-resource --methods`.
13
+ */
14
+ export declare const REST_RESOURCE_METHOD_IDS: readonly ["list", "read", "create", "update", "delete"];
15
+ export type RestResourceMethodId = (typeof REST_RESOURCE_METHOD_IDS)[number];
16
+ /**
17
+ * Supported editor-plugin shell slots accepted by `wp-typia add editor-plugin --slot`.
18
+ */
19
+ export declare const EDITOR_PLUGIN_SLOT_IDS: readonly ["PluginSidebar"];
20
+ export type EditorPluginSlotId = (typeof EDITOR_PLUGIN_SLOT_IDS)[number];
9
21
  /**
10
22
  * Supported built-in block families accepted by `wp-typia add block --template`.
11
23
  */
12
24
  export declare const ADD_BLOCK_TEMPLATE_IDS: readonly ["basic", "interactivity", "persistence", "compound"];
13
25
  export type AddBlockTemplateId = (typeof ADD_BLOCK_TEMPLATE_IDS)[number];
26
+ export declare const REST_RESOURCE_NAMESPACE_PATTERN: RegExp;
14
27
  export interface RunAddVariationCommandOptions {
15
28
  blockName: string;
16
29
  cwd?: string;
@@ -24,18 +37,40 @@ export interface RunAddBindingSourceCommandOptions {
24
37
  bindingSourceName: string;
25
38
  cwd?: string;
26
39
  }
40
+ export interface RunAddRestResourceCommandOptions {
41
+ cwd?: string;
42
+ methods?: string;
43
+ namespace?: string;
44
+ restResourceName: string;
45
+ }
27
46
  export interface RunAddHookedBlockCommandOptions {
28
47
  anchorBlockName: string;
29
48
  blockName: string;
30
49
  cwd?: string;
31
50
  position: string;
32
51
  }
52
+ /**
53
+ * Options for `wp-typia add editor-plugin`.
54
+ *
55
+ * @property cwd Working directory used to resolve the nearest official workspace.
56
+ * Defaults to `process.cwd()`.
57
+ * @property editorPluginName Human-entered editor plugin name that will be
58
+ * normalized into the generated slug.
59
+ * @property slot Optional editor shell slot. Defaults to `PluginSidebar`.
60
+ */
61
+ export interface RunAddEditorPluginCommandOptions {
62
+ cwd?: string;
63
+ editorPluginName: string;
64
+ slot?: string;
65
+ }
33
66
  export interface RunAddBlockCommandOptions {
67
+ alternateRenderTargets?: string;
34
68
  blockName: string;
35
69
  cwd?: string;
36
70
  dataStorageMode?: string;
37
71
  externalLayerId?: string;
38
72
  externalLayerSource?: string;
73
+ innerBlocksPreset?: string;
39
74
  persistencePolicy?: string;
40
75
  selectExternalLayerId?: (options: Array<{
41
76
  description?: string;
@@ -57,8 +92,10 @@ export interface WorkspaceMutationSnapshot {
57
92
  /** Files or directories created by the mutation that should be removed on rollback. */
58
93
  targetPaths: string[];
59
94
  }
60
- export declare function normalizeBlockSlug(input: string): string;
61
95
  export declare function assertValidGeneratedSlug(label: string, slug: string, usage: string): string;
96
+ export declare function assertValidRestResourceNamespace(namespace: string): string;
97
+ export declare function resolveRestResourceNamespace(workspaceNamespace: string, namespace?: string): string;
98
+ export declare function assertValidRestResourceMethods(methods?: string): RestResourceMethodId[];
62
99
  export declare function assertValidHookedBlockPosition(position: string): HookedBlockPositionId;
63
100
  export declare function getWorkspaceBootstrapPath(workspace: WorkspaceProject): string;
64
101
  export declare function buildWorkspacePhpPrefix(workspacePhpPrefix: string, slug: string): string;
@@ -86,6 +123,14 @@ export declare function snapshotWorkspaceFiles(filePaths: string[]): Promise<Wor
86
123
  export declare function rollbackWorkspaceMutation(snapshot: WorkspaceMutationSnapshot): Promise<void>;
87
124
  export declare function resolveWorkspaceBlock(inventory: WorkspaceInventory, blockSlug: string): WorkspaceInventory["blocks"][number];
88
125
  export declare function assertValidHookAnchor(anchorBlockName: string): string;
126
+ /**
127
+ * Validate and normalize the editor plugin shell slot.
128
+ *
129
+ * @param slot Optional shell slot. Defaults to `PluginSidebar`.
130
+ * @returns The canonical editor plugin slot id.
131
+ * @throws {Error} When the slot is not supported by the workspace scaffold.
132
+ */
133
+ export declare function assertValidEditorPluginSlot(slot?: string): EditorPluginSlotId;
89
134
  export declare function readWorkspaceBlockJson(projectDir: string, blockSlug: string): {
90
135
  blockJson: Record<string, unknown>;
91
136
  blockJsonPath: string;
@@ -94,6 +139,17 @@ export declare function getMutableBlockHooks(blockJson: Record<string, unknown>,
94
139
  export declare function assertVariationDoesNotExist(projectDir: string, blockSlug: string, variationSlug: string, inventory: WorkspaceInventory): void;
95
140
  export declare function assertPatternDoesNotExist(projectDir: string, patternSlug: string, inventory: WorkspaceInventory): void;
96
141
  export declare function assertBindingSourceDoesNotExist(projectDir: string, bindingSourceSlug: string, inventory: WorkspaceInventory): void;
142
+ export declare function assertRestResourceDoesNotExist(projectDir: string, restResourceSlug: string, inventory: WorkspaceInventory): void;
143
+ /**
144
+ * Ensure an editor plugin scaffold does not already exist on disk or in the
145
+ * workspace inventory.
146
+ *
147
+ * @param projectDir Workspace root directory.
148
+ * @param editorPluginSlug Normalized editor plugin slug.
149
+ * @param inventory Parsed workspace inventory.
150
+ * @throws {Error} When the directory or inventory entry already exists.
151
+ */
152
+ export declare function assertEditorPluginDoesNotExist(projectDir: string, editorPluginSlug: string, inventory: WorkspaceInventory): void;
97
153
  /**
98
154
  * Returns help text for the canonical `wp-typia add` subcommands.
99
155
  */
@@ -3,12 +3,36 @@ import { promises as fsp } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { parseScaffoldBlockMetadata } from "@wp-typia/block-runtime/blocks";
5
5
  import { HOOKED_BLOCK_ANCHOR_PATTERN, HOOKED_BLOCK_POSITION_IDS, } from "./hooked-blocks.js";
6
- import { toKebabCase, toSnakeCase, } from "./string-case.js";
6
+ import { toSnakeCase, } from "./string-case.js";
7
7
  import { WORKSPACE_TEMPLATE_PACKAGE, } from "./workspace-project.js";
8
+ export { normalizeBlockSlug, } from "./scaffold-identifiers.js";
8
9
  /**
9
10
  * Supported top-level `wp-typia add` kinds exposed by the canonical CLI.
10
11
  */
11
- export const ADD_KIND_IDS = ["block", "variation", "pattern", "binding-source", "hooked-block"];
12
+ export const ADD_KIND_IDS = [
13
+ "block",
14
+ "variation",
15
+ "pattern",
16
+ "binding-source",
17
+ "rest-resource",
18
+ "hooked-block",
19
+ "editor-plugin",
20
+ ];
21
+ /**
22
+ * Supported plugin-level REST resource methods accepted by
23
+ * `wp-typia add rest-resource --methods`.
24
+ */
25
+ export const REST_RESOURCE_METHOD_IDS = [
26
+ "list",
27
+ "read",
28
+ "create",
29
+ "update",
30
+ "delete",
31
+ ];
32
+ /**
33
+ * Supported editor-plugin shell slots accepted by `wp-typia add editor-plugin --slot`.
34
+ */
35
+ export const EDITOR_PLUGIN_SLOT_IDS = ["PluginSidebar"];
12
36
  /**
13
37
  * Supported built-in block families accepted by `wp-typia add block --template`.
14
38
  */
@@ -19,9 +43,7 @@ export const ADD_BLOCK_TEMPLATE_IDS = [
19
43
  "compound",
20
44
  ];
21
45
  const WORKSPACE_GENERATED_SLUG_PATTERN = /^[a-z][a-z0-9-]*$/;
22
- export function normalizeBlockSlug(input) {
23
- return toKebabCase(input);
24
- }
46
+ export const REST_RESOURCE_NAMESPACE_PATTERN = /^[a-z][a-z0-9-]*(?:\/[a-z0-9-]+)+$/u;
25
47
  export function assertValidGeneratedSlug(label, slug, usage) {
26
48
  if (!slug) {
27
49
  throw new Error(`${label} is required. Use \`${usage}\`.`);
@@ -31,6 +53,33 @@ export function assertValidGeneratedSlug(label, slug, usage) {
31
53
  }
32
54
  return slug;
33
55
  }
56
+ export function assertValidRestResourceNamespace(namespace) {
57
+ const trimmed = namespace.trim();
58
+ if (!trimmed) {
59
+ throw new Error("REST resource namespace is required. Use `--namespace <vendor/v1>` or let the workspace default apply.");
60
+ }
61
+ if (!REST_RESOURCE_NAMESPACE_PATTERN.test(trimmed)) {
62
+ throw new Error("REST resource namespace must use lowercase slash-separated segments like `demo-space/v1`.");
63
+ }
64
+ return trimmed;
65
+ }
66
+ export function resolveRestResourceNamespace(workspaceNamespace, namespace) {
67
+ return assertValidRestResourceNamespace(namespace ?? `${workspaceNamespace}/v1`);
68
+ }
69
+ export function assertValidRestResourceMethods(methods) {
70
+ const rawMethods = typeof methods === "string" && methods.trim().length > 0
71
+ ? methods.split(",").map((value) => value.trim()).filter(Boolean)
72
+ : ["list", "read", "create"];
73
+ const normalizedMethods = Array.from(new Set(rawMethods));
74
+ const invalidMethods = normalizedMethods.filter((method) => !REST_RESOURCE_METHOD_IDS.includes(method));
75
+ if (invalidMethods.length > 0) {
76
+ throw new Error(`REST resource methods must be a comma-separated list of: ${REST_RESOURCE_METHOD_IDS.join(", ")}.`);
77
+ }
78
+ if (normalizedMethods.length === 0) {
79
+ throw new Error("REST resource methods must include at least one of: list, read, create, update, delete.");
80
+ }
81
+ return normalizedMethods;
82
+ }
34
83
  export function assertValidHookedBlockPosition(position) {
35
84
  if (HOOKED_BLOCK_POSITION_IDS.includes(position)) {
36
85
  return position;
@@ -121,6 +170,19 @@ export function assertValidHookAnchor(anchorBlockName) {
121
170
  }
122
171
  return trimmed;
123
172
  }
173
+ /**
174
+ * Validate and normalize the editor plugin shell slot.
175
+ *
176
+ * @param slot Optional shell slot. Defaults to `PluginSidebar`.
177
+ * @returns The canonical editor plugin slot id.
178
+ * @throws {Error} When the slot is not supported by the workspace scaffold.
179
+ */
180
+ export function assertValidEditorPluginSlot(slot = "PluginSidebar") {
181
+ if (EDITOR_PLUGIN_SLOT_IDS.includes(slot)) {
182
+ return slot;
183
+ }
184
+ throw new Error(`Editor plugin slot must be one of: ${EDITOR_PLUGIN_SLOT_IDS.join(", ")}.`);
185
+ }
124
186
  export function readWorkspaceBlockJson(projectDir, blockSlug) {
125
187
  const blockJsonPath = path.join(projectDir, "src", "blocks", blockSlug, "block.json");
126
188
  if (!fs.existsSync(blockJsonPath)) {
@@ -179,21 +241,58 @@ export function assertBindingSourceDoesNotExist(projectDir, bindingSourceSlug, i
179
241
  throw new Error(`A binding source inventory entry already exists for ${bindingSourceSlug}. Choose a different name.`);
180
242
  }
181
243
  }
244
+ export function assertRestResourceDoesNotExist(projectDir, restResourceSlug, inventory) {
245
+ const restResourceDir = path.join(projectDir, "src", "rest", restResourceSlug);
246
+ const restResourcePhpPath = path.join(projectDir, "inc", "rest", `${restResourceSlug}.php`);
247
+ if (fs.existsSync(restResourceDir)) {
248
+ throw new Error(`A REST resource already exists at ${path.relative(projectDir, restResourceDir)}. Choose a different name.`);
249
+ }
250
+ if (fs.existsSync(restResourcePhpPath)) {
251
+ throw new Error(`A REST resource bootstrap already exists at ${path.relative(projectDir, restResourcePhpPath)}. Choose a different name.`);
252
+ }
253
+ if (inventory.restResources.some((entry) => entry.slug === restResourceSlug)) {
254
+ throw new Error(`A REST resource inventory entry already exists for ${restResourceSlug}. Choose a different name.`);
255
+ }
256
+ }
257
+ /**
258
+ * Ensure an editor plugin scaffold does not already exist on disk or in the
259
+ * workspace inventory.
260
+ *
261
+ * @param projectDir Workspace root directory.
262
+ * @param editorPluginSlug Normalized editor plugin slug.
263
+ * @param inventory Parsed workspace inventory.
264
+ * @throws {Error} When the directory or inventory entry already exists.
265
+ */
266
+ export function assertEditorPluginDoesNotExist(projectDir, editorPluginSlug, inventory) {
267
+ const editorPluginDir = path.join(projectDir, "src", "editor-plugins", editorPluginSlug);
268
+ if (fs.existsSync(editorPluginDir)) {
269
+ throw new Error(`An editor plugin already exists at ${path.relative(projectDir, editorPluginDir)}. Choose a different name.`);
270
+ }
271
+ if (inventory.editorPlugins.some((entry) => entry.slug === editorPluginSlug)) {
272
+ throw new Error(`An editor plugin inventory entry already exists for ${editorPluginSlug}. Choose a different name.`);
273
+ }
274
+ }
182
275
  /**
183
276
  * Returns help text for the canonical `wp-typia add` subcommands.
184
277
  */
185
278
  export function formatAddHelpText() {
186
279
  return `Usage:
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>]
188
- wp-typia add variation <name> --block <block-slug>
189
- wp-typia add pattern <name>
190
- wp-typia add binding-source <name>
191
- wp-typia add hooked-block <block-slug> --anchor <anchor-block-name> --position <${HOOKED_BLOCK_POSITION_IDS.join("|")}>
280
+ 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>] [--inner-blocks-preset <freeform|ordered|horizontal|locked-structure>] [--alternate-render-targets <email,mjml,plain-text>] [--data-storage <post-meta|custom-table>] [--persistence-policy <authenticated|public>] [--dry-run]
281
+ wp-typia add variation <name> --block <block-slug> [--dry-run]
282
+ wp-typia add pattern <name> [--dry-run]
283
+ wp-typia add binding-source <name> [--dry-run]
284
+ wp-typia add rest-resource <name> [--namespace <vendor/v1>] [--methods <list,read,create,update,delete>] [--dry-run]
285
+ wp-typia add hooked-block <block-slug> --anchor <anchor-block-name> --position <${HOOKED_BLOCK_POSITION_IDS.join("|")}> [--dry-run]
286
+ wp-typia add editor-plugin <name> [--slot <${EDITOR_PLUGIN_SLOT_IDS.join("|")}>] [--dry-run]
192
287
 
193
288
  Notes:
194
- \`wp-typia add\` runs only inside official ${WORKSPACE_TEMPLATE_PACKAGE} workspaces.
289
+ \`wp-typia add\` runs only inside official ${WORKSPACE_TEMPLATE_PACKAGE} workspaces scaffolded via \`wp-typia create <project-dir> --template workspace\`.
290
+ Pass \`--dry-run\` to preview the workspace files that would change without writing them.
291
+ \`query-loop\` is a create-time scaffold family. Use \`wp-typia create <project-dir> --template query-loop\` instead of \`wp-typia add block\`.
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
  }
@@ -1,4 +1,24 @@
1
- import { type RunAddBindingSourceCommandOptions, type RunAddPatternCommandOptions } from "./cli-add-shared.js";
1
+ import { type RunAddBindingSourceCommandOptions, type RunAddEditorPluginCommandOptions, type RunAddPatternCommandOptions } from "./cli-add-shared.js";
2
+ /**
3
+ * Add one document-level editor plugin scaffold to an official workspace project.
4
+ *
5
+ * @param options Command options for the editor-plugin scaffold workflow.
6
+ * @param options.cwd Working directory used to resolve the nearest official workspace.
7
+ * Defaults to `process.cwd()`.
8
+ * @param options.editorPluginName Human-entered editor-plugin name that will be
9
+ * normalized and validated before files are written.
10
+ * @param options.slot Optional editor plugin shell slot. Defaults to `PluginSidebar`.
11
+ * @returns A promise that resolves with the normalized `editorPluginSlug`, chosen
12
+ * `slot`, and owning `projectDir` after the scaffold files and inventory entry
13
+ * are written successfully.
14
+ * @throws {Error} When the command is run outside an official workspace, when the
15
+ * slug or slot is invalid, or when a conflicting file or inventory entry exists.
16
+ */
17
+ export declare function runAddEditorPluginCommand({ cwd, editorPluginName, slot, }: RunAddEditorPluginCommandOptions): Promise<{
18
+ editorPluginSlug: string;
19
+ projectDir: string;
20
+ slot: string;
21
+ }>;
2
22
  /**
3
23
  * Add one PHP block pattern shell to an official workspace project.
4
24
  *