@wp-typia/project-tools 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/runtime/block-generator-service-core.d.ts +1 -1
  2. package/dist/runtime/block-generator-service-core.js +2 -1
  3. package/dist/runtime/block-generator-service-spec.d.ts +2 -1
  4. package/dist/runtime/built-in-block-artifacts.js +1 -0
  5. package/dist/runtime/built-in-block-code-templates/compound-child.d.ts +2 -2
  6. package/dist/runtime/built-in-block-code-templates/compound-child.js +30 -2
  7. package/dist/runtime/built-in-block-code-templates/compound-parent.d.ts +1 -1
  8. package/dist/runtime/built-in-block-code-templates/compound-parent.js +139 -19
  9. package/dist/runtime/cli-add-block.d.ts +2 -1
  10. package/dist/runtime/cli-add-block.js +19 -1
  11. package/dist/runtime/cli-add-shared.d.ts +55 -1
  12. package/dist/runtime/cli-add-shared.js +101 -2
  13. package/dist/runtime/cli-add-workspace-assets.d.ts +21 -1
  14. package/dist/runtime/cli-add-workspace-assets.js +417 -1
  15. package/dist/runtime/cli-add-workspace-rest.d.ts +14 -0
  16. package/dist/runtime/cli-add-workspace-rest.js +1060 -0
  17. package/dist/runtime/cli-add-workspace.d.ts +10 -1
  18. package/dist/runtime/cli-add-workspace.js +10 -1
  19. package/dist/runtime/cli-add.d.ts +3 -3
  20. package/dist/runtime/cli-add.js +2 -2
  21. package/dist/runtime/cli-core.d.ts +3 -1
  22. package/dist/runtime/cli-core.js +2 -1
  23. package/dist/runtime/cli-doctor-workspace.js +135 -1
  24. package/dist/runtime/cli-help.js +10 -6
  25. package/dist/runtime/cli-scaffold.d.ts +10 -2
  26. package/dist/runtime/cli-scaffold.js +136 -36
  27. package/dist/runtime/cli-templates.d.ts +4 -4
  28. package/dist/runtime/cli-templates.js +79 -39
  29. package/dist/runtime/index.d.ts +4 -3
  30. package/dist/runtime/index.js +3 -2
  31. package/dist/runtime/local-dev-presets.js +7 -2
  32. package/dist/runtime/rest-resource-artifacts.d.ts +35 -0
  33. package/dist/runtime/rest-resource-artifacts.js +158 -0
  34. package/dist/runtime/scaffold-answer-resolution.js +68 -2
  35. package/dist/runtime/scaffold-apply-utils.d.ts +4 -3
  36. package/dist/runtime/scaffold-apply-utils.js +34 -17
  37. package/dist/runtime/scaffold-bootstrap.d.ts +15 -0
  38. package/dist/runtime/scaffold-bootstrap.js +29 -7
  39. package/dist/runtime/scaffold-documents.js +2 -1
  40. package/dist/runtime/scaffold-onboarding.js +7 -3
  41. package/dist/runtime/scaffold-package-manager-files.js +6 -1
  42. package/dist/runtime/scaffold.d.ts +7 -1
  43. package/dist/runtime/scaffold.js +50 -8
  44. package/dist/runtime/template-render.d.ts +5 -2
  45. package/dist/runtime/template-render.js +9 -3
  46. package/dist/runtime/template-source-contracts.d.ts +11 -0
  47. package/dist/runtime/template-source-external.d.ts +1 -1
  48. package/dist/runtime/template-source-external.js +45 -13
  49. package/dist/runtime/template-source-normalization.d.ts +1 -1
  50. package/dist/runtime/template-source-normalization.js +5 -1
  51. package/dist/runtime/template-source-remote.d.ts +5 -0
  52. package/dist/runtime/template-source-remote.js +33 -0
  53. package/dist/runtime/template-source.js +30 -1
  54. package/dist/runtime/workspace-inventory.d.ts +43 -1
  55. package/dist/runtime/workspace-inventory.js +132 -1
  56. package/dist/runtime/workspace-project.d.ts +1 -1
  57. package/dist/runtime/workspace-project.js +2 -2
  58. package/package.json +3 -3
  59. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +428 -49
  60. package/templates/query-loop/src/validator-toolkit.ts.mustache +0 -1
