@wp-typia/project-tools 0.18.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 (44) 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 +1 -1
  4. package/dist/runtime/block-generator-service-core.js +11 -7
  5. package/dist/runtime/block-generator-service-spec.d.ts +8 -1
  6. package/dist/runtime/block-generator-service-spec.js +43 -1
  7. package/dist/runtime/built-in-block-code-templates/compound-child.d.ts +1 -1
  8. package/dist/runtime/built-in-block-code-templates/compound-child.js +14 -9
  9. package/dist/runtime/built-in-block-code-templates/compound-parent.d.ts +2 -2
  10. package/dist/runtime/built-in-block-code-templates/compound-parent.js +100 -43
  11. package/dist/runtime/built-in-block-code-templates/compound-persistence.d.ts +1 -1
  12. package/dist/runtime/built-in-block-code-templates/compound-persistence.js +11 -8
  13. package/dist/runtime/built-in-block-non-ts-artifacts.js +505 -2
  14. package/dist/runtime/cli-add-block.d.ts +4 -1
  15. package/dist/runtime/cli-add-block.js +55 -26
  16. package/dist/runtime/cli-add-shared.d.ts +3 -1
  17. package/dist/runtime/cli-add-shared.js +12 -12
  18. package/dist/runtime/cli-core.d.ts +2 -0
  19. package/dist/runtime/cli-core.js +1 -0
  20. package/dist/runtime/cli-help.js +4 -3
  21. package/dist/runtime/cli-scaffold.d.ts +3 -1
  22. package/dist/runtime/cli-scaffold.js +88 -12
  23. package/dist/runtime/cli-templates.js +26 -1
  24. package/dist/runtime/cli-validation.d.ts +66 -0
  25. package/dist/runtime/cli-validation.js +92 -0
  26. package/dist/runtime/compound-inner-blocks.d.ts +78 -0
  27. package/dist/runtime/compound-inner-blocks.js +88 -0
  28. package/dist/runtime/index.d.ts +2 -0
  29. package/dist/runtime/index.js +1 -0
  30. package/dist/runtime/migration-command-surface.js +2 -0
  31. package/dist/runtime/package-versions.d.ts +1 -0
  32. package/dist/runtime/package-versions.js +12 -0
  33. package/dist/runtime/scaffold-answer-resolution.js +10 -6
  34. package/dist/runtime/scaffold-documents.js +22 -2
  35. package/dist/runtime/scaffold-identifiers.d.ts +17 -0
  36. package/dist/runtime/scaffold-identifiers.js +22 -0
  37. package/dist/runtime/scaffold-onboarding.js +21 -13
  38. package/dist/runtime/scaffold-template-variables.js +22 -0
  39. package/dist/runtime/scaffold.d.ts +16 -1
  40. package/dist/runtime/scaffold.js +7 -4
  41. package/dist/runtime/template-source.js +5 -3
  42. package/dist/runtime/workspace-project.js +1 -1
  43. package/package.json +7 -2
  44. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +318 -18
@@ -10,13 +10,17 @@ 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";
18
19
  import { formatInstallCommand, } from "./package-managers.js";
20
+ import { parseCompoundInnerBlocksPreset } from "./compound-inner-blocks.js";
19
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";
20
24
  const COLLECTION_IMPORT_LINE = "import '../../collection';";
21
25
  // This is a lightweight preflight heuristic for the common install layouts:
22
26
  // node_modules for npm/pnpm/bun/yarn-classic and Yarn PnP marker files.
@@ -61,21 +65,6 @@ async function renderWorkspacePersistenceServerModule(projectDir, variables) {
61
65
  const templateDir = buildServerTemplateRoot(variables.persistencePolicy);
62
66
  await copyInterpolatedDirectory(templateDir, targetDir, variables);
63
67
  }
64
- function normalizeExternalLayerOption(value) {
65
- if (typeof value !== "string") {
66
- return undefined;
67
- }
68
- const trimmed = value.trim();
69
- return trimmed.length > 0 ? trimmed : undefined;
70
- }
71
- function resolveExternalLayerSourceFromCaller(source, callerCwd) {
72
- if (typeof source !== "string" ||
73
- source.length === 0 ||
74
- !(path.isAbsolute(source) || source.startsWith("./") || source.startsWith("../"))) {
75
- return source;
76
- }
77
- return path.resolve(callerCwd, source);
78
- }
79
68
  function hasInstalledWorkspaceDependencies(projectDir) {
80
69
  return WORKSPACE_INSTALL_MARKERS.some((marker) => fs.existsSync(path.join(projectDir, marker)));
81
70
  }
@@ -207,12 +196,14 @@ async function syncWorkspaceAddedBlockArtifacts(projectDir, templateId, variable
207
196
  }
208
197
  }
