@wp-typia/project-tools 0.16.2 → 0.16.4

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 (64) hide show
  1. package/README.md +11 -0
  2. package/dist/runtime/block-generator-service.d.ts +102 -0
  3. package/dist/runtime/block-generator-service.js +268 -0
  4. package/dist/runtime/built-in-block-artifacts.d.ts +37 -0
  5. package/dist/runtime/built-in-block-artifacts.js +1203 -0
  6. package/dist/runtime/built-in-block-code-artifacts.d.ts +30 -0
  7. package/dist/runtime/built-in-block-code-artifacts.js +122 -0
  8. package/dist/runtime/index.d.ts +2 -0
  9. package/dist/runtime/index.js +1 -0
  10. package/dist/runtime/scaffold-apply-utils.d.ts +47 -0
  11. package/dist/runtime/scaffold-apply-utils.js +405 -0
  12. package/dist/runtime/scaffold-identifiers.d.ts +34 -0
  13. package/dist/runtime/scaffold-identifiers.js +82 -0
  14. package/dist/runtime/scaffold.js +33 -0
  15. package/dist/runtime/starter-manifests.d.ts +3 -2
  16. package/dist/runtime/starter-manifests.js +15 -365
  17. package/dist/runtime/template-render.d.ts +5 -0
  18. package/dist/runtime/template-render.js +13 -3
  19. package/package.json +2 -2
  20. package/templates/_shared/compound/persistence/scripts/block-config.ts.mustache +4 -4
  21. package/templates/_shared/persistence/core/scripts/sync-rest-contracts.ts.mustache +4 -4
  22. package/templates/_shared/base/src/hooks.ts.mustache +0 -19
  23. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/block.json.mustache +0 -52
  24. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/edit.tsx.mustache +0 -123
  25. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/hooks.ts.mustache +0 -11
  26. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/interactivity.ts.mustache +0 -305
  27. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/save.tsx.mustache +0 -3
  28. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/types.ts.mustache +0 -61
  29. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/validators.ts.mustache +0 -43
  30. package/templates/_shared/persistence/core/src/index.tsx.mustache +0 -25
  31. package/templates/_shared/persistence/core/src/interactivity.ts.mustache +0 -308
  32. package/templates/_shared/persistence/core/src/save.tsx.mustache +0 -5
  33. package/templates/_shared/persistence/core/src/validators.ts.mustache +0 -43
  34. package/templates/basic/src/block.json.mustache +0 -51
  35. package/templates/basic/src/edit.tsx.mustache +0 -128
  36. package/templates/basic/src/index.tsx.mustache +0 -45
  37. package/templates/basic/src/save.tsx.mustache +0 -30
  38. package/templates/basic/src/types.ts.mustache +0 -56
  39. package/templates/basic/src/validators.ts.mustache +0 -37
  40. package/templates/compound/src/blocks/{{slugKebabCase}}/block.json.mustache +0 -37
  41. package/templates/compound/src/blocks/{{slugKebabCase}}/children.ts.mustache +0 -25
  42. package/templates/compound/src/blocks/{{slugKebabCase}}/edit.tsx.mustache +0 -93
  43. package/templates/compound/src/blocks/{{slugKebabCase}}/hooks.ts.mustache +0 -11
  44. package/templates/compound/src/blocks/{{slugKebabCase}}/index.tsx.mustache +0 -25
  45. package/templates/compound/src/blocks/{{slugKebabCase}}/save.tsx.mustache +0 -32
  46. package/templates/compound/src/blocks/{{slugKebabCase}}/types.ts.mustache +0 -18
  47. package/templates/compound/src/blocks/{{slugKebabCase}}/validators.ts.mustache +0 -35
  48. package/templates/compound/src/blocks/{{slugKebabCase}}-item/block.json.mustache +0 -35
  49. package/templates/compound/src/blocks/{{slugKebabCase}}-item/edit.tsx.mustache +0 -50
  50. package/templates/compound/src/blocks/{{slugKebabCase}}-item/hooks.ts.mustache +0 -11
  51. package/templates/compound/src/blocks/{{slugKebabCase}}-item/index.tsx.mustache +0 -25
  52. package/templates/compound/src/blocks/{{slugKebabCase}}-item/save.tsx.mustache +0 -24
  53. package/templates/compound/src/blocks/{{slugKebabCase}}-item/types.ts.mustache +0 -17
  54. package/templates/compound/src/blocks/{{slugKebabCase}}-item/validators.ts.mustache +0 -35
  55. package/templates/interactivity/src/block.json.mustache +0 -74
  56. package/templates/interactivity/src/edit.tsx.mustache +0 -270
  57. package/templates/interactivity/src/index.tsx.mustache +0 -33
  58. package/templates/interactivity/src/interactivity.ts.mustache +0 -152
  59. package/templates/interactivity/src/save.tsx.mustache +0 -101
  60. package/templates/interactivity/src/types.ts.mustache +0 -32
  61. package/templates/interactivity/src/validators.ts.mustache +0 -47
  62. package/templates/persistence/src/block.json.mustache +0 -52
  63. package/templates/persistence/src/edit.tsx.mustache +0 -165
  64. package/templates/persistence/src/types.ts.mustache +0 -59
