@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.
- package/README.md +43 -0
- package/dist/cli.js +2492 -0
- package/dist/runtime/cli-core.js +222 -0
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/migration-constants.js +14 -0
- package/dist/runtime/migration-diff.js +521 -0
- package/dist/runtime/migration-fixtures.js +89 -0
- package/dist/runtime/migration-manifest.js +129 -0
- package/dist/runtime/migration-project.js +167 -0
- package/dist/runtime/migration-render.js +267 -0
- package/dist/runtime/migration-types.js +1 -0
- package/dist/runtime/migration-utils.js +184 -0
- package/dist/runtime/migrations.js +232 -0
- package/dist/runtime/package-managers.js +135 -0
- package/dist/runtime/scaffold.js +334 -0
- package/dist/runtime/template-registry.js +75 -0
- package/package.json +65 -0
- package/templates/advanced/README.md.mustache +150 -0
- package/templates/advanced/block.json.mustache +43 -0
- package/templates/advanced/index.js +21 -0
- package/templates/advanced/package.json.mustache +47 -0
- package/templates/advanced/render.php.mustache +83 -0
- package/templates/advanced/scripts/lib/typia-metadata-core.ts +1413 -0
- package/templates/advanced/scripts/sync-types-to-block-json.ts.mustache +32 -0
- package/templates/advanced/src/admin/migration-dashboard.tsx.mustache +315 -0
- package/templates/advanced/src/components/ErrorBoundary.tsx.mustache +47 -0
- package/templates/advanced/src/deprecated.ts.mustache +2 -0
- package/templates/advanced/src/edit.tsx.mustache +97 -0
- package/templates/advanced/src/hooks/useDebounce.ts.mustache +20 -0
- package/templates/advanced/src/hooks/useLocalStorage.ts.mustache +31 -0
- package/templates/advanced/src/hooks.ts.mustache +56 -0
- package/templates/advanced/src/index.tsx.mustache +18 -0
- package/templates/advanced/src/migration-detector.ts.mustache +9 -0
- package/templates/advanced/src/migrations/config.ts.mustache +8 -0
- package/templates/advanced/src/migrations/examples/rename-transform-union/README.md.mustache +23 -0
- package/templates/advanced/src/migrations/examples/rename-transform-union/fixture.example.json.mustache +36 -0
- package/templates/advanced/src/migrations/examples/rename-transform-union/rule.example.ts.mustache +47 -0
- package/templates/advanced/src/migrations/fixtures/README.md.mustache +3 -0
- package/templates/advanced/src/migrations/generated/deprecated.ts.mustache +3 -0
- package/templates/advanced/src/migrations/generated/registry.ts.mustache +9 -0
- package/templates/advanced/src/migrations/generated/verify.ts.mustache +1 -0
- package/templates/advanced/src/migrations/helpers.ts.mustache +354 -0
- package/templates/advanced/src/migrations/index.ts.mustache +616 -0
- package/templates/advanced/src/migrations/rules/README.md.mustache +3 -0
- package/templates/advanced/src/migrations/versions/README.md.mustache +3 -0
- package/templates/advanced/src/save.tsx.mustache +12 -0
- package/templates/advanced/src/style.scss.mustache +84 -0
- package/templates/advanced/src/types.ts.mustache +46 -0
- package/templates/advanced/src/utils/classnames.ts.mustache +51 -0
- package/templates/advanced/src/utils/debounce.ts.mustache +37 -0
- package/templates/advanced/src/utils/index.ts.mustache +7 -0
- package/templates/advanced/src/utils/uuid.ts.mustache +17 -0
- package/templates/advanced/src/validators.ts.mustache +39 -0
- package/templates/advanced/src/view.ts.mustache +59 -0
- package/templates/advanced/tsconfig.json.mustache +20 -0
- package/templates/advanced/webpack.config.js.mustache +95 -0
- package/templates/basic/package.json.mustache +39 -0
- package/templates/basic/scripts/lib/typia-metadata-core.ts +1413 -0
- package/templates/basic/scripts/sync-types-to-block-json.ts +25 -0
- package/templates/basic/src/block.json +51 -0
- package/templates/basic/src/edit.tsx +85 -0
- package/templates/basic/src/hooks.ts +75 -0
- package/templates/basic/src/index.tsx +37 -0
- package/templates/basic/src/save.tsx +27 -0
- package/templates/basic/src/style.scss +42 -0
- package/templates/basic/src/types.ts +48 -0
- package/templates/basic/src/validators.ts +39 -0
- package/templates/basic/tsconfig.json +20 -0
- package/templates/basic/webpack.config.js +89 -0
- package/templates/full/package.json.mustache +40 -0
- package/templates/full/scripts/lib/typia-metadata-core.ts +1413 -0
- package/templates/full/scripts/sync-types-to-block-json.ts.mustache +32 -0
- package/templates/full/src/block.json.mustache +120 -0
- package/templates/full/src/edit.tsx.mustache +300 -0
- package/templates/full/src/editor.scss.mustache +251 -0
- package/templates/full/src/hooks.ts.mustache +141 -0
- package/templates/full/src/index.tsx.mustache +27 -0
- package/templates/full/src/save.tsx.mustache +39 -0
- package/templates/full/src/style.scss.mustache +224 -0
- package/templates/full/src/types.ts.mustache +35 -0
- package/templates/full/src/validators.ts.mustache +84 -0
- package/templates/full/tsconfig.json.mustache +20 -0
- package/templates/full/webpack.config.js.mustache +89 -0
- package/templates/interactivity/package.json.mustache +41 -0
- package/templates/interactivity/scripts/lib/typia-metadata-core.ts +1413 -0
- package/templates/interactivity/scripts/sync-types-to-block-json.ts.mustache +32 -0
- package/templates/interactivity/src/block.json.mustache +74 -0
- package/templates/interactivity/src/edit.tsx.mustache +206 -0
- package/templates/interactivity/src/index.tsx.mustache +20 -0
- package/templates/interactivity/src/interactivity.ts.mustache +183 -0
- package/templates/interactivity/src/save.tsx.mustache +87 -0
- package/templates/interactivity/src/style.scss.mustache +60 -0
- package/templates/interactivity/src/types.ts.mustache +30 -0
- package/templates/interactivity/tsconfig.json.mustache +20 -0
- 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
|
+
};
|