209
198
  function assertPersistenceFlagsAllowed(templateId, options) {
210
- const hasPersistenceFlags = typeof options.dataStorageMode === "string" ||
199
+ const hasPersistenceFlags = typeof options.alternateRenderTargets === "string" ||
200
+ typeof options.dataStorageMode === "string" ||
211
201
  typeof options.persistencePolicy === "string";
212
202
  if (!hasPersistenceFlags) {
213
203
  return;
214
204
  }
215
205
  if (templateId === "persistence" || templateId === "compound") {
206
+ parseAlternateRenderTargets(options.alternateRenderTargets);
216
207
  if (typeof options.dataStorageMode === "string" &&
217
208
  options.dataStorageMode !== "custom-table" &&
218
209
  options.dataStorageMode !== "post-meta") {
@@ -223,9 +214,24 @@ function assertPersistenceFlagsAllowed(templateId, options) {
223
214
  options.persistencePolicy !== "public") {
224
215
  throw new Error(`Unsupported persistence policy "${options.persistencePolicy}". Expected one of: authenticated, public.`);
225
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
+ }
226
223
  return;
227
224
  }
228
- throw new Error(`--data-storage and --persistence-policy are supported only for \`wp-typia add block --template persistence\` or \`--template compound\`.`);
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) {
229
+ return;
230
+ }
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);
229
235
  }
230
236
  /**
231
237
  * Seeds an empty official workspace migration project before any blocks are added.
@@ -255,6 +261,9 @@ export async function seedWorkspaceMigrationProject(projectDir, currentMigration
255
261
  * workspace. Defaults to `process.cwd()`.
256
262
  * @param options.dataStorageMode Optional storage mode for persistence-capable
257
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.
258
267
  * @param options.persistencePolicy Optional persistence policy for
259
268
  * persistence-capable templates.
260
269
  * @param options.templateId Built-in block family to scaffold. Defaults to
@@ -267,16 +276,33 @@ export async function seedWorkspaceMigrationProject(projectDir, currentMigration
267
276
  * workspace dependencies have not been installed yet, or target block paths
268
277
  * already exist.
269
278
  */
270
- 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
+ }
271
283
  if (!isAddBlockTemplateId(templateId)) {
272
284
  throw new Error(`Unknown add-block template "${templateId}". Expected one of: ${ADD_BLOCK_TEMPLATE_IDS.join(", ")}`);
273
285
  }
274
286
  const resolvedTemplateId = templateId;
275
- assertPersistenceFlagsAllowed(resolvedTemplateId, { dataStorageMode, persistencePolicy });
287
+ assertPersistenceFlagsAllowed(resolvedTemplateId, {
288
+ alternateRenderTargets,
289
+ dataStorageMode,
290
+ persistencePolicy,
291
+ });
292
+ assertCompoundInnerBlocksPresetAllowed(resolvedTemplateId, innerBlocksPreset);
293
+ const resolvedInnerBlocksPreset = parseCompoundInnerBlocksPreset(innerBlocksPreset);
276
294
  const workspace = resolveWorkspaceProject(cwd);
277
295
  assertWorkspaceDependenciesInstalled(workspace);
