@wp-typia/project-tools 0.11.1

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 (187) hide show
  1. package/README.md +32 -0
  2. package/dist/runtime/cli-add.d.ts +38 -0
  3. package/dist/runtime/cli-add.js +561 -0
  4. package/dist/runtime/cli-core.d.ts +25 -0
  5. package/dist/runtime/cli-core.js +25 -0
  6. package/dist/runtime/cli-doctor.d.ts +34 -0
  7. package/dist/runtime/cli-doctor.js +131 -0
  8. package/dist/runtime/cli-help.d.ts +9 -0
  9. package/dist/runtime/cli-help.js +37 -0
  10. package/dist/runtime/cli-prompt.d.ts +21 -0
  11. package/dist/runtime/cli-prompt.js +53 -0
  12. package/dist/runtime/cli-scaffold.d.ts +79 -0
  13. package/dist/runtime/cli-scaffold.js +206 -0
  14. package/dist/runtime/cli-templates.d.ts +30 -0
  15. package/dist/runtime/cli-templates.js +61 -0
  16. package/dist/runtime/index.d.ts +9 -0
  17. package/dist/runtime/index.js +7 -0
  18. package/dist/runtime/json-utils.d.ts +10 -0
  19. package/dist/runtime/json-utils.js +12 -0
  20. package/dist/runtime/local-dev-presets.d.ts +26 -0
  21. package/dist/runtime/local-dev-presets.js +132 -0
  22. package/dist/runtime/metadata-analysis.d.ts +11 -0
  23. package/dist/runtime/metadata-analysis.js +285 -0
  24. package/dist/runtime/metadata-model.d.ts +84 -0
  25. package/dist/runtime/metadata-model.js +59 -0
  26. package/dist/runtime/metadata-parser.d.ts +53 -0
  27. package/dist/runtime/metadata-parser.js +794 -0
  28. package/dist/runtime/metadata-php-render.d.ts +29 -0
  29. package/dist/runtime/metadata-php-render.js +549 -0
  30. package/dist/runtime/metadata-projection.d.ts +7 -0
  31. package/dist/runtime/metadata-projection.js +233 -0
  32. package/dist/runtime/migration-constants.d.ts +15 -0
  33. package/dist/runtime/migration-constants.js +16 -0
  34. package/dist/runtime/migration-diff.d.ts +2 -0
  35. package/dist/runtime/migration-diff.js +537 -0
  36. package/dist/runtime/migration-fixtures.d.ts +8 -0
  37. package/dist/runtime/migration-fixtures.js +94 -0
  38. package/dist/runtime/migration-fuzz-plan.d.ts +2 -0
  39. package/dist/runtime/migration-fuzz-plan.js +50 -0
  40. package/dist/runtime/migration-manifest.d.ts +19 -0
  41. package/dist/runtime/migration-manifest.js +129 -0
  42. package/dist/runtime/migration-project.d.ts +94 -0
  43. package/dist/runtime/migration-project.js +1101 -0
  44. package/dist/runtime/migration-render.d.ts +11 -0
  45. package/dist/runtime/migration-render.js +741 -0
  46. package/dist/runtime/migration-risk.d.ts +4 -0
  47. package/dist/runtime/migration-risk.js +52 -0
  48. package/dist/runtime/migration-types.d.ts +249 -0
  49. package/dist/runtime/migration-types.js +1 -0
  50. package/dist/runtime/migration-ui-capability.d.ts +17 -0
  51. package/dist/runtime/migration-ui-capability.js +190 -0
  52. package/dist/runtime/migration-utils.d.ts +69 -0
  53. package/dist/runtime/migration-utils.js +246 -0
  54. package/dist/runtime/migrations.d.ts +249 -0
  55. package/dist/runtime/migrations.js +1061 -0
  56. package/dist/runtime/object-utils.d.ts +12 -0
  57. package/dist/runtime/object-utils.js +14 -0
  58. package/dist/runtime/package-managers.d.ts +28 -0
  59. package/dist/runtime/package-managers.js +156 -0
  60. package/dist/runtime/package-versions.d.ts +10 -0
  61. package/dist/runtime/package-versions.js +68 -0
  62. package/dist/runtime/scaffold-onboarding.d.ts +32 -0
  63. package/dist/runtime/scaffold-onboarding.js +99 -0
  64. package/dist/runtime/scaffold.d.ts +146 -0
  65. package/dist/runtime/scaffold.js +612 -0
  66. package/dist/runtime/schema-core.d.ts +267 -0
  67. package/dist/runtime/schema-core.js +597 -0
  68. package/dist/runtime/starter-manifests.d.ts +25 -0
  69. package/dist/runtime/starter-manifests.js +383 -0
  70. package/dist/runtime/string-case.d.ts +36 -0
  71. package/dist/runtime/string-case.js +69 -0
  72. package/dist/runtime/template-builtins.d.ts +38 -0
  73. package/dist/runtime/template-builtins.js +72 -0
  74. package/dist/runtime/template-defaults.d.ts +75 -0
  75. package/dist/runtime/template-defaults.js +65 -0
  76. package/dist/runtime/template-registry.d.ts +36 -0
  77. package/dist/runtime/template-registry.js +94 -0
  78. package/dist/runtime/template-render.d.ts +24 -0
  79. package/dist/runtime/template-render.js +113 -0
  80. package/dist/runtime/template-source.d.ts +71 -0
  81. package/dist/runtime/template-source.js +821 -0
  82. package/dist/runtime/typia-tags.d.ts +1 -0
  83. package/dist/runtime/typia-tags.js +1 -0
  84. package/package.json +79 -0
  85. package/templates/_shared/base/languages/.gitkeep +1 -0
  86. package/templates/_shared/base/package.json.mustache +41 -0
  87. package/templates/_shared/base/scripts/sync-types-to-block-json.ts.mustache +118 -0
  88. package/templates/_shared/base/src/hooks.ts.mustache +19 -0
  89. package/templates/_shared/base/src/validator-toolkit.ts.mustache +31 -0
  90. package/templates/_shared/base/tsconfig.json.mustache +21 -0
  91. package/templates/_shared/base/webpack.config.js.mustache +99 -0
  92. package/templates/_shared/base/{{slugKebabCase}}.php.mustache +53 -0
  93. package/templates/_shared/compound/core/package.json.mustache +45 -0
  94. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +559 -0
  95. package/templates/_shared/compound/core/scripts/block-config.ts.mustache +13 -0
  96. package/templates/_shared/compound/core/scripts/sync-types-to-block-json.ts.mustache +53 -0
  97. package/templates/_shared/compound/core/webpack.config.js.mustache +141 -0
  98. package/templates/_shared/compound/core/{{slugKebabCase}}.php.mustache +51 -0
  99. package/templates/_shared/compound/persistence/package.json.mustache +50 -0
  100. package/templates/_shared/compound/persistence/scripts/block-config.ts.mustache +59 -0
  101. package/templates/_shared/compound/persistence/scripts/sync-rest-contracts.ts.mustache +101 -0
  102. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api-types.ts.mustache +21 -0
  103. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api-validators.ts.mustache +32 -0
  104. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api.ts.mustache +68 -0
  105. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/block.json.mustache +52 -0
  106. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/data.ts.mustache +192 -0
  107. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/edit.tsx.mustache +123 -0
  108. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/hooks.ts.mustache +11 -0
  109. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/interactivity.ts.mustache +132 -0
  110. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/render.php.mustache +158 -0
  111. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/save.tsx.mustache +3 -0
  112. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/types.ts.mustache +56 -0
  113. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/validators.ts.mustache +32 -0
  114. package/templates/_shared/compound/persistence-auth/{{slugKebabCase}}.php.mustache +294 -0
  115. package/templates/_shared/compound/persistence-public/{{slugKebabCase}}.php.mustache +312 -0
  116. package/templates/_shared/migration-ui/common/src/admin/migration-dashboard.tsx +394 -0
  117. package/templates/_shared/migration-ui/common/src/migration-detector.ts +9 -0
  118. package/templates/_shared/migration-ui/common/src/migrations/helpers.ts +490 -0
  119. package/templates/_shared/migration-ui/common/src/migrations/index.ts +886 -0
  120. package/templates/_shared/persistence/auth/{{slugKebabCase}}.php.mustache +290 -0
  121. package/templates/_shared/persistence/core/package.json.mustache +46 -0
  122. package/templates/_shared/persistence/core/scripts/sync-rest-contracts.ts.mustache +113 -0
  123. package/templates/_shared/persistence/core/scripts/sync-types-to-block-json.ts.mustache +125 -0
  124. package/templates/_shared/persistence/core/src/api-types.ts.mustache +21 -0
  125. package/templates/_shared/persistence/core/src/api-validators.ts.mustache +32 -0
  126. package/templates/_shared/persistence/core/src/api.ts.mustache +68 -0
  127. package/templates/_shared/persistence/core/src/data.ts.mustache +192 -0
  128. package/templates/_shared/persistence/core/src/index.tsx.mustache +25 -0
  129. package/templates/_shared/persistence/core/src/interactivity.ts.mustache +134 -0
  130. package/templates/_shared/persistence/core/src/save.tsx.mustache +5 -0
  131. package/templates/_shared/persistence/core/src/validators.ts.mustache +32 -0
  132. package/templates/_shared/persistence/core/{{slugKebabCase}}.php.mustache +336 -0
  133. package/templates/_shared/persistence/public/{{slugKebabCase}}.php.mustache +308 -0
  134. package/templates/_shared/presets/test-preset/.wp-env.test.json.mustache +16 -0
  135. package/templates/_shared/presets/test-preset/playwright.config.ts.mustache +22 -0
  136. package/templates/_shared/presets/test-preset/scripts/wait-for-wp-env.mjs.mustache +102 -0
  137. package/templates/_shared/presets/test-preset/scripts/wp-env-utils.cjs.mustache +32 -0
  138. package/templates/_shared/presets/test-preset/tests/e2e/smoke.spec.ts.mustache +34 -0
  139. package/templates/_shared/presets/wp-env/.wp-env.json.mustache +16 -0
  140. package/templates/_shared/rest-helpers/auth/inc/rest-auth.php.mustache +37 -0
  141. package/templates/_shared/rest-helpers/public/inc/rest-public.php.mustache +314 -0
  142. package/templates/_shared/rest-helpers/shared/inc/rest-shared.php.mustache +58 -0
  143. package/templates/_shared/workspace/persistence-auth/inc/rest-auth.php.mustache +36 -0
  144. package/templates/_shared/workspace/persistence-auth/inc/rest-shared.php.mustache +55 -0
  145. package/templates/_shared/workspace/persistence-auth/server.php.mustache +237 -0
  146. package/templates/_shared/workspace/persistence-public/inc/rest-public.php.mustache +273 -0
  147. package/templates/_shared/workspace/persistence-public/inc/rest-shared.php.mustache +55 -0
  148. package/templates/_shared/workspace/persistence-public/server.php.mustache +252 -0
  149. package/templates/basic/src/block.json.mustache +51 -0
  150. package/templates/basic/src/edit.tsx.mustache +128 -0
  151. package/templates/basic/src/editor.scss.mustache +8 -0
  152. package/templates/basic/src/hooks.ts.mustache +18 -0
  153. package/templates/basic/src/index.tsx.mustache +45 -0
  154. package/templates/basic/src/save.tsx.mustache +30 -0
  155. package/templates/basic/src/style.scss.mustache +40 -0
  156. package/templates/basic/src/types.ts.mustache +56 -0
  157. package/templates/basic/src/validators.ts.mustache +26 -0
  158. package/templates/compound/src/blocks/{{slugKebabCase}}/block.json.mustache +37 -0
  159. package/templates/compound/src/blocks/{{slugKebabCase}}/children.ts.mustache +25 -0
  160. package/templates/compound/src/blocks/{{slugKebabCase}}/edit.tsx.mustache +93 -0
  161. package/templates/compound/src/blocks/{{slugKebabCase}}/hooks.ts.mustache +11 -0
  162. package/templates/compound/src/blocks/{{slugKebabCase}}/index.tsx.mustache +25 -0
  163. package/templates/compound/src/blocks/{{slugKebabCase}}/save.tsx.mustache +32 -0
  164. package/templates/compound/src/blocks/{{slugKebabCase}}/style.scss.mustache +31 -0
  165. package/templates/compound/src/blocks/{{slugKebabCase}}/types.ts.mustache +13 -0
  166. package/templates/compound/src/blocks/{{slugKebabCase}}/validators.ts.mustache +17 -0
  167. package/templates/compound/src/blocks/{{slugKebabCase}}-item/block.json.mustache +35 -0
  168. package/templates/compound/src/blocks/{{slugKebabCase}}-item/edit.tsx.mustache +50 -0
  169. package/templates/compound/src/blocks/{{slugKebabCase}}-item/hooks.ts.mustache +11 -0
  170. package/templates/compound/src/blocks/{{slugKebabCase}}-item/index.tsx.mustache +25 -0
  171. package/templates/compound/src/blocks/{{slugKebabCase}}-item/save.tsx.mustache +24 -0
  172. package/templates/compound/src/blocks/{{slugKebabCase}}-item/types.ts.mustache +12 -0
  173. package/templates/compound/src/blocks/{{slugKebabCase}}-item/validators.ts.mustache +17 -0
  174. package/templates/interactivity/package.json.mustache +42 -0
  175. package/templates/interactivity/src/block.json.mustache +73 -0
  176. package/templates/interactivity/src/edit.tsx.mustache +270 -0
  177. package/templates/interactivity/src/index.tsx.mustache +32 -0
  178. package/templates/interactivity/src/interactivity.ts.mustache +152 -0
  179. package/templates/interactivity/src/save.tsx.mustache +101 -0
  180. package/templates/interactivity/src/style.scss.mustache +60 -0
  181. package/templates/interactivity/src/types.ts.mustache +32 -0
  182. package/templates/interactivity/src/validators.ts.mustache +36 -0
  183. package/templates/persistence/src/block.json.mustache +52 -0
  184. package/templates/persistence/src/edit.tsx.mustache +165 -0
  185. package/templates/persistence/src/render.php.mustache +126 -0
  186. package/templates/persistence/src/style.scss.mustache +46 -0
  187. package/templates/persistence/src/types.ts.mustache +55 -0
