@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
@@ -2,6 +2,7 @@ import { buildTemplateVariablesFromBlockSpec, createBuiltInBlockSpec, } from './
2
2
  import { getPackageVersions } from './package-versions.js';
3
3
  import { buildBlockCssClassName, buildFrontendCssClassName, resolveScaffoldIdentifiers, } from './scaffold-identifiers.js';
4
4
  import { BUILTIN_BLOCK_METADATA_VERSION, COMPOUND_CHILD_BLOCK_METADATA_DEFAULTS, getBuiltInTemplateMetadataDefaults, } from './template-defaults.js';
5
+ import { DEFAULT_COMPOUND_INNER_BLOCKS_PRESET_ID, getCompoundInnerBlocksPresetDefinition, } from './compound-inner-blocks.js';
5
6
  import { getTemplateById, isBuiltInTemplateId, } from './template-registry.js';
6
7
  import { toPascalCase, toSnakeCase, } from './string-case.js';
7
8
  /**
@@ -43,6 +44,8 @@ export function getTemplateVariables(templateId, answers) {
43
44
  const compoundChildTitle = `${title} Item`;
44
45
  const cssClassName = buildBlockCssClassName(namespace, slug);
45
46
  const compoundChildCssClassName = buildBlockCssClassName(namespace, `${slug}-item`);
47
+ const compoundInnerBlocksPreset = answers.compoundInnerBlocksPreset ?? DEFAULT_COMPOUND_INNER_BLOCKS_PRESET_ID;
48
+ const compoundInnerBlocksPresetDefinition = getCompoundInnerBlocksPresetDefinition(compoundInnerBlocksPreset);
46
49
  const compoundPersistenceEnabled = templateId === 'persistence'
47
50
  ? true
48
51
  : templateId === 'compound'
@@ -55,6 +58,8 @@ export function getTemplateVariables(templateId, answers) {
55
58
  ? answers.persistencePolicy ?? 'authenticated'
56
59
  : 'authenticated';
57
60
  return {
61
+ alternateRenderTargetsCsv: '',
62
+ alternateRenderTargetsJson: '[]',
58
63
  apiClientPackageVersion,
59
64
  author: answers.author.trim(),
60
65
  blockRuntimePackageVersion,
@@ -68,6 +73,23 @@ export function getTemplateVariables(templateId, answers) {
68
73
  compoundChildIcon: COMPOUND_CHILD_BLOCK_METADATA_DEFAULTS.icon,
69
74
  compoundChildTitleJson: JSON.stringify(compoundChildTitle),
70
75
  compoundPersistenceEnabled: compoundPersistenceEnabled ? 'true' : 'false',
76
+ compoundInnerBlocksDirectInsert: compoundInnerBlocksPresetDefinition.directInsert
77
+ ? 'true'
78
+ : 'false',
79
+ compoundInnerBlocksOrientation: compoundInnerBlocksPresetDefinition.orientation ?? '',
80
+ compoundInnerBlocksOrientationExpression: compoundInnerBlocksPresetDefinition.orientation
81
+ ? `'${compoundInnerBlocksPresetDefinition.orientation}'`
82
+ : 'undefined',
83
+ compoundInnerBlocksPreset,
84
+ compoundInnerBlocksPresetDescription: compoundInnerBlocksPresetDefinition.description,
85
+ compoundInnerBlocksPresetLabel: compoundInnerBlocksPresetDefinition.label,
86
+ compoundInnerBlocksTemplateLockExpression: compoundInnerBlocksPresetDefinition.templateLock === false
87
+ ? 'false'
88
+ : `'${compoundInnerBlocksPresetDefinition.templateLock}'`,
89
+ hasAlternateEmailRenderTarget: 'false',
90
+ hasAlternateMjmlRenderTarget: 'false',
91
+ hasAlternatePlainTextRenderTarget: 'false',
92
+ hasAlternateRenderTargets: 'false',
71
93
  projectToolsPackageVersion,
72
94
  cssClassName,
73
95
  dataStorageMode,
@@ -8,6 +8,7 @@ import type { PackageManagerId } from "./package-managers.js";
8
8
  */