278
- const normalizedExternalLayerId = normalizeExternalLayerOption(externalLayerId);
279
- const normalizedExternalLayerSource = resolveExternalLayerSourceFromCaller(normalizeExternalLayerOption(externalLayerSource), cwd);
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
+ });
280
306
  const resolvedExternalLayerSelection = await resolveOptionalInteractiveExternalLayerId({
281
307
  callerCwd: cwd,
282
308
  externalLayerId: normalizedExternalLayerId,
@@ -285,10 +311,11 @@ export async function runAddBlockCommand({ blockName, cwd = process.cwd(), dataS
285
311
  });
286
312
  let tempRoot = "";
287
313
  try {
288
- const normalizedSlug = normalizeBlockSlug(blockName);
289
- if (!normalizedSlug) {
290
- throw new Error("Block name is required. Use `wp-typia add block <name> --template <family>`.");
291
- }
314
+ const normalizedSlug = resolveNonEmptyNormalizedBlockSlug({
315
+ input: blockName,
316
+ label: "Block name",
317
+ usage: "wp-typia add block <name> --template <family>",
318
+ });
292
319
  const defaults = getDefaultAnswers(normalizedSlug, resolvedTemplateId);
293
320
  tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "wp-typia-add-block-"));
294
321
  const tempProjectDir = path.join(tempRoot, normalizedSlug);
@@ -305,9 +332,11 @@ export async function runAddBlockCommand({ blockName, cwd = process.cwd(), dataS
305
332
  : [];
306
333
  const result = await (async () => {
307
334
  const scaffoldResult = await scaffoldProject({
335
+ alternateRenderTargets,
308
336
  answers: {
309
337
  ...defaults,
310
338
  author: workspace.author,
339
+ compoundInnerBlocksPreset: resolvedInnerBlocksPreset,
311
340
  namespace: workspace.workspace.namespace,
312
341
  phpPrefix: blockPhpPrefix,
313
342
  slug: normalizedSlug,
@@ -1,6 +1,7 @@
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
  */
@@ -63,11 +64,13 @@ export interface RunAddEditorPluginCommandOptions {
63
64
  slot?: string;
64
65
  }
65
66
  export interface RunAddBlockCommandOptions {
67
+ alternateRenderTargets?: string;
66
68
  blockName: string;
67
69
  cwd?: string;
68
70
  dataStorageMode?: string;
69
71
  externalLayerId?: string;
70
72
  externalLayerSource?: string;
73
+ innerBlocksPreset?: string;
71
74
  persistencePolicy?: string;
72
75
  selectExternalLayerId?: (options: Array<{
73
76
  description?: string;
@@ -89,7 +92,6 @@ export interface WorkspaceMutationSnapshot {
89
92
  /** Files or directories created by the mutation that should be removed on rollback. */
90
93
  targetPaths: string[];
91
94
  }
92
- export declare function normalizeBlockSlug(input: string): string;
93
95
  export declare function assertValidGeneratedSlug(label: string, slug: string, usage: string): string;
94
96
  export declare function assertValidRestResourceNamespace(namespace: string): string;
95
97
  export declare function resolveRestResourceNamespace(workspaceNamespace: string, namespace?: string): string;
@@ -3,8 +3,9 @@ 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
  */
@@ -43,9 +44,6 @@ export const ADD_BLOCK_TEMPLATE_IDS = [
43
44
  ];
44
45
  const WORKSPACE_GENERATED_SLUG_PATTERN = /^[a-z][a-z0-9-]*$/;
45
46
  export const REST_RESOURCE_NAMESPACE_PATTERN = /^[a-z][a-z0-9-]*(?:\/[a-z0-9-]+)+$/u;
46
- export function normalizeBlockSlug(input) {
47
- return toKebabCase(input);
48
- }
49
47
  export function assertValidGeneratedSlug(label, slug, usage) {
50
48
  if (!slug) {
51
49
  throw new Error(`${label} is required. Use \`${usage}\`.`);
@@ -279,16 +277,18 @@ export function assertEditorPluginDoesNotExist(projectDir, editorPluginSlug, inv
279
277
  */
280
278
  export function formatAddHelpText() {
281
279
  return `Usage:
282
- 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>]
283
- wp-typia add variation <name> --block <block-slug>
284
- wp-typia add pattern <name>
285
- wp-typia add binding-source <name>
286
- wp-typia add rest-resource <name> [--namespace <vendor/v1>] [--methods <list,read,create,update,delete>]
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("|")}>]
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]
289
287
 
290
288
  Notes:
291
- \`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\`.
292
292
  \`add variation\` targets an existing block slug from \`scripts/block-config.ts\`.
293
293
  \`add pattern\` scaffolds a namespaced PHP pattern shell under \`src/patterns/\`.
294
294
  \`add binding-source\` scaffolds shared PHP and editor registration under \`src/bindings/\`.
@@ -27,6 +27,8 @@ export { getDoctorChecks, runDoctor, type DoctorCheck } from "./cli-doctor.js";
27
27
  export { createCliCommandError, CliDiagnosticError, formatCliDiagnosticError, formatDoctorCheckLine, formatDoctorSummaryLine, getDoctorFailureDetailLines, getFailingDoctorChecks, isCliDiagnosticError, } from "./cli-diagnostics.js";
28
28
  export type { CliDiagnosticMessage } from "./cli-diagnostics.js";
29
29
  export { EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, getWorkspaceBlockSelectOptions, runAddBindingSourceCommand, runAddBlockCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddRestResourceCommand, runAddVariationCommand, seedWorkspaceMigrationProject, } from "./cli-add.js";
30
+ export { COMPOUND_INNER_BLOCKS_PRESET_IDS, getCompoundInnerBlocksPresetDefinition, } from "./compound-inner-blocks.js";
31
+ export type { CompoundInnerBlocksPresetId } from "./compound-inner-blocks.js";
30
32
  export { HOOKED_BLOCK_POSITION_IDS } from "./hooked-blocks.js";
31
33
  export type { EditorPluginSlotId } from "./cli-add.js";
32
34
  export type { HookedBlockPositionId } from "./hooked-blocks.js";
@@ -26,6 +26,7 @@
26
26
  export { getDoctorChecks, runDoctor } from "./cli-doctor.js";
27
27
  export { createCliCommandError, CliDiagnosticError, formatCliDiagnosticError, formatDoctorCheckLine, formatDoctorSummaryLine, getDoctorFailureDetailLines, getFailingDoctorChecks, isCliDiagnosticError, } from "./cli-diagnostics.js";
28
28
  export { EDITOR_PLUGIN_SLOT_IDS, formatAddHelpText, getWorkspaceBlockSelectOptions, runAddBindingSourceCommand, runAddBlockCommand, runAddEditorPluginCommand, runAddHookedBlockCommand, runAddPatternCommand, runAddRestResourceCommand, runAddVariationCommand, seedWorkspaceMigrationProject, } from "./cli-add.js";
29
+ export { COMPOUND_INNER_BLOCKS_PRESET_IDS, getCompoundInnerBlocksPresetDefinition, } from "./compound-inner-blocks.js";
29
30
  export { HOOKED_BLOCK_POSITION_IDS } from "./hooked-blocks.js";
30
31
  export { formatHelpText } from "./cli-help.js";
31
32
  export { getNextSteps, getOptionalOnboarding, runScaffoldFlow, } from "./cli-scaffold.js";
@@ -14,10 +14,10 @@ export function formatHelpText() {
14
14
  wp-typia create <project-dir> [--template query-loop] [--external-layer-source <./path|github:owner/repo/path[#ref]|npm-package>] [--external-layer-id <layer-id>] [--query-post-type <post-type>] [--namespace <value>] [--text-domain <value>] [--php-prefix <value>] [--with-migration-ui] [--with-wp-env] [--with-test-preset] [--yes] [--dry-run] [--no-install] [--package-manager <id>]
15
15
  wp-typia create <project-dir> [--template <./path|github:owner/repo/path[#ref]>] [--variant <name>] [--namespace <value>] [--text-domain <value>] [--php-prefix <value>] [--with-wp-env] [--with-test-preset] [--yes] [--dry-run] [--no-install] [--package-manager <id>]
16
16
  wp-typia create <project-dir> [--template <npm-package>] [--variant <name>] [--namespace <value>] [--text-domain <value>] [--php-prefix <value>] [--with-wp-env] [--with-test-preset] [--yes] [--dry-run] [--no-install] [--package-manager <id>]
17
- wp-typia create <project-dir> [--template persistence] [--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>] [--namespace <value>] [--text-domain <value>] [--php-prefix <value>] [--with-migration-ui] [--with-wp-env] [--with-test-preset] [--yes] [--dry-run] [--no-install] [--package-manager <id>]
18
- wp-typia create <project-dir> [--template compound] [--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>] [--namespace <value>] [--text-domain <value>] [--php-prefix <value>] [--with-migration-ui] [--with-wp-env] [--with-test-preset] [--yes] [--dry-run] [--no-install] [--package-manager <id>]
17
+ wp-typia create <project-dir> [--template persistence] [--external-layer-source <./path|github:owner/repo/path[#ref]|npm-package>] [--external-layer-id <layer-id>] [--alternate-render-targets <email,mjml,plain-text>] [--data-storage <post-meta|custom-table>] [--persistence-policy <authenticated|public>] [--namespace <value>] [--text-domain <value>] [--php-prefix <value>] [--with-migration-ui] [--with-wp-env] [--with-test-preset] [--yes] [--dry-run] [--no-install] [--package-manager <id>]
18
+ wp-typia create <project-dir> [--template compound] [--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>] [--namespace <value>] [--text-domain <value>] [--php-prefix <value>] [--with-migration-ui] [--with-wp-env] [--with-test-preset] [--yes] [--dry-run] [--no-install] [--package-manager <id>]
19
19
  wp-typia <project-dir> [create flags...]
20
- wp-typia add block <name> --template <basic|interactivity|persistence|compound> [--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>]
20
+ wp-typia add block <name> --template <basic|interactivity|persistence|compound> [--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>]
21
21
  wp-typia add variation <name> --block <block-slug>
22
22
  wp-typia add pattern <name>
23
23
  wp-typia add binding-source <name>
@@ -38,6 +38,7 @@ Notes:
38
38
  \`wp-typia create\` is the canonical scaffold command.
39
39
  \`wp-typia <project-dir>\` remains a backward-compatible alias to \`create\` when \`<project-dir>\` is the only positional argument.
40
40
  Use \`--template workspace\` as shorthand for \`@wp-typia/create-workspace-template\`, the official empty workspace scaffold behind \`wp-typia add ...\`.
41
+ \`query-loop\` is create-only. Use \`wp-typia create <project-dir> --template query-loop\`; \`wp-typia add block\` accepts only basic, interactivity, persistence, and compound families.
41
42
  \`add variation\` uses an existing workspace block from \`scripts/block-config.ts\`.
42
43
  \`add pattern\` scaffolds a namespaced PHP pattern shell under \`src/patterns/\`.
43
44
  \`add binding-source\` scaffolds shared PHP and editor registration under \`src/bindings/\`.
@@ -26,12 +26,14 @@ export interface ScaffoldDryRunPlan {
26
26
  }
27
27
  interface RunScaffoldFlowOptions {
28
28
  allowExistingDir?: boolean;
29
+ alternateRenderTargets?: string;
29
30
  cwd?: string;
30
31
  dataStorageMode?: string;
31
32
  dryRun?: boolean;
32
33
  externalLayerId?: string;
33
34
  externalLayerSource?: string;
34
35
  installDependencies?: Parameters<typeof scaffoldProject>[0]["installDependencies"];
36
+ innerBlocksPreset?: string;
35
37
  isInteractive?: boolean;
36
38
  namespace?: string;
37
39
  noInstall?: boolean;
@@ -80,7 +82,7 @@ export declare function getOptionalOnboarding({ availableScripts, packageManager
80
82
  * project.
81
83
  * @returns The scaffold result together with next-step guidance.
82
84
  */
83
- export declare function runScaffoldFlow({ projectInput, cwd, templateId, dataStorageMode, dryRun, externalLayerId, externalLayerSource, persistencePolicy, packageManager, namespace, textDomain, phpPrefix, queryPostType, yes, noInstall, onProgress, isInteractive, allowExistingDir, selectTemplate, selectDataStorage, selectExternalLayerId, selectPersistencePolicy, selectPackageManager, promptText, installDependencies, variant, selectWithTestPreset, selectWithWpEnv, selectWithMigrationUi, withMigrationUi, withTestPreset, withWpEnv, }: RunScaffoldFlowOptions): Promise<{
85
+ export declare function runScaffoldFlow({ projectInput, cwd, templateId, alternateRenderTargets, dataStorageMode, dryRun, externalLayerId, externalLayerSource, innerBlocksPreset, persistencePolicy, packageManager, namespace, textDomain, phpPrefix, queryPostType, yes, noInstall, onProgress, isInteractive, allowExistingDir, selectTemplate, selectDataStorage, selectExternalLayerId, selectPersistencePolicy, selectPackageManager, promptText, installDependencies, variant, selectWithTestPreset, selectWithWpEnv, selectWithMigrationUi, withMigrationUi, withTestPreset, withWpEnv, }: RunScaffoldFlowOptions): Promise<{
84
86
  dryRun: boolean;
85
87
  optionalOnboarding: OptionalOnboardingGuidance;
86
88
  plan: ScaffoldDryRunPlan | undefined;
@@ -3,12 +3,15 @@ import { promises as fsp } from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { collectScaffoldAnswers, DATA_STORAGE_MODES, PERSISTENCE_POLICIES, isDataStorageMode, isPersistencePolicy, resolvePackageManagerId, resolveTemplateId, scaffoldProject, } from "./scaffold.js";
6
+ import { parseAlternateRenderTargets } from "./alternate-render-targets.js";
7
+ import { parseCompoundInnerBlocksPreset } from "./compound-inner-blocks.js";
6
8
  import { formatInstallCommand, formatRunScript, } from "./package-managers.js";
7
9
  import { getPrimaryDevelopmentScript } from "./local-dev-presets.js";
8
10
  import { getOptionalOnboardingNote, getOptionalOnboardingSteps, } from "./scaffold-onboarding.js";
9
11
  import { formatNonEmptyTargetDirectoryError } from "./scaffold-bootstrap.js";
10
12
  import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, isBuiltInTemplateId, } from "./template-registry.js";
11
13
  import { resolveOptionalInteractiveExternalLayerId, } from "./external-layer-selection.js";
14
+ import { assertBuiltInTemplateVariantAllowed, resolveLocalCliPathOption, normalizeOptionalCliString, } from "./cli-validation.js";
12
15
  async function listRelativeProjectFiles(rootDir) {
13
16
  const relativeFiles = [];
14
17
  async function visit(currentDir) {
@@ -36,13 +39,14 @@ async function assertDryRunTargetDirectoryReady(projectDir, allowExistingDir) {
36
39
  throw new Error(formatNonEmptyTargetDirectoryError(projectDir));
37
40
  }
38
41
  }
39
- async function buildScaffoldDryRunPlan({ allowExistingDir, answers, cwd, dataStorageMode, externalLayerId, externalLayerSource, externalLayerSourceLabel, installDependencies, noInstall, onProgress, packageManager, persistencePolicy, projectDir, templateId, variant, withMigrationUi, withTestPreset, withWpEnv, }) {
42
+ async function buildScaffoldDryRunPlan({ allowExistingDir, alternateRenderTargets, answers, cwd, dataStorageMode, externalLayerId, externalLayerSource, externalLayerSourceLabel, installDependencies, noInstall, onProgress, packageManager, persistencePolicy, projectDir, templateId, variant, withMigrationUi, withTestPreset, withWpEnv, }) {
40
43
  await assertDryRunTargetDirectoryReady(projectDir, allowExistingDir);
41
44
  const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), "wp-typia-scaffold-plan-"));
42
45
  const previewProjectDir = path.join(tempRoot, "preview-project");
43
46
  try {
44
47
  const result = await scaffoldProject({
45
48
  allowExistingDir: false,
49
+ alternateRenderTargets,
46
50
  answers,
47
51
  cwd,
48
52
  dataStorageMode,
@@ -109,14 +113,74 @@ function templateUsesPersistenceSettings(templateId, options) {
109
113
  function templateSupportsPersistenceFlags(templateId) {
110
114
  return templateId === "persistence" || templateId === "compound";
111
115
  }
116
+ function templateSupportsCompoundInnerBlocksPreset(templateId) {
117
+ return templateId === "compound";
118
+ }
119
+ function createTemplateLabel(templateId) {
120
+ return templateId === OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE
121
+ ? "`--template workspace`"
122
+ : `"${templateId}"`;
123
+ }
124
+ function collectTemplateCapabilityWarnings(options) {
125
+ const warnings = [];
126
+ const trimmedQueryPostType = options.queryPostType?.trim();
127
+ if (trimmedQueryPostType &&
128
+ options.templateId !== "query-loop" &&
129
+ (isBuiltInTemplateId(options.templateId) ||
130
+ options.templateId === OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE)) {
131
+ warnings.push(`\`--query-post-type\` only applies to \`wp-typia create --template query-loop\`, which scaffolds a create-time \`core/query\` variation instead of a standalone block. ${createTemplateLabel(options.templateId)} will ignore "${trimmedQueryPostType}".`);
132
+ }
133
+ if (options.withMigrationUi === true &&
134
+ !isBuiltInTemplateId(options.templateId) &&
135
+ options.templateId !== OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE) {
136
+ warnings.push(`\`--with-migration-ui\` was ignored for ${createTemplateLabel(options.templateId)}. Migration UI currently scaffolds built-in templates and the official \`--template workspace\` flow; external templates still need to opt into that surface explicitly.`);
137
+ }
138
+ return warnings;
139
+ }
140
+ function templateSupportsAlternateRenderTargets(options) {
141
+ if (!options.alternateRenderTargets) {
142
+ return false;
143
+ }
144
+ if (options.templateId === "persistence") {
145
+ return true;
146
+ }
147
+ if (options.templateId !== "compound") {
148
+ return false;
149
+ }
150
+ return templateUsesPersistenceSettings(options.templateId, {
151
+ dataStorageMode: options.dataStorageMode,
152
+ persistencePolicy: options.persistencePolicy,
153
+ });
154
+ }
112
155
  function validateCreateFlagContract(options) {
113
- const { dataStorageMode, persistencePolicy, templateId, variant } = options;
156
+ const { alternateRenderTargets, dataStorageMode, innerBlocksPreset, persistencePolicy, templateId, variant, } = options;
114
157
  if ((dataStorageMode || persistencePolicy) &&
115
158
  !templateSupportsPersistenceFlags(templateId)) {
116
159
  throw new Error("`--data-storage` and `--persistence-policy` are supported only for `wp-typia create --template persistence` or `--template compound`.");
117
160
  }
118
- if (variant && isBuiltInTemplateId(templateId)) {
119
- throw new Error(`--variant is only supported for official external template configs. Received variant "${variant}" for built-in template "${templateId}".`);
161
+ if (alternateRenderTargets &&
162
+ !templateSupportsAlternateRenderTargets({
163
+ alternateRenderTargets,
164
+ dataStorageMode,
165
+ persistencePolicy,
166
+ templateId,
167
+ })) {
168
+ if (templateId === "compound") {
169
+ throw new Error("`--alternate-render-targets` on `wp-typia create --template compound` requires the persistence-enabled server render path. Add `--data-storage <post-meta|custom-table>` or `--persistence-policy <authenticated|public>` first.");
170
+ }
171
+ throw new Error("`--alternate-render-targets` is supported only for `wp-typia create --template persistence` or persistence-enabled `--template compound` scaffolds.");
172
+ }
173
+ parseAlternateRenderTargets(alternateRenderTargets);
174
+ if (innerBlocksPreset &&
175
+ !templateSupportsCompoundInnerBlocksPreset(templateId)) {
176
+ throw new Error("`--inner-blocks-preset` is supported only for `wp-typia create --template compound`.");
177
+ }
178
+ parseCompoundInnerBlocksPreset(innerBlocksPreset);
179
+ if (isBuiltInTemplateId(templateId)) {
180
+ assertBuiltInTemplateVariantAllowed({
181
+ templateId,
182
+ variant,
183
+ });
120
184
  }
121
185
  }
122
186
  function parseSelectableValue(label, value, isValue, allowedValues) {
@@ -203,14 +267,13 @@ export function getOptionalOnboarding({ availableScripts, packageManager, templa
203
267
  * project.
204
268
  * @returns The scaffold result together with next-step guidance.
205
269
  */
206
- export async function runScaffoldFlow({ projectInput, cwd = process.cwd(), templateId, dataStorageMode, dryRun = false, externalLayerId, externalLayerSource, persistencePolicy, packageManager, namespace, textDomain, phpPrefix, queryPostType, yes = false, noInstall = false, onProgress, isInteractive = false, allowExistingDir = false, selectTemplate, selectDataStorage, selectExternalLayerId, selectPersistencePolicy, selectPackageManager, promptText, installDependencies = undefined, variant, selectWithTestPreset, selectWithWpEnv, selectWithMigrationUi, withMigrationUi, withTestPreset, withWpEnv, }) {
207
- const normalizedExternalLayerId = typeof externalLayerId === "string" && externalLayerId.trim().length > 0
208
- ? externalLayerId.trim()
209
- : undefined;
210
- const normalizedExternalLayerSource = typeof externalLayerSource === "string" &&
211
- externalLayerSource.trim().length > 0
212
- ? externalLayerSource.trim()
213
- : undefined;
270
+ export async function runScaffoldFlow({ projectInput, cwd = process.cwd(), templateId, alternateRenderTargets, dataStorageMode, dryRun = false, externalLayerId, externalLayerSource, innerBlocksPreset, persistencePolicy, packageManager, namespace, textDomain, phpPrefix, queryPostType, yes = false, noInstall = false, onProgress, isInteractive = false, allowExistingDir = false, selectTemplate, selectDataStorage, selectExternalLayerId, selectPersistencePolicy, selectPackageManager, promptText, installDependencies = undefined, variant, selectWithTestPreset, selectWithWpEnv, selectWithMigrationUi, withMigrationUi, withTestPreset, withWpEnv, }) {
271
+ const normalizedExternalLayerId = normalizeOptionalCliString(externalLayerId);
272
+ const normalizedExternalLayerSource = resolveLocalCliPathOption({
273
+ cwd,
274
+ label: "--external-layer-source",
275
+ value: externalLayerSource,
276
+ });
214
277
  validateCreateProjectInput(projectInput);
215
278
  const resolvedTemplateId = await resolveTemplateId({
216
279
  templateId,
@@ -219,11 +282,14 @@ export async function runScaffoldFlow({ projectInput, cwd = process.cwd(), templ
219
282
  selectTemplate,
220
283
  });
221
284
  validateCreateFlagContract({
285
+ alternateRenderTargets,
222
286
  dataStorageMode,
287
+ innerBlocksPreset,
223
288
  persistencePolicy,
224
289
  templateId: resolvedTemplateId,
225
290
  variant,
226
291
  });
292
+ const resolvedInnerBlocksPreset = parseCompoundInnerBlocksPreset(innerBlocksPreset);
227
293
  const resolvedExternalLayerSelection = isBuiltInTemplateId(resolvedTemplateId) && isInteractive
228
294
  ? await resolveOptionalInteractiveExternalLayerId({
229
295
  callerCwd: cwd,
@@ -302,9 +368,13 @@ export async function runScaffoldFlow({ projectInput, cwd = process.cwd(), templ
302
368
  yes,
303
369
  promptText,
304
370
  });
371
+ if (resolvedTemplateId === "compound" && resolvedInnerBlocksPreset) {
372
+ answers.compoundInnerBlocksPreset = resolvedInnerBlocksPreset;
373
+ }
305
374
  const resolvedResult = dryRun
306
375
  ? await buildScaffoldDryRunPlan({
307
376
  allowExistingDir,
377
+ alternateRenderTargets,
308
378
  answers,
309
379
  cwd,
310
380
  dataStorageMode: resolvedDataStorage,
@@ -326,6 +396,7 @@ export async function runScaffoldFlow({ projectInput, cwd = process.cwd(), templ
326
396
  : {
327
397
  plan: undefined,
328
398
  result: await scaffoldProject({
399
+ alternateRenderTargets,
329
400
  answers,
330
401
  allowExistingDir,
331
402
  cwd,
@@ -386,6 +457,11 @@ export async function runScaffoldFlow({ projectInput, cwd = process.cwd(), templ
386
457
  ...resolvedResult.result,
387
458
  warnings: [
388
459
  ...resolvedResult.result.warnings,
460
+ ...collectTemplateCapabilityWarnings({
461
+ queryPostType,
462
+ templateId: resolvedTemplateId,
463
+ withMigrationUi,
464
+ }),
389
465
  ...collectProjectDirectoryWarnings(projectDir),
390
466
  ],
391
467
  },
@@ -21,6 +21,10 @@ export function formatTemplateFeatures(template) {
21
21
  if (capabilityHints.length > 0) {
22
22
  lines.push(` Supports: ${capabilityHints.join(" • ")}`);
23
23
  }
24
+ const specialNotes = getTemplateSpecialNotes(template);
25
+ if (specialNotes.length > 0) {
26
+ lines.push(` Notes: ${specialNotes.join(" • ")}`);
27
+ }
24
28
  if (template.id === OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE) {
25
29
  lines.push(` Alias: ${WORKSPACE_TEMPLATE_ALIAS} (\`--template ${WORKSPACE_TEMPLATE_ALIAS}\`)`);
26
30
  }
@@ -51,6 +55,13 @@ export function formatTemplateDetails(template) {
51
55
  detailLines.push(` - ${capabilityHint}`);
52
56
  }
53
57
  }
58
+ const specialNotes = getTemplateSpecialNotes(template);
59
+ if (specialNotes.length > 0) {
60
+ detailLines.push("Notes:");
61
+ for (const specialNote of specialNotes) {
62
+ detailLines.push(` - ${specialNote}`);
63
+ }
64
+ }
54
65
  detailLines.push("Logical layers:");
55
66
  for (const logicalLayer of getTemplateLogicalLayerSummaries(template)) {
56
67
  detailLines.push(` - ${logicalLayer}`);
@@ -60,7 +71,12 @@ export function formatTemplateDetails(template) {
60
71
  }
61
72
  function getTemplateCapabilityHints(template) {
62
73
  if (template.id === "persistence" || template.id === "compound") {
63
- return ["--data-storage", "--persistence-policy", "external layers"];
74
+ return [
75
+ "--alternate-render-targets",
76
+ "--data-storage",
77
+ "--persistence-policy",
78
+ "external layers",
79
+ ];
64
80
  }
65
81
  if (template.id === "query-loop") {
66
82
  return ["--query-post-type", "external layers"];
@@ -70,6 +86,15 @@ function getTemplateCapabilityHints(template) {
70
86
  }
71
87
  return [];
72
88
  }
89
+ function getTemplateSpecialNotes(template) {
90
+ if (template.id === "query-loop") {
91
+ return [
92
+ "Create-time variation scaffold only; use `wp-typia create <project-dir> --template query-loop` instead of `wp-typia add block`.",
93
+ "Owns a `core/query` variation, so it does not generate `src/types.ts`, `block.json`, or Typia manifests.",
94
+ ];
95
+ }
96
+ return [];
97
+ }
73
98
  function getTemplateIdentityLines(template) {
74
99
  if (template.id === OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE) {
75
100
  return [
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Normalize one optional CLI string flag by trimming whitespace and collapsing
3
+ * empty strings to `undefined`.
4
+ *
5
+ * @param value Raw CLI value before normalization.
6
+ * @returns The trimmed string when present, otherwise `undefined`.
7
+ */
8
+ export declare function normalizeOptionalCliString(value?: string): string | undefined;
9
+ /**
10
+ * Resolve one CLI path flag relative to the caller when it is expressed as a
11
+ * local filesystem path.
12
+ *
13
+ * Non-local values such as npm package specs or `github:` locators pass
14
+ * through unchanged. Local relative and absolute paths are resolved against the
15
+ * provided `cwd` and must exist on disk.
16
+ *
17
+ * @param options Path resolution inputs for one CLI flag.
18
+ * @param options.cwd Caller working directory used for relative path
19
+ * resolution.
20
+ * @param options.label Human-readable option label used in thrown errors.
21
+ * @param options.value Raw CLI value before trimming and path resolution.
22
+ * @returns The normalized string, or `undefined` when the option was omitted.
23
+ * @throws When a local-looking path resolves to a missing filesystem entry.
24
+ */
25
+ export declare function resolveLocalCliPathOption(options: {
26
+ cwd: string;
27
+ label: string;
28
+ value?: string;
29
+ }): string | undefined;
30
+ /**
31
+ * Validate the built-in template composition rule for external layers.
32
+ *
33
+ * @param options External layer CLI options after normalization.
34
+ * @param options.externalLayerId Optional selected layer id.
35
+ * @param options.externalLayerSource Optional layer source locator or path.
36
+ * @throws When `externalLayerId` is provided without `externalLayerSource`.
37
+ */
38
+ export declare function assertExternalLayerCompositionOptions(options: {
39
+ externalLayerId?: string;
40
+ externalLayerSource?: string;
41
+ }): void;
42
+ /**
43
+ * Build the shared error message used when a built-in template receives a
44
+ * `--variant` override.
45
+ *
46
+ * @param options Built-in template context.
47
+ * @param options.templateId Built-in template id that rejected the variant.
48
+ * @param options.variant User-supplied variant override.
49
+ * @returns The canonical user-facing error message.
50
+ */
51
+ export declare function createBuiltInVariantErrorMessage(options: {
52
+ templateId: string;
53
+ variant: string;
54
+ }): string;
55
+ /**
56
+ * Reject unsupported `--variant` usage for built-in templates.
57
+ *
58
+ * @param options Built-in template validation context.
59
+ * @param options.templateId Built-in template id being scaffolded.
60
+ * @param options.variant Optional variant override from CLI flags.
61
+ * @throws When a built-in template receives any explicit `--variant` value.
62
+ */
63
+ export declare function assertBuiltInTemplateVariantAllowed(options: {
64
+ templateId: string;
65
+ variant?: string;
66
+ }): void;