@wp-typia/create 0.1.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 (95) hide show
  1. package/README.md +43 -0
  2. package/dist/cli.js +2492 -0
  3. package/dist/runtime/cli-core.js +222 -0
  4. package/dist/runtime/index.js +4 -0
  5. package/dist/runtime/migration-constants.js +14 -0
  6. package/dist/runtime/migration-diff.js +521 -0
  7. package/dist/runtime/migration-fixtures.js +89 -0
  8. package/dist/runtime/migration-manifest.js +129 -0
  9. package/dist/runtime/migration-project.js +167 -0
  10. package/dist/runtime/migration-render.js +267 -0
  11. package/dist/runtime/migration-types.js +1 -0
  12. package/dist/runtime/migration-utils.js +184 -0
  13. package/dist/runtime/migrations.js +232 -0
  14. package/dist/runtime/package-managers.js +135 -0
  15. package/dist/runtime/scaffold.js +334 -0
  16. package/dist/runtime/template-registry.js +75 -0
  17. package/package.json +65 -0
  18. package/templates/advanced/README.md.mustache +150 -0
  19. package/templates/advanced/block.json.mustache +43 -0
  20. package/templates/advanced/index.js +21 -0
  21. package/templates/advanced/package.json.mustache +47 -0
  22. package/templates/advanced/render.php.mustache +83 -0
  23. package/templates/advanced/scripts/lib/typia-metadata-core.ts +1413 -0
  24. package/templates/advanced/scripts/sync-types-to-block-json.ts.mustache +32 -0
  25. package/templates/advanced/src/admin/migration-dashboard.tsx.mustache +315 -0
  26. package/templates/advanced/src/components/ErrorBoundary.tsx.mustache +47 -0
  27. package/templates/advanced/src/deprecated.ts.mustache +2 -0
  28. package/templates/advanced/src/edit.tsx.mustache +97 -0
  29. package/templates/advanced/src/hooks/useDebounce.ts.mustache +20 -0
  30. package/templates/advanced/src/hooks/useLocalStorage.ts.mustache +31 -0
  31. package/templates/advanced/src/hooks.ts.mustache +56 -0
  32. package/templates/advanced/src/index.tsx.mustache +18 -0
  33. package/templates/advanced/src/migration-detector.ts.mustache +9 -0
  34. package/templates/advanced/src/migrations/config.ts.mustache +8 -0
  35. package/templates/advanced/src/migrations/examples/rename-transform-union/README.md.mustache +23 -0
  36. package/templates/advanced/src/migrations/examples/rename-transform-union/fixture.example.json.mustache +36 -0
  37. package/templates/advanced/src/migrations/examples/rename-transform-union/rule.example.ts.mustache +47 -0
  38. package/templates/advanced/src/migrations/fixtures/README.md.mustache +3 -0
  39. package/templates/advanced/src/migrations/generated/deprecated.ts.mustache +3 -0
  40. package/templates/advanced/src/migrations/generated/registry.ts.mustache +9 -0
  41. package/templates/advanced/src/migrations/generated/verify.ts.mustache +1 -0
  42. package/templates/advanced/src/migrations/helpers.ts.mustache +354 -0
  43. package/templates/advanced/src/migrations/index.ts.mustache +616 -0
  44. package/templates/advanced/src/migrations/rules/README.md.mustache +3 -0
  45. package/templates/advanced/src/migrations/versions/README.md.mustache +3 -0
  46. package/templates/advanced/src/save.tsx.mustache +12 -0
  47. package/templates/advanced/src/style.scss.mustache +84 -0
  48. package/templates/advanced/src/types.ts.mustache +46 -0
  49. package/templates/advanced/src/utils/classnames.ts.mustache +51 -0
  50. package/templates/advanced/src/utils/debounce.ts.mustache +37 -0
  51. package/templates/advanced/src/utils/index.ts.mustache +7 -0
  52. package/templates/advanced/src/utils/uuid.ts.mustache +17 -0
  53. package/templates/advanced/src/validators.ts.mustache +39 -0
  54. package/templates/advanced/src/view.ts.mustache +59 -0
  55. package/templates/advanced/tsconfig.json.mustache +20 -0
  56. package/templates/advanced/webpack.config.js.mustache +95 -0
  57. package/templates/basic/package.json.mustache +39 -0
  58. package/templates/basic/scripts/lib/typia-metadata-core.ts +1413 -0
  59. package/templates/basic/scripts/sync-types-to-block-json.ts +25 -0
  60. package/templates/basic/src/block.json +51 -0
  61. package/templates/basic/src/edit.tsx +85 -0
  62. package/templates/basic/src/hooks.ts +75 -0
  63. package/templates/basic/src/index.tsx +37 -0
  64. package/templates/basic/src/save.tsx +27 -0
  65. package/templates/basic/src/style.scss +42 -0
  66. package/templates/basic/src/types.ts +48 -0
  67. package/templates/basic/src/validators.ts +39 -0
  68. package/templates/basic/tsconfig.json +20 -0
  69. package/templates/basic/webpack.config.js +89 -0
  70. package/templates/full/package.json.mustache +40 -0
  71. package/templates/full/scripts/lib/typia-metadata-core.ts +1413 -0
  72. package/templates/full/scripts/sync-types-to-block-json.ts.mustache +32 -0
  73. package/templates/full/src/block.json.mustache +120 -0
  74. package/templates/full/src/edit.tsx.mustache +300 -0
  75. package/templates/full/src/editor.scss.mustache +251 -0
  76. package/templates/full/src/hooks.ts.mustache +141 -0
  77. package/templates/full/src/index.tsx.mustache +27 -0
  78. package/templates/full/src/save.tsx.mustache +39 -0
  79. package/templates/full/src/style.scss.mustache +224 -0
  80. package/templates/full/src/types.ts.mustache +35 -0
  81. package/templates/full/src/validators.ts.mustache +84 -0
  82. package/templates/full/tsconfig.json.mustache +20 -0
  83. package/templates/full/webpack.config.js.mustache +89 -0
  84. package/templates/interactivity/package.json.mustache +41 -0
  85. package/templates/interactivity/scripts/lib/typia-metadata-core.ts +1413 -0
  86. package/templates/interactivity/scripts/sync-types-to-block-json.ts.mustache +32 -0
  87. package/templates/interactivity/src/block.json.mustache +74 -0
  88. package/templates/interactivity/src/edit.tsx.mustache +206 -0
  89. package/templates/interactivity/src/index.tsx.mustache +20 -0
  90. package/templates/interactivity/src/interactivity.ts.mustache +183 -0
  91. package/templates/interactivity/src/save.tsx.mustache +87 -0
  92. package/templates/interactivity/src/style.scss.mustache +60 -0
  93. package/templates/interactivity/src/types.ts.mustache +30 -0
  94. package/templates/interactivity/tsconfig.json.mustache +20 -0
  95. package/templates/interactivity/webpack.config.js.mustache +89 -0