@@ -10,12 +10,17 @@ import { ensureMigrationDirectories, writeInitialMigrationScaffold, writeMigrati
10
10
  import { syncPersistenceRestArtifacts, } from "./persistence-rest-artifacts.js";
11
11
  import { buildGitignore, buildReadme, mergeTextLines, } from "./scaffold-documents.js";
12
12
  import { getStarterManifestFiles, stringifyStarterManifest, } from "./starter-manifests.js";
13
+ import { formatNonEmptyTargetDirectoryError, } from "./scaffold-bootstrap.js";
13
14
  import { stringifyBuiltInBlockJsonDocument, } from "./built-in-block-artifacts.js";
14
15
  import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, } from "./template-registry.js";
15
16
  import { copyInterpolatedDirectory } from "./template-render.js";
16
- import { formatInstallCommand, formatPackageExecCommand, getPackageManager, transformPackageManagerText, } from "./package-managers.js";
17
+ import { formatInstallCommand, formatPackageExecCommand, transformPackageManagerText, } from "./package-managers.js";
18
+ import { normalizePackageJson } from "./scaffold-package-manager-files.js";
17
19
  import { replaceRepositoryReferencePlaceholders, resolveScaffoldRepositoryReference, } from "./scaffold-repository-reference.js";
18
20
  export { buildGitignore, buildReadme, mergeTextLines, } from "./scaffold-documents.js";
21
+ async function reportScaffoldProgress(onProgress, event) {
22
+ await onProgress?.(event);
23
+ }
19
24
  const EPHEMERAL_NODE_MODULES_LINK_TYPE = process.platform === "win32" ? "junction" : "dir";
20
25
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
26
  const LOCKFILES = {
@@ -34,7 +39,7 @@ export async function ensureDirectory(targetDir, allowExisting = false) {
34
39
  }
35
40
  const entries = await fsp.readdir(targetDir);
36
41
  if (entries.length > 0) {
37
- throw new Error(`Target directory is not empty: ${targetDir}`);
42
+ throw new Error(formatNonEmptyTargetDirectoryError(targetDir));
38
43
  }
39
44
  }
40
45
  export async function writeStarterManifestFiles(targetDir, templateId, variables, artifacts) {
@@ -135,22 +140,13 @@ export async function normalizePackageManagerFiles(targetDir, packageManagerId)
135
140
  await fsp.rm(yarnRcPath, { force: true });
136
141
  }
137
142
  }