@@ -0,0 +1,612 @@
1
+ import fs from "node:fs";
2
+ import { promises as fsp } from "node:fs";
3
+ import path from "node:path";
4
+ import { execSync } from "node:child_process";
5
+ import { PACKAGE_MANAGER_IDS, formatPackageExecCommand, formatInstallCommand, formatRunScript, getPackageManager, transformPackageManagerText, } from "./package-managers.js";
6
+ import { applyGeneratedProjectDxPackageJson, applyLocalDevPresetFiles, getPrimaryDevelopmentScript, } from "./local-dev-presets.js";
7
+ import { applyMigrationUiCapability } from "./migration-ui-capability.js";
8
+ import { getPackageVersions } from "./package-versions.js";
9
+ import { ensureMigrationDirectories, writeInitialMigrationScaffold, writeMigrationConfig, } from "./migration-project.js";
10
+ import { getCompoundExtensionWorkflowSection, getOptionalOnboardingNote, getOptionalOnboardingSteps, getPhpRestExtensionPointsSection, getTemplateSourceOfTruthNote, } from "./scaffold-onboarding.js";
11
+ import { getStarterManifestFiles, stringifyStarterManifest } from "./starter-manifests.js";
12
+ import { toKebabCase, toPascalCase, toSnakeCase, toTitleCase, } from "./string-case.js";
13
+ import { BUILTIN_BLOCK_METADATA_VERSION, COMPOUND_CHILD_BLOCK_METADATA_DEFAULTS, getBuiltInTemplateMetadataDefaults, getRemovedBuiltInTemplateMessage, isRemovedBuiltInTemplateId, } from "./template-defaults.js";
14
+ import { copyInterpolatedDirectory } from "./template-render.js";
15
+ import { TEMPLATE_IDS, getTemplateById, isBuiltInTemplateId } from "./template-registry.js";
16
+ import { resolveTemplateSource } from "./template-source.js";
17
+ const BLOCK_SLUG_PATTERN = /^[a-z][a-z0-9-]*$/;
18
+ const PHP_PREFIX_PATTERN = /^[a-z_][a-z0-9_]*$/;
19
+ const PHP_PREFIX_MAX_LENGTH = 50;
20
+ const OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE = "@wp-typia/create-workspace-template";
21
+ const LOCKFILES = {
22
+ bun: ["bun.lock", "bun.lockb"],
23
+ npm: ["package-lock.json"],
24
+ pnpm: ["pnpm-lock.yaml"],
25
+ yarn: ["yarn.lock"],
26
+ };
27
+ export const DATA_STORAGE_MODES = ["post-meta", "custom-table"];
28
+ export const PERSISTENCE_POLICIES = ["authenticated", "public"];
29
+ function validateBlockSlug(input) {
30
+ return BLOCK_SLUG_PATTERN.test(input) || "Use lowercase letters, numbers, and hyphens only";
31
+ }
32
+ function validateNamespace(input) {
33
+ return BLOCK_SLUG_PATTERN.test(toKebabCase(input))
34
+ ? true
35
+ : "Use lowercase letters, numbers, and hyphens only";
36
+ }
37
+ function validateTextDomain(input) {
38
+ return BLOCK_SLUG_PATTERN.test(toKebabCase(input))
39
+ ? true
40
+ : "Use lowercase letters, numbers, and hyphens only";
41
+ }
42
+ function validatePhpPrefix(input) {
43
+ const normalizedPrefix = toSnakeCase(input);
44
+ if (normalizedPrefix.length > PHP_PREFIX_MAX_LENGTH) {
45
+ return `Use ${PHP_PREFIX_MAX_LENGTH} characters or fewer to keep generated database identifiers within MySQL limits`;
46
+ }
47
+ return PHP_PREFIX_PATTERN.test(normalizedPrefix)
48
+ ? true
49
+ : "Use letters, numbers, and underscores only, starting with a letter";
50
+ }
51
+ function assertValidIdentifier(label, value, validate) {
52
+ const result = validate(value);
53
+ if (result !== true) {
54
+ throw new Error(typeof result === "string" ? `${label}: ${result}` : `${label} is invalid`);
55
+ }
56
+ return value;
57
+ }
58
+ function normalizeBlockSlug(input) {
59
+ return toKebabCase(input);
60
+ }
61
+ function resolveValidatedBlockSlug(value) {
62
+ return assertValidIdentifier("Block slug", normalizeBlockSlug(value), validateBlockSlug);
63
+ }
64
+ function resolveValidatedNamespace(value) {
65
+ return assertValidIdentifier("Namespace", toKebabCase(value), validateNamespace);
66
+ }
67
+ function resolveValidatedTextDomain(value) {
68
+ return assertValidIdentifier("Text domain", toKebabCase(value), validateTextDomain);
69
+ }
70
+ function resolveValidatedPhpPrefix(value) {
71
+ return assertValidIdentifier("PHP prefix", toSnakeCase(value), validatePhpPrefix);
72
+ }
73
+ /**
74
+ * Builds the generated WordPress wrapper CSS class for a scaffolded block.
75
+ *
76
+ * Returns `wp-block-{namespace}-{slug}` when a non-empty namespace is present,
77
+ * or `wp-block-{slug}` when the namespace is empty or undefined. Both inputs
78
+ * are normalized and validated with the same scaffold identifier rules used for
79
+ * block names.
80
+ */
81
+ export function buildBlockCssClassName(namespace, slug) {
82
+ const normalizedSlug = resolveValidatedBlockSlug(slug);
83
+ const normalizedNamespace = typeof namespace === "string" && namespace.trim().length > 0
84
+ ? resolveValidatedNamespace(namespace)
85
+ : "";
86
+ return normalizedNamespace.length > 0
87
+ ? `wp-block-${normalizedNamespace}-${normalizedSlug}`
88
+ : `wp-block-${normalizedSlug}`;
89
+ }
90
+ function buildFrontendCssClassName(blockCssClassName) {
91
+ return `${blockCssClassName}-frontend`;
92
+ }
93
+ function resolveScaffoldIdentifiers({ namespace, phpPrefix, slug, textDomain, }) {
94
+ const normalizedSlug = resolveValidatedBlockSlug(slug);
95
+ return {
96
+ namespace: resolveValidatedNamespace(namespace),
97
+ phpPrefix: resolveValidatedPhpPrefix(phpPrefix ?? normalizedSlug),
98
+ slug: normalizedSlug,
99
+ textDomain: resolveValidatedTextDomain(textDomain ?? normalizedSlug),
100
+ };
101
+ }
102
+ export function isDataStorageMode(value) {
103
+ return DATA_STORAGE_MODES.includes(value);
104
+ }
105
+ export function isPersistencePolicy(value) {
106
+ return PERSISTENCE_POLICIES.includes(value);
107
+ }
108
+ export function detectAuthor() {
109
+ try {
110
+ return (execSync("git config user.name", {
111
+ encoding: "utf8",
112
+ stdio: ["ignore", "pipe", "ignore"],
113
+ }).trim() || "Your Name");
114
+ }
115
+ catch {
116
+ return "Your Name";
117
+ }
118
+ }
119
+ export function getDefaultAnswers(projectName, templateId) {
120
+ const template = isBuiltInTemplateId(templateId) ? getTemplateById(templateId) : null;
121
+ const slugDefault = normalizeBlockSlug(projectName) || "my-wp-typia-block";
122
+ return {
123
+ author: detectAuthor(),
124
+ dataStorageMode: templateId === "persistence" ? "custom-table" : undefined,
125
+ description: template?.description ?? "A WordPress block scaffolded from a remote template",
126
+ namespace: slugDefault,
127
+ persistencePolicy: templateId === "persistence" ? "authenticated" : undefined,
128
+ phpPrefix: toSnakeCase(slugDefault),
129
+ slug: slugDefault,
130
+ textDomain: slugDefault,
131
+ title: toTitleCase(slugDefault),
132
+ };
133
+ }
134
+ export async function resolveTemplateId({ templateId, yes = false, isInteractive = false, selectTemplate, }) {
135
+ if (templateId) {
136
+ if (isRemovedBuiltInTemplateId(templateId)) {
137
+ throw new Error(getRemovedBuiltInTemplateMessage(templateId));
138
+ }
139
+ if (isBuiltInTemplateId(templateId)) {
140
+ return getTemplateById(templateId).id;
141
+ }
142
+ return templateId;
143
+ }
144
+ if (yes) {
145
+ return "basic";
146
+ }
147
+ if (!isInteractive || !selectTemplate) {
148
+ throw new Error(`Template is required in non-interactive mode. Use --template <${TEMPLATE_IDS.join("|")}|./path|github:owner/repo/path[#ref]|npm-package>.`);
149
+ }
150
+ return selectTemplate();
151
+ }
152
+ export async function resolvePackageManagerId({ packageManager, yes = false, isInteractive = false, selectPackageManager, }) {
153
+ if (packageManager) {
154
+ return getPackageManager(packageManager).id;
155
+ }
156
+ if (yes) {
157
+ throw new Error(`Package manager is required when using --yes. Use --package-manager <${PACKAGE_MANAGER_IDS.join("|")}>.`);
158
+ }
159
+ if (!isInteractive || !selectPackageManager) {
160
+ throw new Error(`Package manager is required in non-interactive mode. Use --package-manager <${PACKAGE_MANAGER_IDS.join("|")}>.`);
161
+ }
162
+ return selectPackageManager();
163
+ }
164
+ export async function collectScaffoldAnswers({ projectName, templateId, yes = false, dataStorageMode, namespace, persistencePolicy, phpPrefix, promptText, textDomain, }) {
165
+ const defaults = getDefaultAnswers(projectName, templateId);
166
+ if (yes) {
167
+ const identifiers = resolveScaffoldIdentifiers({
168
+ namespace: namespace ?? defaults.namespace,
169
+ phpPrefix,
170
+ slug: defaults.slug,
171
+ textDomain,
172
+ });
173
+ return {
174
+ ...defaults,
175
+ dataStorageMode: dataStorageMode ?? defaults.dataStorageMode,
176
+ namespace: identifiers.namespace,
177
+ persistencePolicy: persistencePolicy ?? defaults.persistencePolicy,
178
+ phpPrefix: identifiers.phpPrefix,
179
+ textDomain: identifiers.textDomain,
180
+ };
181
+ }
182
+ if (!promptText) {
183
+ throw new Error("Interactive answers require a promptText callback.");
184
+ }
185
+ const identifiers = resolveScaffoldIdentifiers({
186
+ namespace: namespace ?? (await promptText("Namespace", defaults.namespace, validateNamespace)),
187
+ phpPrefix,
188
+ slug: await promptText("Block slug", defaults.slug, validateBlockSlug),
189
+ textDomain,
190
+ });
191
+ return {
192
+ author: await promptText("Author", defaults.author),
193
+ dataStorageMode: dataStorageMode ?? defaults.dataStorageMode,
194
+ description: await promptText("Description", defaults.description),
195
+ namespace: identifiers.namespace,
196
+ persistencePolicy: persistencePolicy ?? defaults.persistencePolicy,
197
+ phpPrefix: identifiers.phpPrefix,
198
+ slug: identifiers.slug,
199
+ textDomain: identifiers.textDomain,
200
+ title: await promptText("Block title", toTitleCase(identifiers.slug)),
201
+ };
202
+ }
203
+ export function getTemplateVariables(templateId, answers) {
204
+ const { apiClientPackageVersion, blockRuntimePackageVersion, blockTypesPackageVersion, projectToolsPackageVersion, restPackageVersion, } = getPackageVersions();
205
+ const template = isBuiltInTemplateId(templateId) ? getTemplateById(templateId) : null;
206
+ const metadataDefaults = isBuiltInTemplateId(templateId)
207
+ ? getBuiltInTemplateMetadataDefaults(templateId)
208
+ : null;
209
+ const identifiers = resolveScaffoldIdentifiers({
210
+ namespace: answers.namespace,
211
+ phpPrefix: answers.phpPrefix,
212
+ slug: answers.slug,
213
+ textDomain: answers.textDomain,
214
+ });
215
+ const slug = identifiers.slug;
216
+ const slugSnakeCase = toSnakeCase(slug);
217
+ const pascalCase = toPascalCase(slug);
218
+ const title = answers.title.trim();
219
+ const namespace = identifiers.namespace;
220
+ const description = answers.description.trim();
221
+ const textDomain = identifiers.textDomain;
222
+ const phpPrefix = identifiers.phpPrefix;
223
+ const phpPrefixUpper = phpPrefix.toUpperCase();
224
+ const compoundChildTitle = `${title} Item`;
225
+ const cssClassName = buildBlockCssClassName(namespace, slug);
226
+ const compoundChildCssClassName = buildBlockCssClassName(namespace, `${slug}-item`);
227
+ const compoundPersistenceEnabled = templateId === "persistence"
228
+ ? true
229
+ : templateId === "compound"
230
+ ? Boolean(answers.dataStorageMode || answers.persistencePolicy)
231
+ : false;
232
+ const dataStorageMode = templateId === "persistence" || compoundPersistenceEnabled
233
+ ? answers.dataStorageMode ?? "custom-table"
234
+ : "custom-table";
235
+ const persistencePolicy = templateId === "persistence" || compoundPersistenceEnabled
236
+ ? answers.persistencePolicy ?? "authenticated"
237
+ : "authenticated";
238
+ return {
239
+ apiClientPackageVersion,
240
+ author: answers.author.trim(),
241
+ blockRuntimePackageVersion,
242
+ blockMetadataVersion: BUILTIN_BLOCK_METADATA_VERSION,
243
+ blockTypesPackageVersion,
244
+ category: metadataDefaults?.category ?? template?.defaultCategory ?? "widgets",
245
+ icon: metadataDefaults?.icon ?? "smiley",
246
+ compoundChildTitle,
247
+ compoundChildCategory: COMPOUND_CHILD_BLOCK_METADATA_DEFAULTS.category,
248
+ compoundChildCssClassName,
249
+ compoundChildIcon: COMPOUND_CHILD_BLOCK_METADATA_DEFAULTS.icon,
250
+ compoundChildTitleJson: JSON.stringify(compoundChildTitle),
251
+ compoundPersistenceEnabled: compoundPersistenceEnabled ? "true" : "false",
252
+ projectToolsPackageVersion,
253
+ cssClassName,
254
+ dataStorageMode,
255
+ dashCase: slug,
256
+ description,
257
+ frontendCssClassName: buildFrontendCssClassName(cssClassName),
258
+ isAuthenticatedPersistencePolicy: persistencePolicy === "authenticated" ? "true" : "false",
259
+ isPublicPersistencePolicy: persistencePolicy === "public" ? "true" : "false",
260
+ keyword: slug.replace(/-/g, " "),
261
+ namespace,
262
+ needsMigration: "{{needsMigration}}",
263
+ pascalCase,
264
+ phpPrefix,
265
+ phpPrefixUpper,
266
+ restPackageVersion,
267
+ publicWriteRequestIdDeclaration: persistencePolicy === "public"
268
+ ? "publicWriteRequestId: string & tags.MinLength< 1 > & tags.MaxLength< 128 >;"
269
+ : "publicWriteRequestId?: string & tags.MinLength< 1 > & tags.MaxLength< 128 >;",
270
+ restWriteAuthIntent: persistencePolicy === "public"
271
+ ? "public-write-protected"
272
+ : "authenticated",
273
+ restWriteAuthMechanism: persistencePolicy === "public" ? "public-signed-token" : "rest-nonce",
274
+ restWriteAuthMode: persistencePolicy === "public" ? "public-signed-token" : "authenticated-rest-nonce",
275
+ slug,
276
+ slugCamelCase: pascalCase.charAt(0).toLowerCase() + pascalCase.slice(1),
277
+ slugKebabCase: slug,
278
+ slugSnakeCase,
279
+ textDomain,
280
+ textdomain: textDomain,
281
+ title,
282
+ titleJson: JSON.stringify(title),
283
+ titleCase: pascalCase,
284
+ persistencePolicy,
285
+ };
286
+ }
287
+ async function ensureDirectory(targetDir, allowExisting = false) {
288
+ if (!fs.existsSync(targetDir)) {
289
+ await fsp.mkdir(targetDir, { recursive: true });
290
+ return;
291
+ }
292
+ if (allowExisting) {
293
+ return;
294
+ }
295
+ const entries = await fsp.readdir(targetDir);
296
+ if (entries.length > 0) {
297
+ throw new Error(`Target directory is not empty: ${targetDir}`);
298
+ }
299
+ }
300
+ function buildReadme(templateId, variables, packageManager, { withMigrationUi = false, withTestPreset = false, withWpEnv = false, } = {}) {
301
+ const optionalOnboardingSteps = getOptionalOnboardingSteps(packageManager, templateId, {
302
+ compoundPersistenceEnabled: variables.compoundPersistenceEnabled === "true",
303
+ });
304
+ const sourceOfTruthNote = getTemplateSourceOfTruthNote(templateId, {
305
+ compoundPersistenceEnabled: variables.compoundPersistenceEnabled === "true",
306
+ });
307
+ const publicPersistencePolicyNote = variables.isPublicPersistencePolicy === "true"
308
+ ? "Public persistence writes use signed short-lived tokens, per-request ids, and coarse rate limiting by default. Add application-specific abuse controls before using the same pattern for high-value metrics or experiments."
309
+ : null;
310
+ const compoundExtensionWorkflowSection = getCompoundExtensionWorkflowSection(packageManager, templateId);
311
+ const phpRestExtensionPointsSection = getPhpRestExtensionPointsSection(templateId, {
312
+ compoundPersistenceEnabled: variables.compoundPersistenceEnabled === "true",
313
+ slug: variables.slug,
314
+ });
315
+ const developmentScript = getPrimaryDevelopmentScript(templateId);
316
+ const wpEnvSection = withWpEnv
317
+ ? `## Local WordPress\n\n\`\`\`bash\n${formatRunScript(packageManager, "wp-env:start")}\n${formatRunScript(packageManager, "wp-env:stop")}\n${formatRunScript(packageManager, "wp-env:reset")}\n\`\`\``
318
+ : "";
319
+ const testPresetSection = withTestPreset
320
+ ? `## Local Test Preset\n\n\`\`\`bash\n${formatRunScript(packageManager, "wp-env:start:test")}\n${formatRunScript(packageManager, "wp-env:wait:test")}\n${formatRunScript(packageManager, "test:e2e")}\n\`\`\`\n\nThe generated smoke test uses \`.wp-env.test.json\` and verifies that the scaffolded block registers in the WordPress editor.`
321
+ : "";
322
+ const migrationSection = withMigrationUi
323
+ ? `## Migration UI\n\nThis scaffold already includes an initialized migration workspace at \`v1\`, generated deprecated/runtime artifacts, and an editor-embedded migration dashboard. Migration versions are schema lineage labels and are separate from your package or plugin release version. Use the existing CLI commands to snapshot, diff, scaffold, verify, and fuzz future schema changes.\n\n\`\`\`bash\n${formatRunScript(packageManager, "migration:doctor")}\n${formatRunScript(packageManager, "migration:verify")}\n${formatRunScript(packageManager, "migration:fuzz")}\n\`\`\`\n\nRun \`migration:init\` only when retrofitting migration support into an older project that was not scaffolded with \`--with-migration-ui\`.`
324
+ : "";
325
+ return `# ${variables.title}
326
+
327
+ ${variables.description}
328
+
329
+ ## Template
330
+
331
+ ${templateId}
332
+
333
+ ## Development
334
+
335
+ \`\`\`bash
336
+ ${formatInstallCommand(packageManager)}
337
+ ${formatRunScript(packageManager, developmentScript)}
338
+ \`\`\`
339
+
340
+ ## Build
341
+
342
+ \`\`\`bash
343
+ ${formatRunScript(packageManager, "build")}
344
+ \`\`\`
345
+
346
+ ## Optional First Sync
347
+
348
+ \`\`\`bash
349
+ ${optionalOnboardingSteps.join("\n")}
350
+ \`\`\`
351
+
352
+ ${getOptionalOnboardingNote(packageManager, templateId)}
353
+
354
+ ${sourceOfTruthNote}${publicPersistencePolicyNote ? `\n\n${publicPersistencePolicyNote}` : ""}${migrationSection ? `\n\n${migrationSection}` : ""}${compoundExtensionWorkflowSection ? `\n\n${compoundExtensionWorkflowSection}` : ""}${wpEnvSection ? `\n\n${wpEnvSection}` : ""}${testPresetSection ? `\n\n${testPresetSection}` : ""}${phpRestExtensionPointsSection ? `\n\n${phpRestExtensionPointsSection}` : ""}
355
+ `;
356
+ }
357
+ function buildGitignore() {
358
+ return `# Dependencies
359
+ node_modules/
360
+ .yarn/
361
+ .pnp.*
362
+
363
+ # Build
364
+ build/
365
+ dist/
366
+
367
+ # Editor
368
+ .vscode/
369
+ .idea/
370
+
371
+ # OS
372
+ .DS_Store
373
+ Thumbs.db
374
+
375
+ # WordPress
376
+ *.log
377
+ .wp-env/
378
+ `;
379
+ }
380
+ function mergeTextLines(primaryContent, existingContent) {
381
+ const normalizedPrimary = primaryContent.replace(/\r\n/g, "\n").trimEnd();
382
+ const normalizedExisting = existingContent.replace(/\r\n/g, "\n").trimEnd();
383
+ const mergedLines = [];
384
+ const seen = new Set();
385
+ for (const line of [...normalizedPrimary.split("\n"), ...normalizedExisting.split("\n")]) {
386
+ if (line.length === 0 && mergedLines[mergedLines.length - 1] === "") {
387
+ continue;
388
+ }
389
+ if (line.length > 0 && seen.has(line)) {
390
+ continue;
391
+ }
392
+ if (line.length > 0) {
393
+ seen.add(line);
394
+ }
395
+ mergedLines.push(line);
396
+ }
397
+ return `${mergedLines.join("\n").replace(/\n{3,}/g, "\n\n")}\n`;
398
+ }
399
+ async function writeStarterManifestFiles(targetDir, templateId, variables) {
400
+ const manifests = getStarterManifestFiles(templateId, variables);
401
+ for (const { document, relativePath } of manifests) {
402
+ const destinationPath = path.join(targetDir, relativePath);
403
+ await fsp.mkdir(path.dirname(destinationPath), { recursive: true });
404
+ await fsp.writeFile(destinationPath, stringifyStarterManifest(document), "utf8");
405
+ }
406
+ }
407
+ async function normalizePackageManagerFiles(targetDir, packageManagerId) {
408
+ const yarnRcPath = path.join(targetDir, ".yarnrc.yml");
409
+ if (packageManagerId === "yarn") {
410
+ await fsp.writeFile(yarnRcPath, "nodeLinker: node-modules\n", "utf8");
411
+ return;
412
+ }
413
+ if (fs.existsSync(yarnRcPath)) {
414
+ await fsp.rm(yarnRcPath, { force: true });
415
+ }
416
+ }
417
+ async function normalizePackageJson(targetDir, packageManagerId) {
418
+ const packageJsonPath = path.join(targetDir, "package.json");
419
+ if (!fs.existsSync(packageJsonPath)) {
420
+ return;
421
+ }
422
+ const packageManager = getPackageManager(packageManagerId);
423
+ const packageJson = JSON.parse(await fsp.readFile(packageJsonPath, "utf8"));
424
+ packageJson.packageManager = packageManager.packageManagerField;
425
+ if (packageJson.scripts) {
426
+ for (const [key, value] of Object.entries(packageJson.scripts)) {
427
+ if (typeof value === "string") {
428
+ packageJson.scripts[key] = transformPackageManagerText(value, packageManagerId);
429
+ }
430
+ }
431
+ }
432
+ await fsp.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, "\t")}\n`, "utf8");
433
+ }
434
+ async function removeUnexpectedLockfiles(targetDir, packageManagerId) {
435
+ const keep = new Set(LOCKFILES[packageManagerId] ?? []);
436
+ const allLockfiles = Object.values(LOCKFILES).flat();
437
+ await Promise.all(allLockfiles.map(async (filename) => {
438
+ if (keep.has(filename)) {
439
+ return;
440
+ }
441
+ const filePath = path.join(targetDir, filename);
442
+ if (fs.existsSync(filePath)) {
443
+ await fsp.rm(filePath, { force: true });
444
+ }
445
+ }));
446
+ }
447
+ async function replaceTextRecursively(targetDir, packageManagerId) {
448
+ const textExtensions = new Set([
449
+ ".css",
450
+ ".js",
451
+ ".json",
452
+ ".jsx",
453
+ ".md",
454
+ ".php",
455
+ ".scss",
456
+ ".ts",
457
+ ".tsx",
458
+ ".txt",
459
+ ]);
460
+ async function visit(currentPath) {
461
+ const stats = await fsp.stat(currentPath);
462
+ if (stats.isDirectory()) {
463
+ const entries = await fsp.readdir(currentPath);
464
+ for (const entry of entries) {
465
+ await visit(path.join(currentPath, entry));
466
+ }
467
+ return;
468
+ }
469
+ if (path.basename(currentPath) === "package.json" || !textExtensions.has(path.extname(currentPath))) {
470
+ return;
471
+ }
472
+ const content = await fsp.readFile(currentPath, "utf8");
473
+ const nextContent = transformPackageManagerText(content, packageManagerId)
474
+ .replace(/yourusername\/wp-typia-boilerplate/g, "imjlk/wp-typia")
475
+ .replace(/yourusername\/wp-typia/g, "imjlk/wp-typia");
476
+ if (nextContent !== content) {
477
+ await fsp.writeFile(currentPath, nextContent, "utf8");
478
+ }
479
+ }
480
+ await visit(targetDir);
481
+ }
482
+ async function defaultInstallDependencies({ projectDir, packageManager, }) {
483
+ execSync(formatInstallCommand(packageManager), {
484
+ cwd: projectDir,
485
+ stdio: "inherit",
486
+ });
487
+ }
488
+ function isOfficialWorkspaceProject(projectDir) {
489
+ const packageJsonPath = path.join(projectDir, "package.json");
490
+ if (!fs.existsSync(packageJsonPath)) {
491
+ return false;
492
+ }
493
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
494
+ return (packageJson.wpTypia?.projectType === "workspace" &&
495
+ packageJson.wpTypia?.templatePackage === OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE);
496
+ }
497
+ async function applyWorkspaceMigrationCapability(projectDir, packageManager) {
498
+ const packageJsonPath = path.join(projectDir, "package.json");
499
+ const packageJson = JSON.parse(await fsp.readFile(packageJsonPath, "utf8"));
500
+ const wpTypiaPackageVersion = getPackageVersions().wpTypiaPackageVersion;
501
+ const canonicalCliSpecifier = wpTypiaPackageVersion === "^0.0.0"
502
+ ? "wp-typia"
503
+ : `wp-typia@${wpTypiaPackageVersion.replace(/^[~^]/u, "")}`;
504
+ const migrationCli = (args) => formatPackageExecCommand(packageManager, canonicalCliSpecifier, `migrate ${args}`);
505
+ packageJson.scripts = {
506
+ ...(packageJson.scripts ?? {}),
507
+ "migration:init": migrationCli("init --current-migration-version v1"),
508
+ "migration:snapshot": migrationCli("snapshot"),
509
+ "migration:diff": migrationCli("diff"),
510
+ "migration:scaffold": migrationCli("scaffold"),
511
+ "migration:doctor": migrationCli("doctor --all"),
512
+ "migration:fixtures": migrationCli("fixtures --all"),
513
+ "migration:verify": migrationCli("verify --all"),
514
+ "migration:fuzz": migrationCli("fuzz --all"),
515
+ };
516
+ await fsp.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, "\t")}\n`, "utf8");
517
+ writeMigrationConfig(projectDir, {
518
+ blocks: [],
519
+ currentMigrationVersion: "v1",
520
+ snapshotDir: "src/migrations/versions",
521
+ supportedMigrationVersions: ["v1"],
522
+ });
523
+ ensureMigrationDirectories(projectDir, []);
524
+ writeInitialMigrationScaffold(projectDir, "v1", []);
525
+ }
526
+ export async function scaffoldProject({ projectDir, templateId, answers, dataStorageMode, persistencePolicy, packageManager, cwd = process.cwd(), allowExistingDir = false, noInstall = false, installDependencies = undefined, variant, withMigrationUi = false, withTestPreset = false, withWpEnv = false, }) {
527
+ const resolvedPackageManager = getPackageManager(packageManager).id;
528
+ const isBuiltInTemplate = isBuiltInTemplateId(templateId);
529
+ const variables = getTemplateVariables(templateId, {
530
+ ...answers,
531
+ dataStorageMode: dataStorageMode ?? answers.dataStorageMode,
532
+ persistencePolicy: persistencePolicy ?? answers.persistencePolicy,
533
+ });
534
+ const templateSource = await resolveTemplateSource(templateId, cwd, variables, variant);
535
+ const supportsMigrationUi = isBuiltInTemplate || templateSource.isOfficialWorkspaceTemplate === true;
536
+ if (withMigrationUi && !supportsMigrationUi) {
537
+ await templateSource.cleanup?.();
538
+ throw new Error("`--with-migration-ui` is currently supported only for built-in templates and @wp-typia/create-workspace-template.");
539
+ }
540
+ try {
541
+ await ensureDirectory(projectDir, allowExistingDir);
542
+ await copyInterpolatedDirectory(templateSource.templateDir, projectDir, variables);
543
+ }
544
+ finally {
545
+ if (templateSource.cleanup) {
546
+ await templateSource.cleanup();
547
+ }
548
+ }
549
+ const isOfficialWorkspace = isOfficialWorkspaceProject(projectDir);
550
+ if (isBuiltInTemplate) {
551
+ await writeStarterManifestFiles(projectDir, templateId, variables);
552
+ await applyLocalDevPresetFiles({
553
+ projectDir,
554
+ variables,
555
+ withTestPreset,
556
+ withWpEnv,
557
+ });
558
+ if (withMigrationUi) {
559
+ await applyMigrationUiCapability({
560
+ packageManager: resolvedPackageManager,
561
+ projectDir,
562
+ templateId,
563
+ variables,
564
+ });
565
+ }
566
+ }
567
+ else if (withMigrationUi && isOfficialWorkspace) {
568
+ await applyWorkspaceMigrationCapability(projectDir, resolvedPackageManager);
569
+ }
570
+ const readmePath = path.join(projectDir, "README.md");
571
+ if (!fs.existsSync(readmePath)) {
572
+ await fsp.writeFile(readmePath, buildReadme(templateId, variables, resolvedPackageManager, {
573
+ withMigrationUi: isBuiltInTemplate || isOfficialWorkspace ? withMigrationUi : false,
574
+ withTestPreset: isBuiltInTemplate ? withTestPreset : false,
575
+ withWpEnv: isBuiltInTemplate ? withWpEnv : false,
576
+ }), "utf8");
577
+ }
578
+ const gitignorePath = path.join(projectDir, ".gitignore");
579
+ const existingGitignore = fs.existsSync(gitignorePath)
580
+ ? await fsp.readFile(gitignorePath, "utf8")
581
+ : "";
582
+ await fsp.writeFile(gitignorePath, mergeTextLines(buildGitignore(), existingGitignore), "utf8");
583
+ await normalizePackageJson(projectDir, resolvedPackageManager);
584
+ if (isBuiltInTemplate) {
585
+ await applyGeneratedProjectDxPackageJson({
586
+ compoundPersistenceEnabled: variables.compoundPersistenceEnabled === "true",
587
+ packageManager: resolvedPackageManager,
588
+ projectDir,
589
+ templateId,
590
+ withTestPreset,
591
+ withWpEnv,
592
+ });
593
+ }
594
+ await normalizePackageManagerFiles(projectDir, resolvedPackageManager);
595
+ await removeUnexpectedLockfiles(projectDir, resolvedPackageManager);
596
+ await replaceTextRecursively(projectDir, resolvedPackageManager);
597
+ if (!noInstall) {
598
+ const installer = installDependencies ?? defaultInstallDependencies;
599
+ await installer({
600
+ projectDir,
601
+ packageManager: resolvedPackageManager,
602
+ });
603
+ }
604
+ return {
605
+ projectDir,
606
+ selectedVariant: templateSource.selectedVariant ?? null,
607
+ templateId,
608
+ packageManager: resolvedPackageManager,
609
+ variables,
610
+ warnings: templateSource.warnings ?? [],
611
+ };
612
+ }