@@ -0,0 +1,334 @@
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, formatInstallCommand, formatRunScript, getPackageManager, transformPackageManagerText, } from "./package-managers.js";
6
+ import { TEMPLATE_IDS, getTemplateById } from "./template-registry.js";
7
+ const BLOCK_SLUG_PATTERN = /^[a-z][a-z0-9-]*$/;
8
+ const LOCKFILES = {
9
+ bun: ["bun.lock", "bun.lockb"],
10
+ npm: ["package-lock.json"],
11
+ pnpm: ["pnpm-lock.yaml"],
12
+ yarn: ["yarn.lock"],
13
+ };
14
+ function toKebabCase(input) {
15
+ return input
16
+ .trim()
17
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
18
+ .replace(/[^A-Za-z0-9]+/g, "-")
19
+ .replace(/^-+|-+$/g, "")
20
+ .replace(/-{2,}/g, "-")
21
+ .toLowerCase();
22
+ }
23
+ function toSnakeCase(input) {
24
+ return toKebabCase(input).replace(/-/g, "_");
25
+ }
26
+ function toPascalCase(input) {
27
+ return toKebabCase(input)
28
+ .split("-")
29
+ .filter(Boolean)
30
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
31
+ .join("");
32
+ }
33
+ function toTitle(input) {
34
+ return toKebabCase(input)
35
+ .split("-")
36
+ .filter(Boolean)
37
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
38
+ .join(" ");
39
+ }
40
+ function validateBlockSlug(input) {
41
+ return BLOCK_SLUG_PATTERN.test(input) || "Use lowercase letters, numbers, and hyphens only";
42
+ }
43
+ export function detectAuthor() {
44
+ try {
45
+ return (execSync("git config user.name", {
46
+ encoding: "utf8",
47
+ stdio: ["ignore", "pipe", "ignore"],
48
+ }).trim() || "Your Name");
49
+ }
50
+ catch {
51
+ return "Your Name";
52
+ }
53
+ }
54
+ export function getDefaultAnswers(projectName, templateId) {
55
+ const template = getTemplateById(templateId);
56
+ const slugDefault = toKebabCase(projectName || "my-wp-typia-block");
57
+ return {
58
+ author: detectAuthor(),
59
+ description: template.description,
60
+ namespace: "create-block",
61
+ slug: slugDefault,
62
+ title: toTitle(slugDefault),
63
+ };
64
+ }
65
+ export async function resolveTemplateId({ templateId, yes = false, isInteractive = false, selectTemplate, }) {
66
+ if (templateId) {
67
+ return getTemplateById(templateId).id;
68
+ }
69
+ if (yes) {
70
+ return "basic";
71
+ }
72
+ if (!isInteractive || !selectTemplate) {
73
+ throw new Error(`Template is required in non-interactive mode. Use --template <${TEMPLATE_IDS.join("|")}>.`);
74
+ }
75
+ return selectTemplate();
76
+ }
77
+ export async function resolvePackageManagerId({ packageManager, yes = false, isInteractive = false, selectPackageManager, }) {
78
+ if (packageManager) {
79
+ return getPackageManager(packageManager).id;
80
+ }
81
+ if (yes) {
82
+ throw new Error(`Package manager is required when using --yes. Use --package-manager <${PACKAGE_MANAGER_IDS.join("|")}>.`);
83
+ }
84
+ if (!isInteractive || !selectPackageManager) {
85
+ throw new Error(`Package manager is required in non-interactive mode. Use --package-manager <${PACKAGE_MANAGER_IDS.join("|")}>.`);
86
+ }
87
+ return selectPackageManager();
88
+ }
89
+ export async function collectScaffoldAnswers({ projectName, templateId, yes = false, promptText, }) {
90
+ const defaults = getDefaultAnswers(projectName, templateId);
91
+ if (yes) {
92
+ return defaults;
93
+ }
94
+ if (!promptText) {
95
+ throw new Error("Interactive answers require a promptText callback.");
96
+ }
97
+ const slug = toKebabCase(await promptText("Block slug", defaults.slug, validateBlockSlug));
98
+ return {
99
+ author: await promptText("Author", defaults.author),
100
+ description: await promptText("Description", defaults.description),
101
+ namespace: await promptText("Namespace", defaults.namespace),
102
+ slug,
103
+ title: await promptText("Block title", toTitle(slug)),
104
+ };
105
+ }
106
+ export function getTemplateVariables(templateId, answers) {
107
+ const template = getTemplateById(templateId);
108
+ const slug = toKebabCase(answers.slug);
109
+ const slugSnakeCase = toSnakeCase(slug);
110
+ const pascalCase = toPascalCase(slug);
111
+ const title = answers.title.trim();
112
+ const namespace = answers.namespace.trim();
113
+ const description = answers.description.trim();
114
+ return {
115
+ author: answers.author.trim(),
116
+ category: template.defaultCategory,
117
+ cssClassName: `wp-block-${slug}`,
118
+ dashCase: slug,
119
+ dashicon: "smiley",
120
+ description,
121
+ keyword: slug.replace(/-/g, " "),
122
+ namespace,
123
+ needsMigration: "{{needsMigration}}",
124
+ pascalCase,
125
+ slug,
126
+ slugCamelCase: pascalCase.charAt(0).toLowerCase() + pascalCase.slice(1),
127
+ slugKebabCase: slug,
128
+ slugSnakeCase,
129
+ textDomain: slugSnakeCase,
130
+ textdomain: slugSnakeCase,
131
+ title,
132
+ titleCase: pascalCase,
133
+ };
134
+ }
135
+ function replaceVariables(content, variables) {
136
+ return content.replace(/\{\{([^}]+)\}\}/g, (match, rawKey) => {
137
+ const key = rawKey.trim();
138
+ return Object.prototype.hasOwnProperty.call(variables, key) ? variables[key] : match;
139
+ });
140
+ }
141
+ async function ensureDirectory(targetDir, allowExisting = false) {
142
+ if (!fs.existsSync(targetDir)) {
143
+ await fsp.mkdir(targetDir, { recursive: true });
144
+ return;
145
+ }
146
+ if (allowExisting) {
147
+ return;
148
+ }
149
+ const entries = await fsp.readdir(targetDir);
150
+ if (entries.length > 0) {
151
+ throw new Error(`Target directory is not empty: ${targetDir}`);
152
+ }
153
+ }
154
+ async function copyTemplateDir(sourceDir, targetDir, variables) {
155
+ const entries = await fsp.readdir(sourceDir, { withFileTypes: true });
156
+ for (const entry of entries) {
157
+ const sourcePath = path.join(sourceDir, entry.name);
158
+ const destinationName = entry.name.endsWith(".mustache")
159
+ ? entry.name.slice(0, -".mustache".length)
160
+ : entry.name;
161
+ const destinationPath = path.join(targetDir, destinationName);
162
+ if (entry.isDirectory()) {
163
+ await fsp.mkdir(destinationPath, { recursive: true });
164
+ await copyTemplateDir(sourcePath, destinationPath, variables);
165
+ continue;
166
+ }
167
+ const content = await fsp.readFile(sourcePath, "utf8");
168
+ await fsp.writeFile(destinationPath, replaceVariables(content, variables), "utf8");
169
+ }
170
+ }
171
+ function buildReadme(templateId, variables, packageManager) {
172
+ return `# ${variables.title}
173
+
174
+ ${variables.description}
175
+
176
+ ## Template
177
+
178
+ ${templateId}
179
+
180
+ ## Development
181
+
182
+ \`\`\`bash
183
+ ${formatInstallCommand(packageManager)}
184
+ ${formatRunScript(packageManager, "start")}
185
+ \`\`\`
186
+
187
+ ## Build
188
+
189
+ \`\`\`bash
190
+ ${formatRunScript(packageManager, "build")}
191
+ \`\`\`
192
+
193
+ ## Type Sync
194
+
195
+ \`\`\`bash
196
+ ${formatRunScript(packageManager, "sync-types")}
197
+ \`\`\`
198
+
199
+ \`src/types.ts\` remains the source of truth for \`block.json\` and \`typia.manifest.json\`.
200
+ `;
201
+ }
202
+ function buildGitignore() {
203
+ return `# Dependencies
204
+ node_modules/
205
+ .yarn/
206
+ .pnp.*
207
+
208
+ # Build
209
+ build/
210
+ dist/
211
+
212
+ # Editor
213
+ .vscode/
214
+ .idea/
215
+
216
+ # OS
217
+ .DS_Store
218
+ Thumbs.db
219
+
220
+ # WordPress
221
+ *.log
222
+ .wp-env/
223
+ `;
224
+ }
225
+ async function normalizePackageManagerFiles(targetDir, packageManagerId) {
226
+ const yarnRcPath = path.join(targetDir, ".yarnrc.yml");
227
+ if (packageManagerId === "yarn") {
228
+ await fsp.writeFile(yarnRcPath, "nodeLinker: node-modules\n", "utf8");
229
+ return;
230
+ }
231
+ if (fs.existsSync(yarnRcPath)) {
232
+ await fsp.rm(yarnRcPath, { force: true });
233
+ }
234
+ }
235
+ async function normalizePackageJson(targetDir, packageManagerId) {
236
+ const packageJsonPath = path.join(targetDir, "package.json");
237
+ if (!fs.existsSync(packageJsonPath)) {
238
+ return;
239
+ }
240
+ const packageManager = getPackageManager(packageManagerId);
241
+ const packageJson = JSON.parse(await fsp.readFile(packageJsonPath, "utf8"));
242
+ packageJson.packageManager = packageManager.packageManagerField;
243
+ if (packageJson.scripts) {
244
+ for (const [key, value] of Object.entries(packageJson.scripts)) {
245
+ if (typeof value === "string") {
246
+ packageJson.scripts[key] = transformPackageManagerText(value, packageManagerId);
247
+ }
248
+ }
249
+ }
250
+ await fsp.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, "\t")}\n`, "utf8");
251
+ }
252
+ async function removeUnexpectedLockfiles(targetDir, packageManagerId) {
253
+ const keep = new Set(LOCKFILES[packageManagerId] ?? []);
254
+ const allLockfiles = Object.values(LOCKFILES).flat();
255
+ await Promise.all(allLockfiles.map(async (filename) => {
256
+ if (keep.has(filename)) {
257
+ return;
258
+ }
259
+ const filePath = path.join(targetDir, filename);
260
+ if (fs.existsSync(filePath)) {
261
+ await fsp.rm(filePath, { force: true });
262
+ }
263
+ }));
264
+ }
265
+ async function replaceTextRecursively(targetDir, packageManagerId) {
266
+ const textExtensions = new Set([
267
+ ".css",
268
+ ".js",
269
+ ".json",
270
+ ".jsx",
271
+ ".md",
272
+ ".php",
273
+ ".scss",
274
+ ".ts",
275
+ ".tsx",
276
+ ".txt",
277
+ ]);
278
+ async function visit(currentPath) {
279
+ const stats = await fsp.stat(currentPath);
280
+ if (stats.isDirectory()) {
281
+ const entries = await fsp.readdir(currentPath);
282
+ for (const entry of entries) {
283
+ await visit(path.join(currentPath, entry));
284
+ }
285
+ return;
286
+ }
287
+ if (path.basename(currentPath) === "package.json" || !textExtensions.has(path.extname(currentPath))) {
288
+ return;
289
+ }
290
+ const content = await fsp.readFile(currentPath, "utf8");
291
+ const nextContent = transformPackageManagerText(content, packageManagerId)
292
+ .replace(/yourusername\/wp-typia-boilerplate/g, "imjlk/wp-typia")
293
+ .replace(/yourusername\/wp-typia/g, "imjlk/wp-typia");
294
+ if (nextContent !== content) {
295
+ await fsp.writeFile(currentPath, nextContent, "utf8");
296
+ }
297
+ }
298
+ await visit(targetDir);
299
+ }
300
+ async function defaultInstallDependencies({ projectDir, packageManager, }) {
301
+ execSync(formatInstallCommand(packageManager), {
302
+ cwd: projectDir,
303
+ stdio: "inherit",
304
+ });
305
+ }
306
+ export async function scaffoldProject({ projectDir, templateId, answers, packageManager, allowExistingDir = false, noInstall = false, installDependencies = undefined, }) {
307
+ const template = getTemplateById(templateId);
308
+ const resolvedPackageManager = getPackageManager(packageManager).id;
309
+ await ensureDirectory(projectDir, allowExistingDir);
310
+ const variables = getTemplateVariables(template.id, answers);
311
+ await copyTemplateDir(template.templateDir, projectDir, variables);
312
+ const readmePath = path.join(projectDir, "README.md");
313
+ if (!fs.existsSync(readmePath)) {
314
+ await fsp.writeFile(readmePath, buildReadme(template.id, variables, resolvedPackageManager), "utf8");
315
+ }
316
+ await fsp.writeFile(path.join(projectDir, ".gitignore"), buildGitignore(), "utf8");
317
+ await normalizePackageJson(projectDir, resolvedPackageManager);
318
+ await normalizePackageManagerFiles(projectDir, resolvedPackageManager);
319
+ await removeUnexpectedLockfiles(projectDir, resolvedPackageManager);
320
+ await replaceTextRecursively(projectDir, resolvedPackageManager);
321
+ if (!noInstall) {
322
+ const installer = installDependencies ?? defaultInstallDependencies;
323
+ await installer({
324
+ projectDir,
325
+ packageManager: resolvedPackageManager,
326
+ });
327
+ }
328
+ return {
329
+ projectDir,
330
+ templateId: template.id,
331
+ packageManager: resolvedPackageManager,
332
+ variables,
333
+ };
334
+ }
@@ -0,0 +1,75 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
+ function resolvePackageRoot(startDir) {
6
+ let currentDir = startDir;
7
+ while (true) {
8
+ const packageJsonPath = path.join(currentDir, "package.json");
9
+ if (fs.existsSync(packageJsonPath)) {
10
+ try {
11
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
12
+ if (packageJson.name === "@wp-typia/create") {
13
+ return currentDir;
14
+ }
15
+ }
16
+ catch {
17
+ // Ignore malformed package.json while walking upward.
18
+ }
19
+ }
20
+ const parentDir = path.dirname(currentDir);
21
+ if (parentDir === currentDir) {
22
+ throw new Error("Unable to resolve the @wp-typia/create package root.");
23
+ }
24
+ currentDir = parentDir;
25
+ }
26
+ }
27
+ const TEMPLATE_ROOT = path.join(resolvePackageRoot(__dirname), "templates");
28
+ export const TEMPLATE_REGISTRY = Object.freeze([
29
+ {
30
+ id: "basic",
31
+ description: "A lightweight WordPress block with Typia validation",
32
+ defaultCategory: "text",
33
+ features: ["Type-safe attributes", "Runtime validation", "Minimal setup"],
34
+ templateDir: path.join(TEMPLATE_ROOT, "basic"),
35
+ },
36
+ {
37
+ id: "full",
38
+ description: "A full-featured WordPress block with Typia validation and utilities",
39
+ defaultCategory: "widgets",
40
+ features: ["Advanced controls", "Custom hooks", "Style options"],
41
+ templateDir: path.join(TEMPLATE_ROOT, "full"),
42
+ },
43
+ {
44
+ id: "interactivity",
45
+ description: "An interactive WordPress block with Typia validation and Interactivity API",
46
+ defaultCategory: "widgets",
47
+ features: ["Interactivity API", "Client-side state", "Event handling"],
48
+ templateDir: path.join(TEMPLATE_ROOT, "interactivity"),
49
+ },
50
+ {
51
+ id: "advanced",
52
+ description: "An advanced WordPress block with Typia validation and migration tooling",
53
+ defaultCategory: "widgets",
54
+ features: ["Migration system", "Version tracking", "Admin dashboard"],
55
+ templateDir: path.join(TEMPLATE_ROOT, "advanced"),
56
+ },
57
+ ]);
58
+ export const TEMPLATE_IDS = TEMPLATE_REGISTRY.map((template) => template.id);
59
+ export function listTemplates() {
60
+ return TEMPLATE_REGISTRY;
61
+ }
62
+ export function getTemplateById(templateId) {
63
+ const template = TEMPLATE_REGISTRY.find((entry) => entry.id === templateId);
64
+ if (!template) {
65
+ throw new Error(`Unknown template "${templateId}". Expected one of: ${TEMPLATE_IDS.join(", ")}`);
66
+ }
67
+ return template;
68
+ }
69
+ export function getTemplateSelectOptions() {
70
+ return TEMPLATE_REGISTRY.map((template) => ({
71
+ label: template.id,
72
+ value: template.id,
73
+ hint: template.features.join(", "),
74
+ }));
75
+ }
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@wp-typia/create",
3
+ "version": "0.1.0",
4
+ "description": "Bun-first scaffolding CLI for WordPress Typia block templates",
5
+ "packageManager": "bun@1.3.10",
6
+ "type": "module",
7
+ "main": "dist/runtime/index.js",
8
+ "exports": {
9
+ ".": "./dist/runtime/index.js",
10
+ "./cli": "./dist/cli.js",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "bin": {
14
+ "wp-typia": "dist/cli.js"
15
+ },
16
+ "files": [
17
+ "dist/",
18
+ "templates/",
19
+ "README.md",
20
+ "package.json"
21
+ ],
22
+ "scripts": {
23
+ "build": "rm -rf dist && bunli build --runtime node && tsc -p tsconfig.runtime.json",
24
+ "dev": "bun run src/cli.ts",
25
+ "test": "bun run build && bunli test --pattern ./tests/create-cli.test.ts && bunli test --pattern ./tests/migrations.test.ts",
26
+ "test:coverage": "bun run build && bunli test --pattern ./tests/create-cli.test.ts --coverage && bunli test --pattern ./tests/migrations.test.ts --coverage && bun test tests/*.test.ts --coverage --coverage-reporter=lcov --coverage-dir=coverage",
27
+ "clean": "rm -rf dist",
28
+ "prepack": "bun run build"
29
+ },
30
+ "keywords": [
31
+ "wordpress",
32
+ "gutenberg",
33
+ "typia",
34
+ "template",
35
+ "scaffold",
36
+ "cli",
37
+ "bun"
38
+ ],
39
+ "author": "imjlk",
40
+ "license": "GPL-2.0-or-later",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/imjlk/wp-typia.git",
44
+ "directory": "packages/create"
45
+ },
46
+ "bugs": {
47
+ "url": "https://github.com/imjlk/wp-typia/issues"
48
+ },
49
+ "homepage": "https://github.com/imjlk/wp-typia/tree/main/packages/create#readme",
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "engines": {
54
+ "node": ">=16.0.0",
55
+ "bun": ">=1.3.10"
56
+ },
57
+ "dependencies": {},
58
+ "devDependencies": {
59
+ "bunli": "^0.8.2",
60
+ "react": "^19.2.0",
61
+ "react-devtools-core": "^7.0.1",
62
+ "typescript": "^5.9.2",
63
+ "ws": "^8.18.0"
64
+ }
65
+ }
@@ -0,0 +1,150 @@
1
+ # {{title}}
2
+
3
+ A WordPress block with **Typia validation**, a **dynamic `render.php` server boundary**, **Interactivity API** support, and **snapshot-based migration tooling**.
4
+
5
+ ## 🚀 Features
6
+
7
+ - **✅ Typia Validation**: Type-safe attributes with compile-time and runtime validation
8
+ - **🧱 Dynamic Rendering**: `render.php` uses the generated `typia-validator.php` at the server boundary
9
+ - **⚡ Interactivity API**: Lightweight frontend interactions
10
+ - **🎯 Auto-Sync**: Types automatically sync to `block.json` and `typia.manifest.json`
11
+ - **🧬 Snapshot Migrations**: Preserve legacy block contracts and scaffold deprecated migrations from Typia manifests
12
+ - **🔧 TypeScript**: Full type safety throughout
13
+
14
+ ## 🏗️ Development
15
+
16
+ ```bash
17
+ # Install dependencies
18
+ bun install
19
+
20
+ # Development with hot reload
21
+ bun run start
22
+
23
+ # Build for production
24
+ bun run build
25
+
26
+ # Type checking
27
+ npx tsc --noEmit
28
+
29
+ # Sync block metadata manually
30
+ bun run sync-types
31
+
32
+ # Bootstrap migration snapshots for the first release
33
+ bun run migration:init
34
+
35
+ # Compare the current schema with an older snapshot
36
+ bun run migration:diff -- --from 1.0.0
37
+
38
+ # Scaffold a legacy-to-current migration edge
39
+ bun run migration:scaffold -- --from 1.0.0
40
+
41
+ # Verify generated migration rules against stored fixtures
42
+ bun run migration:verify
43
+ ```
44
+
45
+ ## 📝 Type System
46
+
47
+ Edit `src/types.ts` to define your block attributes:
48
+
49
+ ```typescript
50
+ export interface {{titleCase}}Attributes {
51
+ content: string & tags.MinLength<1> & tags.Default<"Hello World">;
52
+ isVisible: boolean & tags.Default<true>;
53
+ count: number & tags.Minimum<0> & tags.Maximum<100>;
54
+ }
55
+ ```
56
+
57
+ **`block.json`, `typia.manifest.json`, and `typia-validator.php` are automatically updated** when you run `bun run start` or `bun run build`.
58
+
59
+ ## 🎨 Styling
60
+
61
+ - `src/style.scss` - Frontend and editor styles
62
+ - Block CSS class: `.wp-block-{{slugKebabCase}}`
63
+
64
+ ## ⚡ Interactivity
65
+
66
+ Frontend interactions are defined in `src/view.ts`:
67
+
68
+ ```typescript
69
+ const { state, actions } = store('{{namespace}}/{{slug}}', {
70
+ state: { isActive: false },
71
+ actions: { toggle() { state.isActive = !state.isActive; } }
72
+ });
73
+ ```
74
+
75
+ ## 🧱 Server Boundary
76
+
77
+ The block renders on the server through `render.php`. That file loads the generated `typia-validator.php`, applies defaults, validates the supported subset, and safely returns an empty string when the payload is invalid.
78
+
79
+ ## 🧬 Migration Authoring
80
+
81
+ Generated rule files now expose both `renameMap` and `transforms`:
82
+
83
+ ```ts
84
+ export const renameMap = {
85
+ "content": "headline",
86
+ "settings.label": "settings.title",
87
+ // "linkTarget.url.href": "cta.href",
88
+ };
89
+
90
+ export const transforms = {
91
+ // "count": (legacyValue) => Number(legacyValue ?? 0),
92
+ };
93
+ ```
94
+
95
+ Scaffold now does more than leave TODOs behind:
96
+
97
+ - high-confidence top-level and nested leaf renames are written directly into `renameMap`
98
+ - semantic-risk coercions get suggested transform bodies
99
+ - edge fixtures are generated at `src/migrations/fixtures/<from>-to-<to>.json`
100
+ - `migration:verify` replays every fixture case before it accepts the edge
101
+ - `src/migrations/examples/rename-transform-union/` ships a realistic reference pack that does not affect the active migration graph
102
+
103
+ Recommended flow:
104
+
105
+ 1. update `src/types.ts`
106
+ 2. run `bun run sync-types`
107
+ 3. snapshot the release you want to preserve
108
+ 4. scaffold the legacy edge
109
+ 5. review auto-applied renames and suggested transforms
110
+ 6. adjust nested `renameMap` / `transforms` entries if object or union branch leaves changed
111
+ 6. edit the generated fixture cases if the real legacy payload is richer
112
+ 7. run `bun run migration:verify`
113
+ 8. use the admin dashboard for dry-run previews before batch migration
114
+
115
+ Example:
116
+
117
+ ```bash
118
+ bun run migration:snapshot -- --version 2.0.0
119
+ bun run migration:scaffold -- --from 1.0.0
120
+ # review auto-applied renameMap entries
121
+ # review nested leaf paths like "settings.label" or "linkTarget.url.href"
122
+ # fill suggested transforms if needed
123
+ bun run migration:verify
124
+ ```
125
+
126
+ The dashboard then shows:
127
+
128
+ - changed field paths
129
+ - union branch matches
130
+ - unresolved/manual review badges
131
+ - dry-run previews before any write happens
132
+
133
+ ## 📦 Generated Files
134
+
135
+ - `block.json` - WordPress-facing attribute metadata generated from TypeScript types
136
+ - `typia.manifest.json` - Manifest v2 with default markers and discriminated union metadata
137
+ - `typia-validator.php` - Generated PHP validator for the supported server-side subset
138
+ - `render.php` - Dynamic block rendering entrypoint
139
+ - `typia-migration-registry.php` - PHP-readable snapshot and edge summary
140
+ - `src/migrations/versions/` - Versioned snapshots of legacy block contracts and save implementations
141
+ - `src/migrations/rules/` - Per-edge migration rules generated from Typia manifest diffs
142
+ - `src/migrations/fixtures/` - Edge fixtures used by `migration:verify` and the dashboard preview
143
+ - `src/migrations/examples/` - Reference-only migration examples for rename + transform + union authoring
144
+ - `src/migrations/generated/` - Generated deprecated array, registry, and verification helpers
145
+ - `build/` - Compiled JavaScript and CSS
146
+ - `src/types.ts` - **Edit this to change block attributes**
147
+
148
+ ---
149
+
150
+ *Generated with `@wp-typia/create` and Typia validation*
@@ -0,0 +1,43 @@
1
+ {
2
+ "$schema": "https://schemas.wp.org/trunk/block.json",
3
+ "apiVersion": 3,
4
+ "name": "{{namespace}}/{{slug}}",
5
+ "version": "0.1.0",
6
+ "title": "{{title}}",
7
+ "category": "{{category}}",
8
+ "icon": "{{dashicon}}",
9
+ "description": "{{description}}",
10
+ "keywords": [
11
+ "typia",
12
+ "{{slug}}",
13
+ "custom"
14
+ ],
15
+ "example": {
16
+ "attributes": {
17
+ "id": "Example id",
18
+ "version": 1,
19
+ "className": "Example className",
20
+ "content": "",
21
+ "alignment": "left",
22
+ "isVisible": true
23
+ }
24
+ },
25
+ "supports": {
26
+ "html": false,
27
+ "interactivity": {
28
+ "clientNavigation": true
29
+ }
30
+ },
31
+ "textdomain": "{{textdomain}}",
32
+ "editorScript": "file:./index.js",
33
+ "editorStyle": "file:./index.css",
34
+ "render": "file:./render.php",
35
+ "style": "file:./style-index.css",
36
+ "viewScript": "file:./view.js",
37
+ "attributes": {
38
+ "content": {
39
+ "type": "string",
40
+ "default": ""
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,21 @@
1
+ const { join } = require( 'path' );
2
+
3
+ module.exports = {
4
+ templatesPath: join( __dirname ),
5
+ defaultValues: {
6
+ namespace: 'create-block',
7
+ category: 'widgets',
8
+ dashicon: 'admin-site-alt3',
9
+ textdomain: '',
10
+ editorScript: 'file:./index.js',
11
+ editorStyle: 'file:./index.css',
12
+ style: 'file:./style-index.css',
13
+ viewScript: 'file:./view.js',
14
+ },
15
+ variants: {
16
+ 'typia': {
17
+ title: 'Typia Block',
18
+ description: 'A WordPress block with Typia validation and Interactivity API',
19
+ }
20
+ }
21
+ };