9
9
  export interface ScaffoldAnswers {
10
10
  author: string;
11
+ compoundInnerBlocksPreset?: import("./compound-inner-blocks.js").CompoundInnerBlocksPresetId;
11
12
  dataStorageMode?: DataStorageMode;
12
13
  description: string;
13
14
  /** Block namespace used in generated block names such as `namespace/slug`. */
@@ -29,6 +30,8 @@ export type PersistencePolicy = (typeof PERSISTENCE_POLICIES)[number];
29
30
  * Normalized template variables shared by built-in and remote scaffold flows.
30
31
  */
31
32
  export interface ScaffoldTemplateVariables extends Record<string, string> {
33
+ alternateRenderTargetsCsv: string;
34
+ alternateRenderTargetsJson: string;
32
35
  apiClientPackageVersion: string;
33
36
  author: string;
34
37
  blockRuntimePackageVersion: string;
@@ -42,6 +45,17 @@ export interface ScaffoldTemplateVariables extends Record<string, string> {
42
45
  compoundChildIcon: string;
43
46
  compoundChildTitleJson: string;
44
47
  compoundPersistenceEnabled: "false" | "true";
48
+ compoundInnerBlocksDirectInsert: "false" | "true";
49
+ compoundInnerBlocksOrientation: "" | "horizontal" | "vertical";
50
+ compoundInnerBlocksOrientationExpression: string;
51
+ compoundInnerBlocksPreset: string;
52
+ compoundInnerBlocksPresetDescription: string;
53
+ compoundInnerBlocksPresetLabel: string;
54
+ compoundInnerBlocksTemplateLockExpression: string;
55
+ hasAlternateEmailRenderTarget: "false" | "true";
56
+ hasAlternateMjmlRenderTarget: "false" | "true";
57
+ hasAlternatePlainTextRenderTarget: "false" | "true";
58
+ hasAlternateRenderTargets: "false" | "true";
45
59
  projectToolsPackageVersion: string;
46
60
  cssClassName: string;
47
61
  dashCase: string;
@@ -124,8 +138,14 @@ interface InstallDependenciesOptions {
124
138
  packageManager: PackageManagerId;
125
139
  projectDir: string;
126
140
  }
141
+ export interface ScaffoldProgressEvent {
142
+ detail: string;
143
+ phase: "finalize-project" | "generate-files" | "install-dependencies" | "resolve-template" | "seed-artifacts";
144
+ title: string;
145
+ }
127
146
  interface ScaffoldProjectOptions {
128
147
  allowExistingDir?: boolean;
148
+ alternateRenderTargets?: string;
129
149
  answers: ScaffoldAnswers;
130
150
  cwd?: string;
131
151
  dataStorageMode?: DataStorageMode;
@@ -134,6 +154,7 @@ interface ScaffoldProjectOptions {
134
154
  externalLayerSourceLabel?: string;
135
155
  installDependencies?: ((options: InstallDependenciesOptions) => Promise<void>) | undefined;
136
156
  noInstall?: boolean;
157
+ onProgress?: ((event: ScaffoldProgressEvent) => void | Promise<void>) | undefined;
137
158
  packageManager: PackageManagerId;
138
159
  persistencePolicy?: PersistencePolicy;
139
160
  projectDir: string;
@@ -157,4 +178,4 @@ export { collectScaffoldAnswers, detectAuthor, getDefaultAnswers, resolvePackage
157
178
  export { getTemplateVariables } from "./scaffold-template-variables.js";
158
179
  export declare function isDataStorageMode(value: string): value is DataStorageMode;
159
180
  export declare function isPersistencePolicy(value: string): value is PersistencePolicy;
160
- export declare function scaffoldProject({ projectDir, templateId, answers, dataStorageMode, persistencePolicy, packageManager, externalLayerId, externalLayerSource, externalLayerSourceLabel, repositoryReference, cwd, allowExistingDir, noInstall, installDependencies, variant, withMigrationUi, withTestPreset, withWpEnv, }: ScaffoldProjectOptions): Promise<ScaffoldProjectResult>;
181
+ export declare function scaffoldProject({ projectDir, templateId, answers, alternateRenderTargets, dataStorageMode, persistencePolicy, packageManager, externalLayerId, externalLayerSource, externalLayerSourceLabel, repositoryReference, cwd, allowExistingDir, noInstall, installDependencies, onProgress, variant, withMigrationUi, withTestPreset, withWpEnv, }: ScaffoldProjectOptions): Promise<ScaffoldProjectResult>;
@@ -2,16 +2,17 @@ import fs from "node:fs";
2
2
  import { promises as fsp } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { getPackageManager } from "./package-managers.js";
5
- import { buildGitignore, buildReadme, mergeTextLines, replaceTextRecursively, } from "./scaffold-apply-utils.js";
5
+ import { buildGitignore, buildReadme, mergeTextLines, removeQueryLoopPlaceholderFiles, replaceTextRecursively, } from "./scaffold-apply-utils.js";
6
6
  import { applyGeneratedProjectDxPackageJson, applyLocalDevPresetFiles, } from "./local-dev-presets.js";
7
7
  import { applyMigrationUiCapability } from "./migration-ui-capability.js";
8
- import { applyWorkspaceMigrationCapability, ensureScaffoldDirectory, isOfficialWorkspaceProject, seedBuiltInPersistenceArtifacts, writeStarterManifestFiles, } from "./scaffold-bootstrap.js";
8
+ import { applyWorkspaceMigrationCapability, ensureScaffoldDirectory, isWorkspaceProject, seedBuiltInPersistenceArtifacts, writeStarterManifestFiles, } from "./scaffold-bootstrap.js";
9
9
  import { defaultInstallDependencies, normalizePackageJson, normalizePackageManagerFiles, removeUnexpectedLockfiles, } from "./scaffold-package-manager-files.js";
10
10
  import { copyInterpolatedDirectory } from "./template-render.js";
11
11
  import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, isBuiltInTemplateId, } from "./template-registry.js";
12
12
  import { resolveTemplateSource } from "./template-source.js";
13
13
  import { BlockGeneratorService, } from "./block-generator-service.js";
14
14
  import { getTemplateVariables } from "./scaffold-template-variables.js";
15
+ import { assertExternalLayerCompositionOptions, } from "./cli-validation.js";
15
16
  const WORKSPACE_TEMPLATE_ALIAS = "workspace";
16
17
  export const DATA_STORAGE_MODES = ["post-meta", "custom-table"];
17
18
  export const PERSISTENCE_POLICIES = ["authenticated", "public"];
@@ -29,17 +30,27 @@ function normalizeTemplateSelection(templateId) {
29
30
  ? OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE
30
31
  : templateId;
31
32
  }
32
- export async function scaffoldProject({ projectDir, templateId, answers, dataStorageMode, persistencePolicy, packageManager, externalLayerId, externalLayerSource, externalLayerSourceLabel, repositoryReference, cwd = process.cwd(), allowExistingDir = false, noInstall = false, installDependencies = undefined, variant, withMigrationUi = false, withTestPreset = false, withWpEnv = false, }) {
33
+ async function reportScaffoldProgress(onProgress, event) {
34
+ await onProgress?.(event);
35
+ }
36
+ export async function scaffoldProject({ projectDir, templateId, answers, alternateRenderTargets, dataStorageMode, persistencePolicy, packageManager, externalLayerId, externalLayerSource, externalLayerSourceLabel, repositoryReference, cwd = process.cwd(), allowExistingDir = false, noInstall = false, installDependencies = undefined, onProgress = undefined, variant, withMigrationUi = false, withTestPreset = false, withWpEnv = false, }) {
33
37
  const resolvedTemplateId = normalizeTemplateSelection(templateId);
34
38
  const resolvedPackageManager = getPackageManager(packageManager).id;
35
39
  const isBuiltInTemplate = isBuiltInTemplateId(resolvedTemplateId);
36
- if (externalLayerId && !externalLayerSource) {
37
- throw new Error("externalLayerId requires externalLayerSource when composing built-in template layers.");
38
- }
40
+ assertExternalLayerCompositionOptions({
41
+ externalLayerId,
42
+ externalLayerSource,
43
+ });
39
44
  if (isBuiltInTemplate) {
40
45
  const blockGeneratorService = new BlockGeneratorService();
46
+ await reportScaffoldProgress(onProgress, {
47
+ detail: "Preparing template layers, variants, and generated artifact plans.",
48
+ phase: "resolve-template",
49
+ title: "Resolving scaffold template",
50
+ });
41
51
  const plan = await blockGeneratorService.plan({
42
52
  allowExistingDir,
53
+ alternateRenderTargets,
43
54
  answers,
44
55
  cwd,
45
56
  dataStorageMode: dataStorageMode ?? answers.dataStorageMode,
@@ -61,6 +72,7 @@ export async function scaffoldProject({ projectDir, templateId, answers, dataSto
61
72
  const rendered = await blockGeneratorService.render({ validated });
62
73
  return blockGeneratorService.apply({
63
74
  installDependencies,
75
+ onProgress,
64
76
  rendered,
65
77
  });
66
78
  }
@@ -72,13 +84,25 @@ export async function scaffoldProject({ projectDir, templateId, answers, dataSto
72
84
  dataStorageMode: dataStorageMode ?? answers.dataStorageMode,
73
85
  persistencePolicy: persistencePolicy ?? answers.persistencePolicy,
74
86
  });
87
+ await reportScaffoldProgress(onProgress, {
88
+ detail: "Loading template files, variants, and external package metadata when needed.",
89
+ phase: "resolve-template",
90
+ title: "Resolving scaffold template",
91
+ });
75
92
  const templateSource = await resolveTemplateSource(resolvedTemplateId, cwd, variables, variant);
76
- const supportsMigrationUi = isBuiltInTemplate || templateSource.isOfficialWorkspaceTemplate === true;
93
+ const supportsMigrationUi = isBuiltInTemplate ||
94
+ templateSource.isOfficialWorkspaceTemplate === true ||
95
+ templateSource.supportsMigrationUi === true;
77
96
  if (withMigrationUi && !supportsMigrationUi) {
78
97
  await templateSource.cleanup?.();
79
- throw new Error("`--with-migration-ui` is currently supported only for built-in templates and @wp-typia/create-workspace-template.");
98
+ throw new Error("`--with-migration-ui` is currently supported only for built-in templates and workspace-capable wp-typia templates.");
80
99
  }
81
100
  try {
101
+ await reportScaffoldProgress(onProgress, {
102
+ detail: "Copying scaffold files into the target project directory.",
103
+ phase: "generate-files",
104
+ title: "Generating project files",
105
+ });
82
106
  await ensureScaffoldDirectory(projectDir, allowExistingDir);
83
107
  await copyInterpolatedDirectory(templateSource.templateDir, projectDir, variables);
84
108
  }
@@ -87,8 +111,13 @@ export async function scaffoldProject({ projectDir, templateId, answers, dataSto
87
111
  await templateSource.cleanup();
88
112
  }
89
113
  }
90
- const isOfficialWorkspace = isOfficialWorkspaceProject(projectDir);
114
+ const isWorkspace = isWorkspaceProject(projectDir);
91
115
  if (isBuiltInTemplate) {
116
+ await reportScaffoldProgress(onProgress, {
117
+ detail: "Writing starter manifests, local presets, and template-specific generated artifacts.",
118
+ phase: "seed-artifacts",
119
+ title: "Seeding scaffold artifacts",
120
+ });
92
121
  await writeStarterManifestFiles(projectDir, resolvedTemplateId, variables);
93
122
  await seedBuiltInPersistenceArtifacts(projectDir, resolvedTemplateId, variables);
94
123
  await applyLocalDevPresetFiles({
@@ -105,14 +134,25 @@ export async function scaffoldProject({ projectDir, templateId, answers, dataSto
105
134
  variables,
106
135
  });
107
136
  }
137
+ await removeQueryLoopPlaceholderFiles(projectDir, resolvedTemplateId);
108
138
  }
109
- else if (withMigrationUi && isOfficialWorkspace) {
139
+ else if (withMigrationUi && isWorkspace) {
140
+ await reportScaffoldProgress(onProgress, {
141
+ detail: "Initializing workspace migration scripts and starter migration files.",
142
+ phase: "seed-artifacts",
143
+ title: "Seeding scaffold artifacts",
144
+ });
110
145
  await applyWorkspaceMigrationCapability(projectDir, resolvedPackageManager);
111
146
  }
147
+ await reportScaffoldProgress(onProgress, {
148
+ detail: "Writing README, normalizing package metadata, and aligning package-manager files.",
149
+ phase: "finalize-project",
150
+ title: "Finalizing scaffold output",
151
+ });
112
152
  const readmePath = path.join(projectDir, "README.md");
113
153
  if (!fs.existsSync(readmePath)) {
114
154
  await fsp.writeFile(readmePath, buildReadme(resolvedTemplateId, variables, resolvedPackageManager, {
115
- withMigrationUi: isBuiltInTemplate || isOfficialWorkspace ? withMigrationUi : false,
155
+ withMigrationUi: isBuiltInTemplate || isWorkspace ? withMigrationUi : false,
116
156
  withTestPreset: isBuiltInTemplate ? withTestPreset : false,
117
157
  withWpEnv: isBuiltInTemplate ? withWpEnv : false,
118
158
  }), "utf8");
@@ -139,6 +179,11 @@ export async function scaffoldProject({ projectDir, templateId, answers, dataSto
139
179
  repositoryReference,
140
180
  });
141
181
  if (!noInstall) {
182
+ await reportScaffoldProgress(onProgress, {
183
+ detail: "Installing project dependencies with the selected package manager.",
184
+ phase: "install-dependencies",
185
+ title: "Installing dependencies",
186
+ });
142
187
  const installer = installDependencies ?? defaultInstallDependencies;
143
188
  await installer({
144
189
  projectDir,
@@ -13,8 +13,9 @@ export interface CopyRawDirectoryOptions {
13
13
  *
14
14
  * Throwing or rejecting aborts the copy and bubbles to the caller.
15
15
  */
16
- filter?: (sourcePath: string, targetPath: string, entry: fs.Dirent) => boolean | Promise<boolean>;
16
+ filter?: CopyDirectoryFilter;
17
17
  }
18
+ export type CopyDirectoryFilter = (sourcePath: string, destinationPath: string, entry: fs.Dirent) => boolean | Promise<boolean>;
18
19
  /**
19
20
  * Render a Mustache template with full Mustache semantics while leaving values
20
21
  * unescaped for scaffold source generation.
@@ -28,7 +29,9 @@ export declare function copyRawDirectory(sourceDir: string, targetDir: string, o
28
29
  * Copy a template directory using full Mustache semantics for filenames and
29
30
  * text-file contents while leaving rendered values unescaped.
30
31
  */
31
- export declare function copyRenderedDirectory(sourceDir: string, targetDir: string, view: TemplateRenderView): Promise<void>;
32
+ export declare function copyRenderedDirectory(sourceDir: string, targetDir: string, view: TemplateRenderView, options?: {
33
+ filter?: CopyDirectoryFilter;
34
+ }): Promise<void>;
32
35
  /**
33
36
  * Copy a template directory using direct `{{key}}` replacement only.
34
37
  *
@@ -66,15 +66,19 @@ function stripTemplateExtension(entryName) {
66
66
  function renderTemplateDestinationName(entryName, view, renderString) {
67
67
  return renderString(stripTemplateExtension(entryName), view);
68
68
  }
69
- async function traverseTemplateDirectory({ prepareDirectory, renderString, sourceDir, targetDir, view, visitFile, }) {
69
+ async function traverseTemplateDirectory({ filter, prepareDirectory, renderString, sourceDir, targetDir, view, visitFile, }) {
70
70
  const entries = await fsp.readdir(sourceDir, { withFileTypes: true });
71
71
  for (const entry of entries) {
72
72
  const sourcePath = path.join(sourceDir, entry.name);
73
73
  const destinationName = renderTemplateDestinationName(entry.name, view, renderString);
74
74
  const destinationPath = resolveRenderedPath(targetDir, destinationName);
75
+ if (filter && !(await filter(sourcePath, destinationPath, entry))) {
76
+ continue;
77
+ }
75
78
  if (entry.isDirectory()) {
76
79
  await prepareDirectory?.(destinationPath);
77
80
  await traverseTemplateDirectory({
81
+ filter,
78
82
  prepareDirectory,
79
83
  renderString,
80
84
  sourceDir: sourcePath,
@@ -90,9 +94,10 @@ async function traverseTemplateDirectory({ prepareDirectory, renderString, sourc
90
94
  });
91
95
  }
92
96
  }
93
- async function copyTemplateDirectory({ renderString, sourceDir, targetDir, view, }) {
97
+ async function copyTemplateDirectory({ filter, renderString, sourceDir, targetDir, view, }) {
94
98
  await fsp.mkdir(targetDir, { recursive: true });
95
99
  await traverseTemplateDirectory({
100
+ filter,
96
101
  prepareDirectory: async (directoryPath) => {
97
102
  await fsp.mkdir(directoryPath, { recursive: true });
98
103
  },
@@ -134,8 +139,9 @@ export async function copyRawDirectory(sourceDir, targetDir, options = {}) {
134
139
  * Copy a template directory using full Mustache semantics for filenames and
135
140
  * text-file contents while leaving rendered values unescaped.
136
141
  */
137
- export async function copyRenderedDirectory(sourceDir, targetDir, view) {
142
+ export async function copyRenderedDirectory(sourceDir, targetDir, view, options = {}) {
138
143
  await copyTemplateDirectory({
144
+ filter: options.filter,
139
145
  renderString: renderMustacheTemplateString,
140
146
  sourceDir,
141
147
  targetDir,
@@ -35,6 +35,11 @@ export interface ResolvedTemplateSource {
35
35
  features: string[];
36
36
  format: TemplateSourceFormat;
37
37
  isOfficialWorkspaceTemplate?: boolean;
38
+ /**
39
+ * True when the resolved template can participate in scaffold migration UI
40
+ * seeding, such as rendered wp-typia workspace templates.
41
+ */
42
+ supportsMigrationUi?: boolean;
38
43
  templateDir: string;
39
44
  cleanup?: () => Promise<void>;
40
45
  selectedVariant?: string | null;
@@ -58,6 +63,11 @@ export interface ExternalTemplateConfig<TView extends UnknownRecord = TemplateVa
58
63
  blockTemplatesPath?: string;
59
64
  defaultValues?: Partial<TView>;
60
65
  folderName?: string;
66
+ /**
67
+ * Relative template root that renders a fuller wp-typia plugin/workspace
68
+ * scaffold instead of a create-block subset.
69
+ */
70
+ pluginTemplatesPath?: string;
61
71
  transformer?: (view: TView) => UnknownRecord | Promise<UnknownRecord>;
62
72
  variants?: Record<string, Partial<TView>>;
63
73
  }
@@ -65,6 +75,7 @@ export interface SeedSource {
65
75
  assetsDir?: string;
66
76
  blockDir: string;
67
77
  cleanup?: () => Promise<void>;
78
+ formatHint?: 'create-block-subset' | 'wp-typia';
68
79
  rootDir: string;
69
80
  selectedVariant?: string | null;
70
81
  warnings?: string[];
@@ -11,7 +11,7 @@ export declare const EXTERNAL_TEMPLATE_ENTRY_CANDIDATES: readonly ["index.js", "
11
11
  */
12
12
  export declare function getExternalTemplateEntry(sourceDir: string): string | null;
13
13
  /**
14
- * Load an official external create-block template config and render its seed.
14
+ * Load an official external template config and render its seed.
15
15
  *
16
16
  * @param sourceDir Source directory that contains the external template config.
17
17
  * @param context Template render context used for the selected variant.
@@ -55,7 +55,6 @@ async function loadExternalTemplateConfig(sourceDir) {
55
55
  }
56
56
  const warnings = [];
57
57
  for (const ignoredKey of [
58
- 'pluginTemplatesPath',
59
58
  'wpScripts',
60
59
  'wpEnv',
61
60
  'customScripts',
@@ -105,9 +104,42 @@ function extractVariantRenderValues(variantConfig) {
105
104
  delete values.assetsPath;
106
105
  delete values.blockTemplatesPath;
107
106
  delete values.folderName;
107
+ delete values.pluginTemplatesPath;
108
108
  delete values.transformer;
109
109
  return values;
110
110
  }
111
+ function resolveConfiguredTemplatePath(config, variantConfig) {
112
+ const variantPluginTemplatesPath = typeof variantConfig.pluginTemplatesPath === 'string'
113
+ ? variantConfig.pluginTemplatesPath
114
+ : null;
115
+ const variantBlockTemplatesPath = typeof variantConfig.blockTemplatesPath === 'string'
116
+ ? variantConfig.blockTemplatesPath
117
+ : null;
118
+ const configBlockTemplatesPath = typeof config.blockTemplatesPath === 'string'
119
+ ? config.blockTemplatesPath
120
+ : null;
121
+ const configPluginTemplatesPath = typeof config.pluginTemplatesPath === 'string'
122
+ ? config.pluginTemplatesPath
123
+ : null;
124
+ const templatePath = variantPluginTemplatesPath ??
125
+ variantBlockTemplatesPath ??
126
+ configBlockTemplatesPath ??
127
+ configPluginTemplatesPath;
128
+ if (!templatePath) {
129
+ throw new Error('External template config must define blockTemplatesPath or pluginTemplatesPath.');
130
+ }
131
+ const configuredFolderName = (typeof variantConfig.folderName === 'string'
132
+ ? variantConfig.folderName
133
+ : config.folderName) ?? null;
134
+ return {
135
+ folderName: configuredFolderName || '.',
136
+ formatHint: templatePath === variantPluginTemplatesPath ||
137
+ templatePath === configPluginTemplatesPath
138
+ ? 'wp-typia'
139
+ : 'create-block-subset',
140
+ templatePath,
141
+ };
142
+ }
111
143
  async function buildExternalTemplateView(context, config, selectedVariant, variantConfig) {
112
144
  const mergedView = {
113
145
  ...(config.defaultValues ?? {}),
@@ -131,7 +163,7 @@ async function buildExternalTemplateView(context, config, selectedVariant, varia
131
163
  };
132
164
  }
133
165
  /**
134
- * Load an official external create-block template config and render its seed.
166
+ * Load an official external template config and render its seed.
135
167
  *
136
168
  * @param sourceDir Source directory that contains the external template config.
137
169
  * @param context Template render context used for the selected variant.
@@ -141,25 +173,24 @@ async function buildExternalTemplateView(context, config, selectedVariant, varia
141
173
  export async function renderCreateBlockExternalTemplate(sourceDir, context, requestedVariant) {
142
174
  const { config, warnings } = await loadExternalTemplateConfig(sourceDir);
143
175
  const { selectedVariant, variantConfig } = getVariantConfig(config, requestedVariant);
144
- const blockTemplatesPath = (typeof variantConfig.blockTemplatesPath === 'string'
145
- ? variantConfig.blockTemplatesPath
146
- : config.blockTemplatesPath) ?? null;
147
- if (!blockTemplatesPath) {
148
- throw new Error('External template config must define blockTemplatesPath.');
149
- }
176
+ const { folderName, formatHint, templatePath } = resolveConfiguredTemplatePath(config, variantConfig);
150
177
  const tempRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'wp-typia-create-block-external-'));
151
178
  const cleanup = async () => {
152
179
  await fsp.rm(tempRoot, { force: true, recursive: true });
153
180
  };
154
181
  try {
155
182
  const renderedRoot = path.join(tempRoot, 'rendered');
156
- const folderName = (typeof variantConfig.folderName === 'string'
157
- ? variantConfig.folderName
158
- : config.folderName) || '.';
159
183
  const blockDir = resolveSourceSubpath(renderedRoot, folderName);
160
184
  const view = await buildExternalTemplateView(context, config, selectedVariant, variantConfig);
161
- const blockTemplateDir = resolveSourceSubpath(sourceDir, blockTemplatesPath);
162
- await copyRenderedDirectory(blockTemplateDir, blockDir, view);
185
+ const blockTemplateDir = resolveSourceSubpath(sourceDir, templatePath);
186
+ await copyRenderedDirectory(blockTemplateDir, blockDir, view, {
187
+ filter: (sourcePath, _destinationPath, entry) => {
188
+ const mustacheVariantPath = path.join(path.dirname(sourcePath), `${entry.name}.mustache`);
189
+ return !(entry.isFile() &&
190
+ (entry.name === 'package.json' || entry.name === 'README.md') &&
191
+ fs.existsSync(mustacheVariantPath));
192
+ },
193
+ });
163
194
  const assetsPath = typeof variantConfig.assetsPath === 'string'
164
195
  ? variantConfig.assetsPath
165
196
  : config.assetsPath;
@@ -172,6 +203,7 @@ export async function renderCreateBlockExternalTemplate(sourceDir, context, requ
172
203
  : undefined,
173
204
  blockDir,
174
205
  cleanup,
206
+ formatHint,
175
207
  rootDir: tempRoot,
176
208
  selectedVariant,
177
209
  warnings,
@@ -1,5 +1,5 @@
1
1
  export { renderCreateBlockExternalTemplate } from './template-source-external.js';
2
- export { getDefaultCategory, normalizeCreateBlockSubset, normalizeWpTypiaTemplateSeed, } from './template-source-remote.js';
2
+ export { getTemplateProjectType, getDefaultCategory, normalizeCreateBlockSubset, normalizeWpTypiaTemplateSeed, } from './template-source-remote.js';
3
3
  import type { TemplateSourceFormat, TemplateVariableContext } from './template-source-contracts.js';
4
4
  export declare function getTemplateVariableContext(variables: {
5
5
  [key: string]: string;
@@ -4,8 +4,9 @@ import path from 'node:path';
4
4
  import { loadExternalTemplateLayerManifest } from './template-layers.js';
5
5
  import { getPackageVersions } from './package-versions.js';
6
6
  import { getExternalTemplateEntry } from './template-source-external.js';
7
+ import { getTemplateProjectType } from './template-source-remote.js';
7
8
  export { renderCreateBlockExternalTemplate } from './template-source-external.js';
8
- export { getDefaultCategory, normalizeCreateBlockSubset, normalizeWpTypiaTemplateSeed, } from './template-source-remote.js';
9
+ export { getTemplateProjectType, getDefaultCategory, normalizeCreateBlockSubset, normalizeWpTypiaTemplateSeed, } from './template-source-remote.js';
9
10
  export function getTemplateVariableContext(variables) {
10
11
  const { apiClientPackageVersion, blockRuntimePackageVersion, blockTypesPackageVersion, projectToolsPackageVersion, restPackageVersion, } = getPackageVersions();
11
12
  return {
@@ -35,6 +36,9 @@ export async function detectTemplateSourceFormat(sourceDir) {
35
36
  if (getExternalTemplateEntry(sourceDir)) {
36
37
  return 'create-block-external';
37
38
  }
39
+ if (getTemplateProjectType(sourceDir) !== null) {
40
+ return 'wp-typia';
41
+ }
38
42
  const sourceRoot = fs.existsSync(path.join(sourceDir, 'src'))
39
43
  ? path.join(sourceDir, 'src')
40
44
  : sourceDir;
@@ -6,6 +6,11 @@ import type { ResolvedTemplateSource, SeedSource, TemplateVariableContext } from
6
6
  * @returns The declared block category, or "widgets" when detection fails.
7
7
  */
8
8
  export declare function getDefaultCategory(sourceDir: string): string;
9
+ /**
10
+ * Read `wpTypia.projectType` from a rendered or source template package
11
+ * manifest and return it when present.
12
+ */
13
+ export declare function getTemplateProjectType(sourceDir: string): string | null;
9
14
  /**
10
15
  * Copy a wp-typia seed into a normalized temporary template directory.
11
16
  *
@@ -39,6 +39,33 @@ export function getDefaultCategory(sourceDir) {
39
39
  return 'widgets';
40
40
  }
41
41
  }
42
+ function readTemplatePackageJson(sourceDir) {
43
+ for (const candidate of [
44
+ path.join(sourceDir, 'package.json.mustache'),
45
+ path.join(sourceDir, 'package.json'),
46
+ ]) {
47
+ if (!fs.existsSync(candidate)) {
48
+ continue;
49
+ }
50
+ try {
51
+ return JSON.parse(fs.readFileSync(candidate, 'utf8'));
52
+ }
53
+ catch {
54
+ continue;
55
+ }
56
+ }
57
+ return null;
58
+ }
59
+ /**
60
+ * Read `wpTypia.projectType` from a rendered or source template package
61
+ * manifest and return it when present.
62
+ */
63
+ export function getTemplateProjectType(sourceDir) {
64
+ const packageJson = readTemplatePackageJson(sourceDir);
65
+ return typeof packageJson?.wpTypia?.projectType === 'string'
66
+ ? packageJson.wpTypia.projectType
67
+ : null;
68
+ }
42
69
  /**
43
70
  * Copy a wp-typia seed into a normalized temporary template directory.
44
71
  *
@@ -57,6 +84,12 @@ export async function normalizeWpTypiaTemplateSeed(seed) {
57
84
  fs.existsSync(mustacheVariantPath));
58
85
  },
59
86
  });
87
+ if (seed.assetsDir && fs.existsSync(seed.assetsDir)) {
88
+ await fsp.cp(seed.assetsDir, path.join(normalizedDir, 'assets'), {
89
+ recursive: true,
90
+ force: true,
91
+ });
92
+ }
60
93
  }
61
94
  catch (error) {
62
95
  await fsp.rm(tempRoot, { force: true, recursive: true });
@@ -1,15 +1,17 @@
1
1
  import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, isBuiltInTemplateId, } from './template-registry.js';
2
2
  import { resolveBuiltInTemplateSource } from './template-builtins.js';
3
3
  import { parseTemplateLocator, } from './template-source-locators.js';
4
- import { detectTemplateSourceFormat, getDefaultCategory, getTemplateVariableContext, normalizeCreateBlockSubset, normalizeWpTypiaTemplateSeed, renderCreateBlockExternalTemplate, } from './template-source-normalization.js';
4
+ import { detectTemplateSourceFormat, getTemplateProjectType, getDefaultCategory, getTemplateVariableContext, normalizeCreateBlockSubset, normalizeWpTypiaTemplateSeed, renderCreateBlockExternalTemplate, } from './template-source-normalization.js';
5
5
  import { isOfficialWorkspaceTemplateSeed, resolveTemplateSeed, } from './template-source-seeds.js';
6
+ import { assertBuiltInTemplateVariantAllowed, } from './cli-validation.js';
6
7
  export { parseGitHubTemplateLocator, parseNpmTemplateLocator, parseTemplateLocator, } from './template-source-locators.js';
7
8
  export { resolveTemplateSeed } from './template-source-seeds.js';
8
9
  export async function resolveTemplateSource(templateId, cwd, variables, variant) {
9
10
  if (isBuiltInTemplateId(templateId)) {
10
- if (variant) {
11
- throw new Error(`--variant is only supported for official external template configs. Received variant "${variant}" for built-in template "${templateId}".`);
12
- }
11
+ assertBuiltInTemplateVariantAllowed({
12
+ templateId,
13
+ variant,
14
+ });
13
15
  return resolveBuiltInTemplateSource(templateId, {
14
16
  persistenceEnabled: variables.compoundPersistenceEnabled === 'true',
15
17
  persistencePolicy: variables.persistencePolicy === 'public' ? 'public' : 'authenticated',
@@ -28,6 +30,7 @@ export async function resolveTemplateSource(templateId, cwd, variables, variant)
28
30
  throw new Error(`--variant is only supported for official external template configs. Received variant "${variant}" for "${templateId}".`);
29
31
  }
30
32
  normalizedSeed = await normalizeWpTypiaTemplateSeed(seed);
33
+ const supportsMigrationUi = getTemplateProjectType(seed.blockDir) === 'workspace';
31
34
  return {
32
35
  id: templateId,
33
36
  defaultCategory: getDefaultCategory(seed.blockDir),
@@ -35,6 +38,7 @@ export async function resolveTemplateSource(templateId, cwd, variables, variant)
35
38
  features: ['Remote source', 'wp-typia format'],
36
39
  format,
37
40
  isOfficialWorkspaceTemplate,
41
+ supportsMigrationUi,
38
42
  templateDir: normalizedSeed.blockDir,
39
43
  cleanup: normalizedSeed.cleanup,
40
44
  };
@@ -48,6 +52,33 @@ export async function resolveTemplateSource(templateId, cwd, variables, variant)
48
52
  })()
49
53
  : seed;
50
54
  if (format === 'create-block-external') {
55
+ const renderedFormat = normalizedSeed.formatHint ??
56
+ (await detectTemplateSourceFormat(normalizedSeed.blockDir));
57
+ if (renderedFormat === 'wp-typia') {
58
+ const normalized = await normalizeWpTypiaTemplateSeed(normalizedSeed);
59
+ const supportsMigrationUi = getTemplateProjectType(normalizedSeed.blockDir) === 'workspace';
60
+ return {
61
+ cleanup: async () => {
62
+ await normalized.cleanup?.();
63
+ await seed.cleanup?.();
64
+ },
65
+ defaultCategory: getDefaultCategory(normalizedSeed.blockDir),
66
+ description: 'A wp-typia scaffold normalized from an official external template config',
67
+ features: [
68
+ 'Remote source',
69
+ 'official external template',
70
+ 'wp-typia format',
71
+ ...(supportsMigrationUi ? ['workspace-capable scaffold'] : []),
72
+ ],
73
+ format,
74
+ id: 'remote:create-block-external',
75
+ isOfficialWorkspaceTemplate,
76
+ selectedVariant: normalizedSeed.selectedVariant ?? null,
77
+ supportsMigrationUi,
78
+ templateDir: normalized.blockDir,
79
+ warnings: normalizedSeed.warnings ?? [],
80
+ };
81
+ }
51
82
  const normalized = await normalizeCreateBlockSubset(normalizedSeed, context);
52
83
  return {
53
84
  ...normalized,