138
- export async function normalizePackageJson(targetDir, packageManagerId) {
139
- const packageJsonPath = path.join(targetDir, "package.json");
140
- if (!fs.existsSync(packageJsonPath)) {
143
+ export async function removeQueryLoopPlaceholderFiles(projectDir, templateId) {
144
+ if (templateId !== "query-loop") {
141
145
  return;
142
146
  }
143
- const packageManager = getPackageManager(packageManagerId);
144
- const packageJson = JSON.parse(await fsp.readFile(packageJsonPath, "utf8"));
145
- packageJson.packageManager = packageManager.packageManagerField;
146
- if (packageJson.scripts) {
147
- for (const [key, value] of Object.entries(packageJson.scripts)) {
148
- if (typeof value === "string") {
149
- packageJson.scripts[key] = transformPackageManagerText(value, packageManagerId);
150
- }
151
- }
152
- }
153
- await fsp.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, "\t")}\n`, "utf8");
147
+ await fsp.rm(path.join(projectDir, "src", "validator-toolkit.ts"), {
148
+ force: true,
149
+ });
154
150
  }
155
151
  export async function removeUnexpectedLockfiles(targetDir, packageManagerId) {
156
152
  const keep = new Set(LOCKFILES[packageManagerId] ?? []);
@@ -254,8 +250,13 @@ export async function applyWorkspaceMigrationCapability(projectDir, packageManag
254
250
  * Applies a built-in scaffold into the target directory, including generated
255
251
  * code artifacts, starter manifests, preset files, and placeholder rewrites.
256
252
  */
257
- export async function applyBuiltInScaffoldProjectFiles({ projectDir, templateDir, templateId, variables, artifacts, codeArtifacts, readmeContent, gitignoreContent, allowExistingDir = false, packageManager, withMigrationUi = false, withTestPreset = false, withWpEnv = false, noInstall = false, installDependencies, repositoryReference, }) {
253
+ export async function applyBuiltInScaffoldProjectFiles({ projectDir, templateDir, templateId, variables, artifacts, codeArtifacts, readmeContent, gitignoreContent, allowExistingDir = false, packageManager, withMigrationUi = false, withTestPreset = false, withWpEnv = false, noInstall = false, installDependencies, repositoryReference, onProgress, }) {
258
254
  await ensureDirectory(projectDir, allowExistingDir);
255
+ await reportScaffoldProgress(onProgress, {
256
+ detail: "Copying built-in template files and writing generated source modules.",
257
+ phase: "generate-files",
258
+ title: "Generating project files",
259
+ });
259
260
  await copyInterpolatedDirectory(templateDir, projectDir, variables);
260
261
  if (codeArtifacts && codeArtifacts.length > 0) {
261
262
  await writeBuiltInCodeArtifacts(projectDir, codeArtifacts);
@@ -263,6 +264,11 @@ export async function applyBuiltInScaffoldProjectFiles({ projectDir, templateDir
263
264
  if (artifacts && artifacts.length > 0) {
264
265
  await writeBuiltInStructuralArtifacts(projectDir, artifacts);
265
266
  }
267
+ await reportScaffoldProgress(onProgress, {
268
+ detail: "Writing starter manifests, local presets, and seeded template artifacts.",
269
+ phase: "seed-artifacts",
270
+ title: "Seeding scaffold artifacts",
271
+ });
266
272
  await writeStarterManifestFiles(projectDir, templateId, variables, artifacts);
267
273
  await seedBuiltInPersistenceArtifacts(projectDir, templateId, variables);
268
274
  await applyLocalDevPresetFiles({
@@ -279,6 +285,11 @@ export async function applyBuiltInScaffoldProjectFiles({ projectDir, templateDir
279
285
  variables,
280
286
  });
281
287
  }
288
+ await reportScaffoldProgress(onProgress, {
289
+ detail: "Writing README, normalizing package metadata, and aligning package-manager files.",
290
+ phase: "finalize-project",
291
+ title: "Finalizing scaffold output",
292
+ });
282
293
  const readmePath = path.join(projectDir, "README.md");
283
294
  if (!fs.existsSync(readmePath)) {
284
295
  await fsp.writeFile(readmePath, readmeContent ??
@@ -302,12 +313,18 @@ export async function applyBuiltInScaffoldProjectFiles({ projectDir, templateDir
302
313
  withTestPreset,
303
314
  withWpEnv,
304
315
  });
316
+ await removeQueryLoopPlaceholderFiles(projectDir, templateId);
305
317
  await normalizePackageManagerFiles(projectDir, packageManager);
306
318
  await removeUnexpectedLockfiles(projectDir, packageManager);
307
319
  await replaceTextRecursively(projectDir, packageManager, {
308
320
  repositoryReference,
309
321
  });
310
322
  if (!noInstall) {
323
+ await reportScaffoldProgress(onProgress, {
324
+ detail: "Installing project dependencies with the selected package manager.",
325
+ phase: "install-dependencies",
326
+ title: "Installing dependencies",
327
+ });
311
328
  const installer = installDependencies ?? defaultInstallDependencies;
312
329
  await installer({
313
330
  projectDir,
@@ -9,6 +9,14 @@ import type { BuiltInTemplateId } from "./template-registry.js";
9
9
  * @returns A promise that resolves once the directory precondition is satisfied.
10
10
  */
11
11
  export declare function ensureScaffoldDirectory(targetDir: string, allowExisting?: boolean): Promise<void>;
12
+ /**
13
+ * Format the actionable error message used when a scaffold target directory
14
+ * already exists and is not empty.
15
+ *
16
+ * @param targetDir Absolute path to the target directory being evaluated.
17
+ * @returns A human-readable error string with next-step guidance.
18
+ */
19
+ export declare function formatNonEmptyTargetDirectoryError(targetDir: string): string;
12
20
  /**
13
21
  * Writes built-in starter manifest files into a scaffolded project.
14
22
  *
@@ -28,6 +36,13 @@ export declare function writeStarterManifestFiles(targetDir: string, templateId:
28
36
  * @returns A promise that resolves after any required persistence artifacts are generated.
29
37
  */
30
38
  export declare function seedBuiltInPersistenceArtifacts(targetDir: string, templateId: BuiltInTemplateId, variables: ScaffoldTemplateVariables): Promise<void>;
39
+ /**
40
+ * Detects whether a scaffolded project declares the workspace project model.
41
+ *
42
+ * @param projectDir Absolute scaffold target directory.
43
+ * @returns `true` when the project metadata identifies a workspace scaffold.
44
+ */
45
+ export declare function isWorkspaceProject(projectDir: string): boolean;
31
46
  /**
32
47
  * Detects whether a scaffolded project is the official workspace template.
33
48
  *
@@ -25,9 +25,26 @@ export async function ensureScaffoldDirectory(targetDir, allowExisting = false)
25
25
  }
26
26
  const entries = await fsp.readdir(targetDir);
27
27
  if (entries.length > 0) {
28
- throw new Error(`Target directory is not empty: ${targetDir}`);
28
+ throw new Error(formatNonEmptyTargetDirectoryError(targetDir));
29
29
  }
30
30
  }
31
+ function readGeneratedPackageJson(projectDir) {
32
+ const packageJsonPath = path.join(projectDir, "package.json");
33
+ if (!fs.existsSync(packageJsonPath)) {
34
+ return null;
35
+ }
36
+ return JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
37
+ }
38
+ /**
39
+ * Format the actionable error message used when a scaffold target directory
40
+ * already exists and is not empty.
41
+ *
42
+ * @param targetDir Absolute path to the target directory being evaluated.
43
+ * @returns A human-readable error string with next-step guidance.
44
+ */
45
+ export function formatNonEmptyTargetDirectoryError(targetDir) {
46
+ return `Target directory is not empty: ${targetDir}. Choose a new project directory, or empty this directory before rerunning the scaffold.`;
47
+ }
31
48
  /**
32
49
  * Writes built-in starter manifest files into a scaffolded project.
33
50
  *
@@ -77,6 +94,15 @@ export async function seedBuiltInPersistenceArtifacts(targetDir, templateId, var
77
94
  });
78
95
  });
79
96
  }
97
+ /**
98
+ * Detects whether a scaffolded project declares the workspace project model.
99
+ *
100
+ * @param projectDir Absolute scaffold target directory.
101
+ * @returns `true` when the project metadata identifies a workspace scaffold.
102
+ */
103
+ export function isWorkspaceProject(projectDir) {
104
+ return readGeneratedPackageJson(projectDir)?.wpTypia?.projectType === "workspace";
105
+ }
80
106
  /**
81
107
  * Detects whether a scaffolded project is the official workspace template.
82
108
  *
@@ -84,12 +110,8 @@ export async function seedBuiltInPersistenceArtifacts(targetDir, templateId, var
84
110
  * @returns `true` when the project metadata identifies the official workspace template.
85
111
  */
86
112
  export function isOfficialWorkspaceProject(projectDir) {
87
- const packageJsonPath = path.join(projectDir, "package.json");
88
- if (!fs.existsSync(packageJsonPath)) {
89
- return false;
90
- }
91
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
92
- return (packageJson.wpTypia?.projectType === "workspace" &&
113
+ const packageJson = readGeneratedPackageJson(projectDir);
114
+ return (packageJson?.wpTypia?.projectType === "workspace" &&
93
115
  packageJson.wpTypia?.templatePackage === OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE);
94
116
  }
95
117
  /**
@@ -28,6 +28,7 @@ export function buildReadme(templateId, variables, packageManager, { withMigrati
28
28
  slug: variables.slug,
29
29
  });
30
30
  const developmentScript = getPrimaryDevelopmentScript(templateId);
31
+ const noStepsHeading = templateId === 'query-loop' ? 'Variation Workflow' : 'Artifact Refresh';
31
32
  const wpEnvSection = withWpEnv
32
33
  ? `## Local WordPress\n\n\`\`\`bash\n${formatRunScript(packageManager, 'wp-env:start')}\n${formatRunScript(packageManager, 'wp-env:stop')}\n${formatRunScript(packageManager, 'wp-env:reset')}\n\`\`\``
33
34
  : '';
@@ -41,7 +42,7 @@ export function buildReadme(templateId, variables, packageManager, { withMigrati
41
42
  ? `## Advanced Sync\n\n\`\`\`bash\n${optionalOnboardingSteps.join('\n')}\n\`\`\`\n\n${getOptionalOnboardingNote(packageManager, templateId, {
42
43
  compoundPersistenceEnabled,
43
44
  })}`
44
- : `## Artifact Refresh\n\n${getOptionalOnboardingNote(packageManager, templateId, {
45
+ : `## ${noStepsHeading}\n\n${getOptionalOnboardingNote(packageManager, templateId, {
45
46
  compoundPersistenceEnabled,
46
47
  })}`;
47
48
  return `# ${variables.title}