@@ -0,0 +1,405 @@
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 { fileURLToPath } from "node:url";
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 { syncPersistenceRestArtifacts, } from "./persistence-rest-artifacts.js";
11
+ import { getCompoundExtensionWorkflowSection, getOptionalOnboardingNote, getOptionalOnboardingSteps, getPhpRestExtensionPointsSection, getTemplateSourceOfTruthNote, } from "./scaffold-onboarding.js";
12
+ import { getStarterManifestFiles, stringifyStarterManifest, } from "./starter-manifests.js";
13
+ import { stringifyBuiltInBlockJsonDocument, } from "./built-in-block-artifacts.js";
14
+ import { OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE, } from "./template-registry.js";
15
+ import { copyInterpolatedDirectory } from "./template-render.js";
16
+ import { formatInstallCommand, formatPackageExecCommand, formatRunScript, getPackageManager, transformPackageManagerText, } from "./package-managers.js";
17
+ const EPHEMERAL_NODE_MODULES_LINK_TYPE = process.platform === "win32" ? "junction" : "dir";
18
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
+ const LOCKFILES = {
20
+ bun: ["bun.lock", "bun.lockb"],
21
+ npm: ["package-lock.json"],
22
+ pnpm: ["pnpm-lock.yaml"],
23
+ yarn: ["yarn.lock"],
24
+ };
25
+ export async function ensureDirectory(targetDir, allowExisting = false) {
26
+ if (!fs.existsSync(targetDir)) {
27
+ await fsp.mkdir(targetDir, { recursive: true });
28
+ return;
29
+ }
30
+ if (allowExisting) {
31
+ return;
32
+ }
33
+ const entries = await fsp.readdir(targetDir);
34
+ if (entries.length > 0) {
35
+ throw new Error(`Target directory is not empty: ${targetDir}`);
36
+ }
37
+ }
38
+ export function buildReadme(templateId, variables, packageManager, { withMigrationUi = false, withTestPreset = false, withWpEnv = false, } = {}) {
39
+ const optionalOnboardingSteps = getOptionalOnboardingSteps(packageManager, templateId, {
40
+ compoundPersistenceEnabled: variables.compoundPersistenceEnabled === "true",
41
+ });
42
+ const sourceOfTruthNote = getTemplateSourceOfTruthNote(templateId, {
43
+ compoundPersistenceEnabled: variables.compoundPersistenceEnabled === "true",
44
+ });
45
+ const compoundPersistenceEnabled = variables.compoundPersistenceEnabled === "true";
46
+ const publicPersistencePolicyNote = variables.isPublicPersistencePolicy === "true"
47
+ ? "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."
48
+ : null;
49
+ const compoundExtensionWorkflowSection = getCompoundExtensionWorkflowSection(packageManager, templateId);
50
+ const phpRestExtensionPointsSection = getPhpRestExtensionPointsSection(templateId, {
51
+ compoundPersistenceEnabled,
52
+ slug: variables.slug,
53
+ });
54
+ const developmentScript = getPrimaryDevelopmentScript(templateId);
55
+ const wpEnvSection = withWpEnv
56
+ ? `## Local WordPress\n\n\`\`\`bash\n${formatRunScript(packageManager, "wp-env:start")}\n${formatRunScript(packageManager, "wp-env:stop")}\n${formatRunScript(packageManager, "wp-env:reset")}\n\`\`\``
57
+ : "";
58
+ const testPresetSection = withTestPreset
59
+ ? `## 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.`
60
+ : "";
61
+ const migrationSection = withMigrationUi
62
+ ? `## 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\`.`
63
+ : "";
64
+ return `# ${variables.title}
65
+
66
+ ${variables.description}
67
+
68
+ ## Template
69
+
70
+ ${templateId}
71
+
72
+ ## Development
73
+
74
+ \`\`\`bash
75
+ ${formatInstallCommand(packageManager)}
76
+ ${formatRunScript(packageManager, developmentScript)}
77
+ \`\`\`
78
+
79
+ ## Build
80
+
81
+ \`\`\`bash
82
+ ${formatRunScript(packageManager, "build")}
83
+ \`\`\`
84
+
85
+ ## Optional First Sync
86
+
87
+ \`\`\`bash
88
+ ${optionalOnboardingSteps.join("\n")}
89
+ \`\`\`
90
+
91
+ ${getOptionalOnboardingNote(packageManager, templateId, {
92
+ compoundPersistenceEnabled,
93
+ })}
94
+
95
+ ${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}` : ""}
96
+ `;
97
+ }
98
+ export function buildGitignore() {
99
+ return `# Dependencies
100
+ node_modules/
101
+ .yarn/
102
+ .pnp.*
103
+
104
+ # Build
105
+ build/
106
+ dist/
107
+
108
+ # Editor
109
+ .vscode/
110
+ .idea/
111
+
112
+ # OS
113
+ .DS_Store
114
+ Thumbs.db
115
+
116
+ # WordPress
117
+ *.log
118
+ .wp-env/
119
+ `;
120
+ }
121
+ export function mergeTextLines(primaryContent, existingContent) {
122
+ const normalizedPrimary = primaryContent.replace(/\r\n/g, "\n").trimEnd();
123
+ const normalizedExisting = existingContent.replace(/\r\n/g, "\n").trimEnd();
124
+ const mergedLines = [];
125
+ const seen = new Set();
126
+ for (const line of [...normalizedPrimary.split("\n"), ...normalizedExisting.split("\n")]) {
127
+ if (line.length === 0 && mergedLines[mergedLines.length - 1] === "") {
128
+ continue;
129
+ }
130
+ if (line.length > 0 && seen.has(line)) {
131
+ continue;
132
+ }
133
+ if (line.length > 0) {
134
+ seen.add(line);
135
+ }
136
+ mergedLines.push(line);
137
+ }
138
+ return `${mergedLines.join("\n").replace(/\n{3,}/g, "\n\n")}\n`;
139
+ }
140
+ export async function writeStarterManifestFiles(targetDir, templateId, variables, artifacts) {
141
+ const manifests = artifacts
142
+ ? artifacts.map((artifact) => ({
143
+ document: artifact.manifestDocument,
144
+ relativePath: `${artifact.relativeDir}/typia.manifest.json`,
145
+ }))
146
+ : getStarterManifestFiles(templateId, variables);
147
+ for (const { document, relativePath } of manifests) {
148
+ const destinationPath = path.join(targetDir, relativePath);
149
+ await fsp.mkdir(path.dirname(destinationPath), { recursive: true });
150
+ await fsp.writeFile(destinationPath, stringifyStarterManifest(document), "utf8");
151
+ }
152
+ }
153
+ async function writeBuiltInStructuralArtifacts(targetDir, artifacts) {
154
+ for (const artifact of artifacts) {
155
+ const destinationDir = path.join(targetDir, artifact.relativeDir);
156
+ await fsp.mkdir(destinationDir, { recursive: true });
157
+ await fsp.writeFile(path.join(destinationDir, "types.ts"), artifact.typesSource, "utf8");
158
+ await fsp.writeFile(path.join(destinationDir, "block.json"), stringifyBuiltInBlockJsonDocument(artifact.blockJsonDocument), "utf8");
159
+ }
160
+ }
161
+ async function writeBuiltInCodeArtifacts(targetDir, codeArtifacts) {
162
+ for (const artifact of codeArtifacts) {
163
+ const destinationPath = path.join(targetDir, artifact.relativePath);
164
+ await fsp.mkdir(path.dirname(destinationPath), { recursive: true });
165
+ await fsp.writeFile(destinationPath, artifact.source, "utf8");
166
+ }
167
+ }
168
+ function resolveScaffoldGeneratorNodeModulesPath() {
169
+ const projectToolsPackageRoot = path.resolve(__dirname, "..", "..");
170
+ const candidates = [
171
+ path.join(projectToolsPackageRoot, "node_modules"),
172
+ path.resolve(projectToolsPackageRoot, "..", ".."),
173
+ path.resolve(projectToolsPackageRoot, "..", "..", "node_modules"),
174
+ ];
175
+ for (const candidate of candidates) {
176
+ if (fs.existsSync(path.join(candidate, "typia", "package.json"))) {
177
+ return candidate;
178
+ }
179
+ }
180
+ return null;
181
+ }
182
+ async function withEphemeralScaffoldNodeModules(targetDir, callback) {
183
+ const targetNodeModulesPath = path.join(targetDir, "node_modules");
184
+ if (fs.existsSync(targetNodeModulesPath)) {
185
+ await callback();
186
+ return;
187
+ }
188
+ const sourceNodeModulesPath = resolveScaffoldGeneratorNodeModulesPath();
189
+ if (!sourceNodeModulesPath) {
190
+ throw new Error("Unable to resolve a node_modules directory with typia for scaffold-time REST artifact generation.");
191
+ }
192
+ await fsp.symlink(sourceNodeModulesPath, targetNodeModulesPath, EPHEMERAL_NODE_MODULES_LINK_TYPE);
193
+ try {
194
+ await callback();
195
+ }
196
+ finally {
197
+ await fsp.rm(targetNodeModulesPath, { force: true, recursive: true });
198
+ }
199
+ }
200
+ /**
201
+ * Seed REST-derived persistence artifacts into a newly scaffolded built-in
202
+ * project before the first manual `sync-rest` run.
203
+ */
204
+ export async function seedBuiltInPersistenceArtifacts(targetDir, templateId, variables) {
205
+ const needsPersistenceArtifacts = templateId === "persistence" ||
206
+ (templateId === "compound" && variables.compoundPersistenceEnabled === "true");
207
+ if (!needsPersistenceArtifacts) {
208
+ return;
209
+ }
210
+ await withEphemeralScaffoldNodeModules(targetDir, async () => {
211
+ if (templateId === "persistence") {
212
+ await syncPersistenceRestArtifacts({
213
+ apiTypesFile: path.join("src", "api-types.ts"),
214
+ outputDir: "src",
215
+ projectDir: targetDir,
216
+ variables,
217
+ });
218
+ return;
219
+ }
220
+ await syncPersistenceRestArtifacts({
221
+ apiTypesFile: path.join("src", "blocks", variables.slugKebabCase, "api-types.ts"),
222
+ outputDir: path.join("src", "blocks", variables.slugKebabCase),
223
+ projectDir: targetDir,
224
+ variables,
225
+ });
226
+ });
227
+ }
228
+ export async function normalizePackageManagerFiles(targetDir, packageManagerId) {
229
+ const yarnRcPath = path.join(targetDir, ".yarnrc.yml");
230
+ if (packageManagerId === "yarn") {
231
+ await fsp.writeFile(yarnRcPath, "nodeLinker: node-modules\n", "utf8");
232
+ return;
233
+ }
234
+ if (fs.existsSync(yarnRcPath)) {
235
+ await fsp.rm(yarnRcPath, { force: true });
236
+ }
237
+ }
238
+ export async function normalizePackageJson(targetDir, packageManagerId) {
239
+ const packageJsonPath = path.join(targetDir, "package.json");
240
+ if (!fs.existsSync(packageJsonPath)) {
241
+ return;
242
+ }
243
+ const packageManager = getPackageManager(packageManagerId);
244
+ const packageJson = JSON.parse(await fsp.readFile(packageJsonPath, "utf8"));
245
+ packageJson.packageManager = packageManager.packageManagerField;
246
+ if (packageJson.scripts) {
247
+ for (const [key, value] of Object.entries(packageJson.scripts)) {
248
+ if (typeof value === "string") {
249
+ packageJson.scripts[key] = transformPackageManagerText(value, packageManagerId);
250
+ }
251
+ }
252
+ }
253
+ await fsp.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, "\t")}\n`, "utf8");
254
+ }
255
+ export async function removeUnexpectedLockfiles(targetDir, packageManagerId) {
256
+ const keep = new Set(LOCKFILES[packageManagerId] ?? []);
257
+ const allLockfiles = Object.values(LOCKFILES).flat();
258
+ await Promise.all(allLockfiles.map(async (filename) => {
259
+ if (keep.has(filename)) {
260
+ return;
261
+ }
262
+ const filePath = path.join(targetDir, filename);
263
+ if (fs.existsSync(filePath)) {
264
+ await fsp.rm(filePath, { force: true });
265
+ }
266
+ }));
267
+ }
268
+ export async function replaceTextRecursively(targetDir, packageManagerId) {
269
+ const textExtensions = new Set([
270
+ ".css",
271
+ ".js",
272
+ ".json",
273
+ ".jsx",
274
+ ".md",
275
+ ".php",
276
+ ".scss",
277
+ ".ts",
278
+ ".tsx",
279
+ ".txt",
280
+ ]);
281
+ async function visit(currentPath) {
282
+ const stats = await fsp.stat(currentPath);
283
+ if (stats.isDirectory()) {
284
+ const entries = await fsp.readdir(currentPath);
285
+ for (const entry of entries) {
286
+ await visit(path.join(currentPath, entry));
287
+ }
288
+ return;
289
+ }
290
+ if (path.basename(currentPath) === "package.json" || !textExtensions.has(path.extname(currentPath))) {
291
+ return;
292
+ }
293
+ const content = await fsp.readFile(currentPath, "utf8");
294
+ const nextContent = transformPackageManagerText(content, packageManagerId)
295
+ .replace(/yourusername\/wp-typia-boilerplate/g, "imjlk/wp-typia")
296
+ .replace(/yourusername\/wp-typia/g, "imjlk/wp-typia");
297
+ if (nextContent !== content) {
298
+ await fsp.writeFile(currentPath, nextContent, "utf8");
299
+ }
300
+ }
301
+ await visit(targetDir);
302
+ }
303
+ export async function defaultInstallDependencies({ projectDir, packageManager, }) {
304
+ execSync(formatInstallCommand(packageManager), {
305
+ cwd: projectDir,
306
+ stdio: "inherit",
307
+ });
308
+ }
309
+ export function isOfficialWorkspaceProject(projectDir) {
310
+ const packageJsonPath = path.join(projectDir, "package.json");
311
+ if (!fs.existsSync(packageJsonPath)) {
312
+ return false;
313
+ }
314
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
315
+ return (packageJson.wpTypia?.projectType === "workspace" &&
316
+ packageJson.wpTypia?.templatePackage === OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE);
317
+ }
318
+ export async function applyWorkspaceMigrationCapability(projectDir, packageManager) {
319
+ const packageJsonPath = path.join(projectDir, "package.json");
320
+ const packageJson = JSON.parse(await fsp.readFile(packageJsonPath, "utf8"));
321
+ const wpTypiaPackageVersion = getPackageVersions().wpTypiaPackageVersion;
322
+ const canonicalCliSpecifier = wpTypiaPackageVersion === "^0.0.0"
323
+ ? "wp-typia"
324
+ : `wp-typia@${wpTypiaPackageVersion.replace(/^[~^]/u, "")}`;
325
+ const migrationCli = (args) => formatPackageExecCommand(packageManager, canonicalCliSpecifier, `migrate ${args}`);
326
+ packageJson.scripts = {
327
+ ...(packageJson.scripts ?? {}),
328
+ "migration:init": migrationCli("init --current-migration-version v1"),
329
+ "migration:snapshot": migrationCli("snapshot"),
330
+ "migration:diff": migrationCli("diff"),
331
+ "migration:scaffold": migrationCli("scaffold"),
332
+ "migration:doctor": migrationCli("doctor --all"),
333
+ "migration:fixtures": migrationCli("fixtures --all"),
334
+ "migration:verify": migrationCli("verify --all"),
335
+ "migration:fuzz": migrationCli("fuzz --all"),
336
+ };
337
+ await fsp.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, "\t")}\n`, "utf8");
338
+ writeMigrationConfig(projectDir, {
339
+ blocks: [],
340
+ currentMigrationVersion: "v1",
341
+ snapshotDir: "src/migrations/versions",
342
+ supportedMigrationVersions: ["v1"],
343
+ });
344
+ ensureMigrationDirectories(projectDir, []);
345
+ writeInitialMigrationScaffold(projectDir, "v1", []);
346
+ }
347
+ export async function applyBuiltInScaffoldProjectFiles({ projectDir, templateDir, templateId, variables, artifacts, codeArtifacts, readmeContent, gitignoreContent, allowExistingDir = false, packageManager, withMigrationUi = false, withTestPreset = false, withWpEnv = false, noInstall = false, installDependencies, }) {
348
+ await ensureDirectory(projectDir, allowExistingDir);
349
+ await copyInterpolatedDirectory(templateDir, projectDir, variables);
350
+ if (codeArtifacts && codeArtifacts.length > 0) {
351
+ await writeBuiltInCodeArtifacts(projectDir, codeArtifacts);
352
+ }
353
+ if (artifacts && artifacts.length > 0) {
354
+ await writeBuiltInStructuralArtifacts(projectDir, artifacts);
355
+ }
356
+ await writeStarterManifestFiles(projectDir, templateId, variables, artifacts);
357
+ await seedBuiltInPersistenceArtifacts(projectDir, templateId, variables);
358
+ await applyLocalDevPresetFiles({
359
+ projectDir,
360
+ variables,
361
+ withTestPreset,
362
+ withWpEnv,
363
+ });
364
+ if (withMigrationUi) {
365
+ await applyMigrationUiCapability({
366
+ packageManager,
367
+ projectDir,
368
+ templateId,
369
+ variables,
370
+ });
371
+ }
372
+ const readmePath = path.join(projectDir, "README.md");
373
+ if (!fs.existsSync(readmePath)) {
374
+ await fsp.writeFile(readmePath, readmeContent ??
375
+ buildReadme(templateId, variables, packageManager, {
376
+ withMigrationUi,
377
+ withTestPreset,
378
+ withWpEnv,
379
+ }), "utf8");
380
+ }
381
+ const gitignorePath = path.join(projectDir, ".gitignore");
382
+ const existingGitignore = fs.existsSync(gitignorePath)
383
+ ? await fsp.readFile(gitignorePath, "utf8")
384
+ : "";
385
+ await fsp.writeFile(gitignorePath, mergeTextLines(gitignoreContent ?? buildGitignore(), existingGitignore), "utf8");
386
+ await normalizePackageJson(projectDir, packageManager);
387
+ await applyGeneratedProjectDxPackageJson({
388
+ compoundPersistenceEnabled: variables.compoundPersistenceEnabled === "true",
389
+ packageManager,
390
+ projectDir,
391
+ templateId,
392
+ withTestPreset,
393
+ withWpEnv,
394
+ });
395
+ await normalizePackageManagerFiles(projectDir, packageManager);
396
+ await removeUnexpectedLockfiles(projectDir, packageManager);
397
+ await replaceTextRecursively(projectDir, packageManager);
398
+ if (!noInstall) {
399
+ const installer = installDependencies ?? defaultInstallDependencies;
400
+ await installer({
401
+ projectDir,
402
+ packageManager,
403
+ });
404
+ }
405
+ }
@@ -0,0 +1,34 @@
1
+ export interface ResolvedScaffoldIdentifiers {
2
+ namespace: string;
3
+ phpPrefix: string;
4
+ slug: string;
5
+ textDomain: string;
6
+ }
7
+ export declare function validateBlockSlug(input: string): true | string;
8
+ export declare function validateNamespace(input: string): true | string;
9
+ export declare function validateTextDomain(input: string): true | string;
10
+ export declare function validatePhpPrefix(input: string): true | string;
11
+ export declare function assertValidIdentifier(label: string, value: string, validate: (value: string) => true | string): string;
12
+ export declare function normalizeBlockSlug(input: string): string;
13
+ export declare function resolveValidatedBlockSlug(value: string): string;
14
+ export declare function resolveValidatedNamespace(value: string): string;
15
+ export declare function resolveValidatedTextDomain(value: string): string;
16
+ export declare function resolveValidatedPhpPrefix(value: string): string;
17
+ /**
18
+ * Builds the generated WordPress wrapper CSS class for a scaffolded block.
19
+ *
20
+ * Returns `wp-block-{namespace}-{slug}` when a non-empty namespace is present,
21
+ * or `wp-block-{slug}` when the namespace is empty or undefined. When the
22
+ * normalized namespace equals the normalized slug, appends `-block` so the
23
+ * generated class avoids repeated namespace segments without colliding with the
24
+ * default core wrapper classes. Both inputs are normalized and validated with
25
+ * the same scaffold identifier rules used for block names.
26
+ */
27
+ export declare function buildBlockCssClassName(namespace: string | undefined, slug: string): string;
28
+ export declare function buildFrontendCssClassName(blockCssClassName: string): string;
29
+ export declare function resolveScaffoldIdentifiers({ namespace, phpPrefix, slug, textDomain, }: {
30
+ namespace: string;
31
+ phpPrefix?: string;
32
+ slug: string;
33
+ textDomain?: string;
34
+ }): ResolvedScaffoldIdentifiers;
@@ -0,0 +1,82 @@
1
+ import { toKebabCase, toSnakeCase, } from "./string-case.js";
2
+ const BLOCK_SLUG_PATTERN = /^[a-z][a-z0-9-]*$/;
3
+ const PHP_PREFIX_PATTERN = /^[a-z_][a-z0-9_]*$/;
4
+ const PHP_PREFIX_MAX_LENGTH = 50;
5
+ export function validateBlockSlug(input) {
6
+ return BLOCK_SLUG_PATTERN.test(input) || "Use lowercase letters, numbers, and hyphens only";
7
+ }
8
+ export function validateNamespace(input) {
9
+ return BLOCK_SLUG_PATTERN.test(toKebabCase(input))
10
+ ? true
11
+ : "Use lowercase letters, numbers, and hyphens only";
12
+ }
13
+ export function validateTextDomain(input) {
14
+ return BLOCK_SLUG_PATTERN.test(toKebabCase(input))
15
+ ? true
16
+ : "Use lowercase letters, numbers, and hyphens only";
17
+ }
18
+ export function validatePhpPrefix(input) {
19
+ const normalizedPrefix = toSnakeCase(input);
20
+ if (normalizedPrefix.length > PHP_PREFIX_MAX_LENGTH) {
21
+ return `Use ${PHP_PREFIX_MAX_LENGTH} characters or fewer to keep generated database identifiers within MySQL limits`;
22
+ }
23
+ return PHP_PREFIX_PATTERN.test(normalizedPrefix)
24
+ ? true
25
+ : "Use letters, numbers, and underscores only, starting with a letter";
26
+ }
27
+ export function assertValidIdentifier(label, value, validate) {
28
+ const result = validate(value);
29
+ if (result !== true) {
30
+ throw new Error(typeof result === "string" ? `${label}: ${result}` : `${label} is invalid`);
31
+ }
32
+ return value;
33
+ }
34
+ export function normalizeBlockSlug(input) {
35
+ return toKebabCase(input);
36
+ }
37
+ export function resolveValidatedBlockSlug(value) {
38
+ return assertValidIdentifier("Block slug", normalizeBlockSlug(value), validateBlockSlug);
39
+ }
40
+ export function resolveValidatedNamespace(value) {
41
+ return assertValidIdentifier("Namespace", toKebabCase(value), validateNamespace);
42
+ }
43
+ export function resolveValidatedTextDomain(value) {
44
+ return assertValidIdentifier("Text domain", toKebabCase(value), validateTextDomain);
45
+ }
46
+ export function resolveValidatedPhpPrefix(value) {
47
+ return assertValidIdentifier("PHP prefix", toSnakeCase(value), validatePhpPrefix);
48
+ }
49
+ /**
50
+ * Builds the generated WordPress wrapper CSS class for a scaffolded block.
51
+ *
52
+ * Returns `wp-block-{namespace}-{slug}` when a non-empty namespace is present,
53
+ * or `wp-block-{slug}` when the namespace is empty or undefined. When the
54
+ * normalized namespace equals the normalized slug, appends `-block` so the
55
+ * generated class avoids repeated namespace segments without colliding with the
56
+ * default core wrapper classes. Both inputs are normalized and validated with
57
+ * the same scaffold identifier rules used for block names.
58
+ */
59
+ export function buildBlockCssClassName(namespace, slug) {
60
+ const normalizedSlug = resolveValidatedBlockSlug(slug);
61
+ const normalizedNamespace = typeof namespace === "string" && namespace.trim().length > 0
62
+ ? resolveValidatedNamespace(namespace)
63
+ : "";
64
+ if (normalizedNamespace === normalizedSlug) {
65
+ return `wp-block-${normalizedSlug}-block`;
66
+ }
67
+ return normalizedNamespace.length > 0
68
+ ? `wp-block-${normalizedNamespace}-${normalizedSlug}`
69
+ : `wp-block-${normalizedSlug}`;
70
+ }
71
+ export function buildFrontendCssClassName(blockCssClassName) {
72
+ return `${blockCssClassName}-frontend`;
73
+ }
74
+ export function resolveScaffoldIdentifiers({ namespace, phpPrefix, slug, textDomain, }) {
75
+ const normalizedSlug = resolveValidatedBlockSlug(slug);
76
+ return {
77
+ namespace: resolveValidatedNamespace(namespace),
78
+ phpPrefix: resolveValidatedPhpPrefix(phpPrefix ?? normalizedSlug),
79
+ slug: normalizedSlug,
80
+ textDomain: resolveValidatedTextDomain(textDomain ?? normalizedSlug),
81
+ };
82
+ }
@@ -15,6 +15,7 @@ import { BUILTIN_BLOCK_METADATA_VERSION, COMPOUND_CHILD_BLOCK_METADATA_DEFAULTS,
15
15
  import { copyInterpolatedDirectory } from "./template-render.js";
16
16
  import { PROJECT_TOOLS_PACKAGE_ROOT, TEMPLATE_IDS, getTemplateById, isBuiltInTemplateId, } from "./template-registry.js";
17
17
  import { resolveTemplateSource } from "./template-source.js";
18
+ import { BlockGeneratorService, buildTemplateVariablesFromBlockSpec, createBuiltInBlockSpec, } from "./block-generator-service.js";
18
19
  const BLOCK_SLUG_PATTERN = /^[a-z][a-z0-9-]*$/;
19
20
  const PHP_PREFIX_PATTERN = /^[a-z_][a-z0-9_]*$/;
20
21
  const PHP_PREFIX_MAX_LENGTH = 50;
@@ -215,6 +216,14 @@ export async function collectScaffoldAnswers({ projectName, templateId, yes = fa
215
216
  };
216
217
  }
217
218
  export function getTemplateVariables(templateId, answers) {
219
+ if (isBuiltInTemplateId(templateId)) {
220
+ return buildTemplateVariablesFromBlockSpec(createBuiltInBlockSpec({
221
+ answers,
222
+ dataStorageMode: answers.dataStorageMode,
223
+ persistencePolicy: answers.persistencePolicy,
224
+ templateId,
225
+ }));
226
+ }
218
227
  const { apiClientPackageVersion, blockRuntimePackageVersion, blockTypesPackageVersion, projectToolsPackageVersion, restPackageVersion, } = getPackageVersions();
219
228
  const template = isBuiltInTemplateId(templateId) ? getTemplateById(templateId) : null;
220
229
  const metadataDefaults = isBuiltInTemplateId(templateId)
@@ -638,6 +647,30 @@ export async function scaffoldProject({ projectDir, templateId, answers, dataSto
638
647
  const resolvedTemplateId = normalizeTemplateSelection(templateId);
639
648
  const resolvedPackageManager = getPackageManager(packageManager).id;
640
649
  const isBuiltInTemplate = isBuiltInTemplateId(resolvedTemplateId);
650
+ if (isBuiltInTemplate) {
651
+ const blockGeneratorService = new BlockGeneratorService();
652
+ const plan = await blockGeneratorService.plan({
653
+ allowExistingDir,
654
+ answers,
655
+ cwd,
656
+ dataStorageMode: dataStorageMode ?? answers.dataStorageMode,
657
+ noInstall,
658
+ packageManager: resolvedPackageManager,
659
+ persistencePolicy: persistencePolicy ?? answers.persistencePolicy,
660
+ projectDir,
661
+ templateId: resolvedTemplateId,
662
+ variant,
663
+ withMigrationUi,
664
+ withTestPreset,
665
+ withWpEnv,
666
+ });
667
+ const validated = await blockGeneratorService.validate({ plan });
668
+ const rendered = await blockGeneratorService.render({ validated });
669
+ return blockGeneratorService.apply({
670
+ installDependencies,
671
+ rendered,
672
+ });
673
+ }
641
674
  const variables = getTemplateVariables(resolvedTemplateId, {
642
675
  ...answers,
643
676
  dataStorageMode: dataStorageMode ?? answers.dataStorageMode,
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Scaffold-time starter manifest builders for generated projects.
3
3
  *
4
- * These helpers create placeholder `typia.manifest.json` artifacts so fresh
5
- * scaffolds can resolve runtime imports before the first canonical sync run.
4
+ * These helpers now reuse the Phase 2 built-in artifact model so generated
5
+ * `types.ts`, `block.json`, and starter `typia.manifest.json` all share the
6
+ * same attribute metadata source of truth.
6
7
  */
7
8
  import type { ManifestDocument } from "./migration-types.js";
8
9
  import type { ScaffoldTemplateVariables } from "./scaffold.js";