@@ -41,7 +41,7 @@ export function getOptionalOnboardingSteps(packageManager, templateId, options =
41
41
  */
42
42
  export function getQuickStartWorkflowNote(packageManager, templateId = "basic", options = {}) {
43
43
  if (templateId === "query-loop") {
44
- return `${formatRunScript(packageManager, "start")} runs the editor build/watch loop that registers your Query Loop variation in the block editor. This scaffold is editor-facing by design: update \`src/index.ts\` when you want to change the variation namespace, default query, allowed controls, or the minimal inline starter layout, update \`src/patterns/*.php\` when you want richer connected layout presets in the inserter, use \`src/query-extension.ts\` when the variation needs custom query params or optional editor-side hooks, and mirror frontend/editor preview query mapping in \`inc/query-runtime.php\`.`;
44
+ return `${formatRunScript(packageManager, "dev")} runs the editor build/watch loop that registers your Query Loop variation in the block editor. This scaffold is editor-facing by design: update \`src/index.ts\` when you want to change the variation namespace, default query, allowed controls, or the minimal inline starter layout, update \`src/patterns/*.php\` when you want richer connected layout presets in the inserter, use \`src/query-extension.ts\` when the variation needs custom query params or optional editor-side hooks, and mirror frontend/editor preview query mapping in \`inc/query-runtime.php\`.`;
45
45
  }
46
46
  const developmentScript = getPrimaryDevelopmentScript(templateId);
47
47
  const devCommand = formatRunScript(packageManager, developmentScript);
@@ -62,7 +62,7 @@ export function getQuickStartWorkflowNote(packageManager, templateId = "basic",
62
62
  */
63
63
  export function getOptionalOnboardingNote(packageManager, templateId = "basic", options = {}) {
64
64
  if (templateId === "query-loop") {
65
- return `This scaffold does not generate \`block.json\` or Typia manifests. Edit \`src/index.ts\` to change the variation contract, edit \`src/patterns/*.php\` when you want richer connected layouts beyond the inline fallback, edit \`src/query-extension.ts\` when you need variation-specific query params or custom editor hooks, and edit \`inc/query-runtime.php\` when those params need frontend or editor preview parity. Then rerun ${formatRunScript(packageManager, "build")}, ${formatRunScript(packageManager, "start")}, or ${formatRunScript(packageManager, "typecheck")} as needed.`;
65
+ return `This scaffold does not generate a \`sync\` script, \`block.json\`, or Typia manifests. Edit \`src/index.ts\` to change the variation contract, edit \`src/patterns/*.php\` when you want richer connected layouts beyond the inline fallback, edit \`src/query-extension.ts\` when you need variation-specific query params or custom editor hooks, and edit \`inc/query-runtime.php\` when those params need frontend or editor preview parity. Then rerun ${formatRunScript(packageManager, "build")}, ${formatRunScript(packageManager, "dev")}, or ${formatRunScript(packageManager, "typecheck")} as needed.`;
66
66
  }
67
67
  const optionalSyncScripts = getOptionalSyncScriptNames(templateId, options);
68
68
  const hasUnifiedSync = optionalSyncScripts.includes("sync");
@@ -135,9 +135,13 @@ export function getCompoundExtensionWorkflowSection(packageManager, templateId)
135
135
 
136
136
  \`\`\`bash
137
137
  ${formatRunScript(packageManager, "add-child", '--slug faq-item --title "FAQ Item"')}
138
+
139
+ ${formatRunScript(packageManager, "add-child", '--slug section --title "Section" --container --inserter visible')}
140
+
141
+ ${formatRunScript(packageManager, "add-child", '--slug clause --title "Clause" --ancestor section')}
138
142
  \`\`\`
139
143
 
140
- This scaffolds a new hidden child block type, updates \`scripts/block-config.ts\` and \`src/blocks/*/children.ts\`, and leaves the default seeded child template unchanged.`;
144
+ This scaffolds additional compound child block types, updates \`scripts/block-config.ts\` and \`src/blocks/*/children.ts\`, and now supports root-level hidden children, visible container children, and nested ancestor chains for richer document-style block hierarchies.`;
141
145
  }
142
146
  function formatPhpRestExtensionPointsSection({ apiTypesPath, extraNote, mainPhpPath, mainPhpScope, transportPath, }) {
143
147
  const schemaJsonGlob = apiTypesPath.replace(/api-types\.ts$/u, "api-schemas/*.schema.json");
@@ -38,7 +38,12 @@ export async function normalizePackageJson(targetDir, packageManagerId) {
38
38
  }
39
39
  const packageManager = getPackageManager(packageManagerId);
40
40
  const packageJson = JSON.parse(await fsp.readFile(packageJsonPath, "utf8"));
41
- packageJson.packageManager = packageManager.packageManagerField;
41
+ if (packageManagerId === "npm") {
42
+ delete packageJson.packageManager;
43
+ }
44
+ else {
45
+ packageJson.packageManager = packageManager.packageManagerField;
46
+ }
42
47
  if (packageJson.scripts) {
43
48
  for (const [key, value] of Object.entries(packageJson.scripts)) {
44
49
  if (typeof value === "string") {
@@ -124,6 +124,11 @@ interface InstallDependenciesOptions {
124
124
  packageManager: PackageManagerId;
125
125
  projectDir: string;
126
126
  }
127
+ export interface ScaffoldProgressEvent {
128
+ detail: string;
129
+ phase: "finalize-project" | "generate-files" | "install-dependencies" | "resolve-template" | "seed-artifacts";
130
+ title: string;
131
+ }
127
132
  interface ScaffoldProjectOptions {
128
133
  allowExistingDir?: boolean;
129
134
  answers: ScaffoldAnswers;
@@ -134,6 +139,7 @@ interface ScaffoldProjectOptions {
134
139
  externalLayerSourceLabel?: string;
135
140
  installDependencies?: ((options: InstallDependenciesOptions) => Promise<void>) | undefined;
136
141
  noInstall?: boolean;
142
+ onProgress?: ((event: ScaffoldProgressEvent) => void | Promise<void>) | undefined;
137
143
  packageManager: PackageManagerId;
138
144
  persistencePolicy?: PersistencePolicy;
139
145
  projectDir: string;
@@ -157,4 +163,4 @@ export { collectScaffoldAnswers, detectAuthor, getDefaultAnswers, resolvePackage
157
163
  export { getTemplateVariables } from "./scaffold-template-variables.js";
158
164
  export declare function isDataStorageMode(value: string): value is DataStorageMode;
159
165
  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>;
166
+ export declare function scaffoldProject({ projectDir, templateId, answers, dataStorageMode, persistencePolicy, packageManager, externalLayerId, externalLayerSource, externalLayerSourceLabel, repositoryReference, cwd, allowExistingDir, noInstall, installDependencies, onProgress, variant, withMigrationUi, withTestPreset, withWpEnv, }: ScaffoldProjectOptions): Promise<ScaffoldProjectResult>;
@@ -2,10 +2,10 @@ 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";
@@ -29,7 +29,10 @@ function normalizeTemplateSelection(templateId) {
29
29
  ? OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE
30
30
  : templateId;
31
31
  }
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, }) {
32
+ async function reportScaffoldProgress(onProgress, event) {
33
+ await onProgress?.(event);
34
+ }
35
+ export async function scaffoldProject({ projectDir, templateId, answers, 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
36
  const resolvedTemplateId = normalizeTemplateSelection(templateId);
34
37
  const resolvedPackageManager = getPackageManager(packageManager).id;
35
38
  const isBuiltInTemplate = isBuiltInTemplateId(resolvedTemplateId);
@@ -38,6 +41,11 @@ export async function scaffoldProject({ projectDir, templateId, answers, dataSto
38
41
  }
39
42
  if (isBuiltInTemplate) {
40
43
  const blockGeneratorService = new BlockGeneratorService();
44
+ await reportScaffoldProgress(onProgress, {
45
+ detail: "Preparing template layers, variants, and generated artifact plans.",
46
+ phase: "resolve-template",
47
+ title: "Resolving scaffold template",
48
+ });
41
49
  const plan = await blockGeneratorService.plan({
42
50
  allowExistingDir,
43
51
  answers,
@@ -61,6 +69,7 @@ export async function scaffoldProject({ projectDir, templateId, answers, dataSto
61
69
  const rendered = await blockGeneratorService.render({ validated });
62
70
  return blockGeneratorService.apply({
63
71
  installDependencies,
72
+ onProgress,
64
73
  rendered,
65
74
  });
66
75
  }
@@ -72,13 +81,25 @@ export async function scaffoldProject({ projectDir, templateId, answers, dataSto
72
81
  dataStorageMode: dataStorageMode ?? answers.dataStorageMode,
73
82
  persistencePolicy: persistencePolicy ?? answers.persistencePolicy,
74
83
  });
84
+ await reportScaffoldProgress(onProgress, {
85
+ detail: "Loading template files, variants, and external package metadata when needed.",
86
+ phase: "resolve-template",
87
+ title: "Resolving scaffold template",
88
+ });
75
89
  const templateSource = await resolveTemplateSource(resolvedTemplateId, cwd, variables, variant);
76
- const supportsMigrationUi = isBuiltInTemplate || templateSource.isOfficialWorkspaceTemplate === true;
90
+ const supportsMigrationUi = isBuiltInTemplate ||
91
+ templateSource.isOfficialWorkspaceTemplate === true ||
92
+ templateSource.supportsMigrationUi === true;
77
93
  if (withMigrationUi && !supportsMigrationUi) {
78
94
  await templateSource.cleanup?.();
79
- throw new Error("`--with-migration-ui` is currently supported only for built-in templates and @wp-typia/create-workspace-template.");
95
+ throw new Error("`--with-migration-ui` is currently supported only for built-in templates and workspace-capable wp-typia templates.");
80
96
  }
81
97
  try {
98
+ await reportScaffoldProgress(onProgress, {
99
+ detail: "Copying scaffold files into the target project directory.",
100
+ phase: "generate-files",
101
+ title: "Generating project files",
102
+ });
82
103
  await ensureScaffoldDirectory(projectDir, allowExistingDir);
83
104
  await copyInterpolatedDirectory(templateSource.templateDir, projectDir, variables);
84
105
  }
@@ -87,8 +108,13 @@ export async function scaffoldProject({ projectDir, templateId, answers, dataSto
87
108
  await templateSource.cleanup();
88
109
  }
89
110
  }
90
- const isOfficialWorkspace = isOfficialWorkspaceProject(projectDir);
111
+ const isWorkspace = isWorkspaceProject(projectDir);
91
112
  if (isBuiltInTemplate) {
113
+ await reportScaffoldProgress(onProgress, {
114
+ detail: "Writing starter manifests, local presets, and template-specific generated artifacts.",
115
+ phase: "seed-artifacts",
116
+ title: "Seeding scaffold artifacts",
117
+ });
92
118
  await writeStarterManifestFiles(projectDir, resolvedTemplateId, variables);
93
119
  await seedBuiltInPersistenceArtifacts(projectDir, resolvedTemplateId, variables);
94
120
  await applyLocalDevPresetFiles({
@@ -105,14 +131,25 @@ export async function scaffoldProject({ projectDir, templateId, answers, dataSto
105
131
  variables,
106
132
  });
107
133
  }
134
+ await removeQueryLoopPlaceholderFiles(projectDir, resolvedTemplateId);
108
135
  }
109
- else if (withMigrationUi && isOfficialWorkspace) {
136
+ else if (withMigrationUi && isWorkspace) {
137
+ await reportScaffoldProgress(onProgress, {
138
+ detail: "Initializing workspace migration scripts and starter migration files.",
139
+ phase: "seed-artifacts",
140
+ title: "Seeding scaffold artifacts",
141
+ });
110
142
  await applyWorkspaceMigrationCapability(projectDir, resolvedPackageManager);
111
143
  }
144
+ await reportScaffoldProgress(onProgress, {
145
+ detail: "Writing README, normalizing package metadata, and aligning package-manager files.",
146
+ phase: "finalize-project",
147
+ title: "Finalizing scaffold output",
148
+ });
112
149
  const readmePath = path.join(projectDir, "README.md");
113
150
  if (!fs.existsSync(readmePath)) {
114
151
  await fsp.writeFile(readmePath, buildReadme(resolvedTemplateId, variables, resolvedPackageManager, {
115
- withMigrationUi: isBuiltInTemplate || isOfficialWorkspace ? withMigrationUi : false,
152
+ withMigrationUi: isBuiltInTemplate || isWorkspace ? withMigrationUi : false,
116
153
  withTestPreset: isBuiltInTemplate ? withTestPreset : false,
117
154
  withWpEnv: isBuiltInTemplate ? withWpEnv : false,
118
155
  }), "utf8");
@@ -139,6 +176,11 @@ export async function scaffoldProject({ projectDir, templateId, answers, dataSto
139
176
  repositoryReference,
140
177
  });
141
178
  if (!noInstall) {
179
+ await reportScaffoldProgress(onProgress, {
180
+ detail: "Installing project dependencies with the selected package manager.",
181
+ phase: "install-dependencies",
182
+ title: "Installing dependencies",
183
+ });
142
184
  const installer = installDependencies ?? defaultInstallDependencies;
143
185
  await installer({
144
186
  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;