@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,32 @@
|
|
|
1
|
+
import { syncBlockMetadata } from "./lib/typia-metadata-core";
|
|
2
|
+
|
|
3
|
+
async function main() {
|
|
4
|
+
const result = await syncBlockMetadata({
|
|
5
|
+
blockJsonFile: "block.json",
|
|
6
|
+
manifestFile: "typia.manifest.json",
|
|
7
|
+
sourceTypeName: "{{titleCase}}Attributes",
|
|
8
|
+
typesFile: "src/types.ts",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
console.log("✅ block.json, typia.manifest.json, and typia-validator.php were generated from TypeScript types!");
|
|
12
|
+
console.log("📝 Generated attributes:", result.attributeNames);
|
|
13
|
+
|
|
14
|
+
if (result.lossyProjectionWarnings.length > 0) {
|
|
15
|
+
console.warn("⚠️ Some Typia constraints were preserved only in typia.manifest.json:");
|
|
16
|
+
for (const warning of result.lossyProjectionWarnings) {
|
|
17
|
+
console.warn(` - ${warning}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (result.phpGenerationWarnings.length > 0) {
|
|
22
|
+
console.warn("⚠️ Some Typia constraints are not yet enforced by typia-validator.php:");
|
|
23
|
+
for (const warning of result.phpGenerationWarnings) {
|
|
24
|
+
console.warn(` - ${warning}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
main().catch((error) => {
|
|
30
|
+
console.error("❌ Type sync failed:", error);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
});
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { useState } from "@wordpress/element";
|
|
2
|
+
import { Button, Card, CardBody, Notice, Spinner } from "@wordpress/components";
|
|
3
|
+
import { __ } from "@wordpress/i18n";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type BatchMigrationResult,
|
|
7
|
+
type BlockScanResult,
|
|
8
|
+
batchMigrateScanResults,
|
|
9
|
+
generateMigrationReport,
|
|
10
|
+
scanSiteForMigrations,
|
|
11
|
+
} from "../migration-detector";
|
|
12
|
+
|
|
13
|
+
interface MigrationStats {
|
|
14
|
+
needsMigration: number;
|
|
15
|
+
total: number;
|
|
16
|
+
unknown: number;
|
|
17
|
+
versions: Record<string, number>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function collectStats(results: BlockScanResult[]): MigrationStats {
|
|
21
|
+
return results.reduce<MigrationStats>(
|
|
22
|
+
(accumulator, result) => {
|
|
23
|
+
accumulator.total += 1;
|
|
24
|
+
if (result.analysis.needsMigration) {
|
|
25
|
+
accumulator.needsMigration += 1;
|
|
26
|
+
}
|
|
27
|
+
if (result.analysis.currentVersion === "unknown") {
|
|
28
|
+
accumulator.unknown += 1;
|
|
29
|
+
}
|
|
30
|
+
accumulator.versions[result.analysis.currentVersion] =
|
|
31
|
+
(accumulator.versions[result.analysis.currentVersion] ?? 0) + 1;
|
|
32
|
+
return accumulator;
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
needsMigration: 0,
|
|
36
|
+
total: 0,
|
|
37
|
+
unknown: 0,
|
|
38
|
+
versions: {},
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function downloadFile(contents: string, fileName: string, type: string) {
|
|
44
|
+
const blob = new Blob([contents], { type });
|
|
45
|
+
const url = URL.createObjectURL(blob);
|
|
46
|
+
const link = document.createElement("a");
|
|
47
|
+
link.href = url;
|
|
48
|
+
link.download = fileName;
|
|
49
|
+
link.click();
|
|
50
|
+
URL.revokeObjectURL(url);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function formatUnionSummary(result: BlockScanResult["preview"]): string {
|
|
54
|
+
if (result.unionBranches.length === 0) {
|
|
55
|
+
return __("No union branch changes", "{{textdomain}}");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return result.unionBranches
|
|
59
|
+
.map((branch) => `${branch.field}: ${branch.legacyBranch ?? "unknown"} → ${branch.nextBranch ?? "unknown"} (${branch.status})`)
|
|
60
|
+
.join(", ");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function renderJsonPreview(value: unknown) {
|
|
64
|
+
return (
|
|
65
|
+
<pre
|
|
66
|
+
style={{
|
|
67
|
+
background: "#f6f7f7",
|
|
68
|
+
borderRadius: "4px",
|
|
69
|
+
margin: "8px 0 0",
|
|
70
|
+
overflowX: "auto",
|
|
71
|
+
padding: "8px",
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
{JSON.stringify(value, null, 2)}
|
|
75
|
+
</pre>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function MigrationDashboard() {
|
|
80
|
+
const [results, setResults] = useState<BlockScanResult[]>([]);
|
|
81
|
+
const [dryRunResult, setDryRunResult] = useState<BatchMigrationResult | null>(null);
|
|
82
|
+
const [isScanning, setIsScanning] = useState(false);
|
|
83
|
+
const [isMigrating, setIsMigrating] = useState(false);
|
|
84
|
+
const [error, setError] = useState<string | null>(null);
|
|
85
|
+
|
|
86
|
+
const stats = collectStats(results);
|
|
87
|
+
|
|
88
|
+
const runScan = async () => {
|
|
89
|
+
setIsScanning(true);
|
|
90
|
+
setError(null);
|
|
91
|
+
setDryRunResult(null);
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
setResults(await scanSiteForMigrations());
|
|
95
|
+
} catch (scanError) {
|
|
96
|
+
setError(scanError instanceof Error ? scanError.message : String(scanError));
|
|
97
|
+
} finally {
|
|
98
|
+
setIsScanning(false);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const runDryRun = async () => {
|
|
103
|
+
setError(null);
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
setDryRunResult(await batchMigrateScanResults(results, { dryRun: true }));
|
|
107
|
+
} catch (batchError) {
|
|
108
|
+
setError(batchError instanceof Error ? batchError.message : String(batchError));
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const runBatchMigration = async () => {
|
|
113
|
+
if (!window.confirm(__("Migrate all detected legacy blocks now?", "{{textdomain}}"))) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
setIsMigrating(true);
|
|
118
|
+
setError(null);
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
setDryRunResult(await batchMigrateScanResults(results, { dryRun: false }));
|
|
122
|
+
await runScan();
|
|
123
|
+
} catch (batchError) {
|
|
124
|
+
setError(batchError instanceof Error ? batchError.message : String(batchError));
|
|
125
|
+
} finally {
|
|
126
|
+
setIsMigrating(false);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const downloadReport = () => {
|
|
131
|
+
downloadFile(
|
|
132
|
+
generateMigrationReport(results),
|
|
133
|
+
"{{slug}}-migration-report.md",
|
|
134
|
+
"text/markdown",
|
|
135
|
+
);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const downloadJson = () => {
|
|
139
|
+
downloadFile(
|
|
140
|
+
JSON.stringify(
|
|
141
|
+
{
|
|
142
|
+
dryRunResult,
|
|
143
|
+
results,
|
|
144
|
+
},
|
|
145
|
+
null,
|
|
146
|
+
2,
|
|
147
|
+
),
|
|
148
|
+
"{{slug}}-migration-report.json",
|
|
149
|
+
"application/json",
|
|
150
|
+
);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<div className="{{slug}}-migration-dashboard">
|
|
155
|
+
<Card>
|
|
156
|
+
<CardBody>
|
|
157
|
+
<div style={{ display: "grid", gap: "12px" }}>
|
|
158
|
+
<div>
|
|
159
|
+
<strong>{__("Migration Manager", "{{textdomain}}")}</strong>
|
|
160
|
+
<p style={{ margin: "4px 0 0" }}>
|
|
161
|
+
{__(
|
|
162
|
+
"Scan posts for legacy attributes, preview field-level changes, and batch migrate to the current Typia contract.",
|
|
163
|
+
"{{textdomain}}",
|
|
164
|
+
)}
|
|
165
|
+
</p>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
|
|
169
|
+
<Button variant="secondary" onClick={runScan} disabled={isScanning}>
|
|
170
|
+
{isScanning ? __("Scanning…", "{{textdomain}}") : __("Scan site", "{{textdomain}}")}
|
|
171
|
+
</Button>
|
|
172
|
+
<Button variant="secondary" onClick={runDryRun} disabled={results.length === 0 || isScanning || isMigrating}>
|
|
173
|
+
{__("Dry run", "{{textdomain}}")}
|
|
174
|
+
</Button>
|
|
175
|
+
<Button variant="primary" onClick={runBatchMigration} disabled={stats.needsMigration === 0 || isScanning || isMigrating}>
|
|
176
|
+
{isMigrating ? __("Migrating…", "{{textdomain}}") : __("Migrate all", "{{textdomain}}")}
|
|
177
|
+
</Button>
|
|
178
|
+
<Button variant="tertiary" onClick={downloadReport} disabled={results.length === 0}>
|
|
179
|
+
{__("Download markdown", "{{textdomain}}")}
|
|
180
|
+
</Button>
|
|
181
|
+
<Button variant="tertiary" onClick={downloadJson} disabled={results.length === 0}>
|
|
182
|
+
{__("Download JSON", "{{textdomain}}")}
|
|
183
|
+
</Button>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
{(isScanning || isMigrating) && <Spinner />}
|
|
187
|
+
|
|
188
|
+
{error && (
|
|
189
|
+
<Notice status="error" isDismissible={false}>
|
|
190
|
+
{error}
|
|
191
|
+
</Notice>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
{results.length > 0 && (
|
|
195
|
+
<div style={{ display: "grid", gap: "8px" }}>
|
|
196
|
+
<div>
|
|
197
|
+
<strong>{__("Summary", "{{textdomain}}")}</strong>
|
|
198
|
+
<ul style={{ margin: "8px 0 0", paddingLeft: "20px" }}>
|
|
199
|
+
<li>{__("Detected blocks", "{{textdomain}}")}: {stats.total}</li>
|
|
200
|
+
<li>{__("Need migration", "{{textdomain}}")}: {stats.needsMigration}</li>
|
|
201
|
+
<li>{__("Unknown shape", "{{textdomain}}")}: {stats.unknown}</li>
|
|
202
|
+
</ul>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<div>
|
|
206
|
+
<strong>{__("Version distribution", "{{textdomain}}")}</strong>
|
|
207
|
+
<ul style={{ margin: "8px 0 0", paddingLeft: "20px" }}>
|
|
208
|
+
{Object.entries(stats.versions).map(([version, count]) => (
|
|
209
|
+
<li key={version}>
|
|
210
|
+
{version}: {count}
|
|
211
|
+
</li>
|
|
212
|
+
))}
|
|
213
|
+
</ul>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<div>
|
|
217
|
+
<strong>{__("Latest results", "{{textdomain}}")}</strong>
|
|
218
|
+
<div style={{ display: "grid", gap: "8px", marginTop: "8px" }}>
|
|
219
|
+
{results.map((result) => (
|
|
220
|
+
<details key={`${result.postId}:${result.blockPath.join(".")}`}>
|
|
221
|
+
<summary>
|
|
222
|
+
<strong>{result.postTitle}</strong>
|
|
223
|
+
{" — "}
|
|
224
|
+
{result.analysis.currentVersion} → {result.analysis.targetVersion}
|
|
225
|
+
{" · "}
|
|
226
|
+
{result.preview.changedFields.length}
|
|
227
|
+
{" "}
|
|
228
|
+
{__("changed", "{{textdomain}}")}
|
|
229
|
+
{result.preview.unresolved.length > 0 ? " · manual review" : ""}
|
|
230
|
+
{result.preview.validationErrors.length > 0 ? " · validation issues" : ""}
|
|
231
|
+
</summary>
|
|
232
|
+
<div style={{ marginTop: "8px", display: "grid", gap: "8px" }}>
|
|
233
|
+
<div>
|
|
234
|
+
<strong>{__("Changed fields", "{{textdomain}}")}</strong>
|
|
235
|
+
<div>
|
|
236
|
+
{result.preview.changedFields.length > 0
|
|
237
|
+
? result.preview.changedFields.join(", ")
|
|
238
|
+
: __("None", "{{textdomain}}")}
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
<div>
|
|
242
|
+
<strong>{__("Union branches", "{{textdomain}}")}</strong>
|
|
243
|
+
<div>{formatUnionSummary(result.preview)}</div>
|
|
244
|
+
</div>
|
|
245
|
+
{result.preview.unresolved.length > 0 && (
|
|
246
|
+
<div>
|
|
247
|
+
<strong>{__("Manual review", "{{textdomain}}")}</strong>
|
|
248
|
+
<div>{result.preview.unresolved.join(", ")}</div>
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
251
|
+
{result.preview.validationErrors.length > 0 && (
|
|
252
|
+
<div>
|
|
253
|
+
<strong>{__("Validation", "{{textdomain}}")}</strong>
|
|
254
|
+
<div>{result.preview.validationErrors.join(", ")}</div>
|
|
255
|
+
</div>
|
|
256
|
+
)}
|
|
257
|
+
<div>
|
|
258
|
+
<strong>{__("Before", "{{textdomain}}")}</strong>
|
|
259
|
+
{renderJsonPreview(result.preview.before)}
|
|
260
|
+
</div>
|
|
261
|
+
<div>
|
|
262
|
+
<strong>{__("After", "{{textdomain}}")}</strong>
|
|
263
|
+
{renderJsonPreview(result.preview.after)}
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
</details>
|
|
267
|
+
))}
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
|
|
273
|
+
{dryRunResult && (
|
|
274
|
+
<div style={{ display: "grid", gap: "8px" }}>
|
|
275
|
+
<strong>{__("Dry-run preview", "{{textdomain}}")}</strong>
|
|
276
|
+
<div style={{ display: "grid", gap: "8px", marginTop: "8px" }}>
|
|
277
|
+
{dryRunResult.posts.map((post) => (
|
|
278
|
+
<details key={post.postId}>
|
|
279
|
+
<summary>
|
|
280
|
+
<strong>{post.postTitle}</strong>
|
|
281
|
+
{" — "}
|
|
282
|
+
{post.status}
|
|
283
|
+
{post.reason ? ` (${post.reason})` : ""}
|
|
284
|
+
</summary>
|
|
285
|
+
<div style={{ marginTop: "8px", display: "grid", gap: "8px" }}>
|
|
286
|
+
{post.previews.map((preview) => (
|
|
287
|
+
<div key={preview.blockPath.join(".")}>
|
|
288
|
+
<div>
|
|
289
|
+
<strong>{preview.currentVersion} → {preview.targetVersion}</strong>
|
|
290
|
+
{preview.preview.changedFields.length > 0
|
|
291
|
+
? ` · ${preview.preview.changedFields.join(", ")}`
|
|
292
|
+
: ""}
|
|
293
|
+
{preview.reason ? ` · ${preview.reason}` : ""}
|
|
294
|
+
</div>
|
|
295
|
+
<div>{formatUnionSummary(preview.preview)}</div>
|
|
296
|
+
{renderJsonPreview({
|
|
297
|
+
after: preview.preview.after,
|
|
298
|
+
before: preview.preview.before,
|
|
299
|
+
unresolved: preview.preview.unresolved,
|
|
300
|
+
validationErrors: preview.preview.validationErrors,
|
|
301
|
+
})}
|
|
302
|
+
</div>
|
|
303
|
+
))}
|
|
304
|
+
</div>
|
|
305
|
+
</details>
|
|
306
|
+
))}
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
)}
|
|
310
|
+
</div>
|
|
311
|
+
</CardBody>
|
|
312
|
+
</Card>
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
fallback?: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface State {
|
|
9
|
+
hasError: boolean;
|
|
10
|
+
error?: Error;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Error Boundary component for {{title}}
|
|
15
|
+
*/
|
|
16
|
+
export class ErrorBoundary extends Component<Props, State> {
|
|
17
|
+
public state: State = {
|
|
18
|
+
hasError: false
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
public static getDerivedStateFromError(error: Error): State {
|
|
22
|
+
return { hasError: true, error };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
26
|
+
console.error('{{title}} Error:', error, errorInfo);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public render() {
|
|
30
|
+
if (this.state.hasError) {
|
|
31
|
+
return this.props.fallback || (
|
|
32
|
+
<div className="{{dashCase}}-error-boundary">
|
|
33
|
+
<h3>Something went wrong with {{title}}</h3>
|
|
34
|
+
<p>Please check the console for more details.</p>
|
|
35
|
+
{this.state.error && (
|
|
36
|
+
<details>
|
|
37
|
+
<summary>Error details</summary>
|
|
38
|
+
<pre>{this.state.error.stack}</pre>
|
|
39
|
+
</details>
|
|
40
|
+
)}
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return this.props.children;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { BlockEditProps } from '@wordpress/blocks';
|
|
2
|
+
import { useBlockProps, InspectorControls, RichText } from '@wordpress/block-editor';
|
|
3
|
+
import { PanelBody, ToggleControl, SelectControl } from '@wordpress/components';
|
|
4
|
+
import { __ } from '@wordpress/i18n';
|
|
5
|
+
import { {{titleCase}}Attributes } from './types';
|
|
6
|
+
import { validators } from './validators';
|
|
7
|
+
import { useTypiaValidation, useAttributeLogger, useDebounce } from './hooks';
|
|
8
|
+
import { ErrorBoundary } from './components/ErrorBoundary';
|
|
9
|
+
import { MigrationDashboard } from './admin/migration-dashboard';
|
|
10
|
+
import { classNames } from './utils';
|
|
11
|
+
|
|
12
|
+
type EditProps = BlockEditProps<{{titleCase}}Attributes>;
|
|
13
|
+
|
|
14
|
+
export default function Edit({ attributes, setAttributes }: EditProps) {
|
|
15
|
+
const blockProps = useBlockProps();
|
|
16
|
+
const { isValid, errors } = useTypiaValidation(attributes, validators.validate);
|
|
17
|
+
const debouncedAttributes = useDebounce(attributes, 300);
|
|
18
|
+
|
|
19
|
+
// Log attribute changes in development
|
|
20
|
+
useAttributeLogger(debouncedAttributes);
|
|
21
|
+
|
|
22
|
+
const updateAttribute = <K extends keyof {{titleCase}}Attributes>(
|
|
23
|
+
key: K,
|
|
24
|
+
value: {{titleCase}}Attributes[K]
|
|
25
|
+
) => {
|
|
26
|
+
const newAttrs = { ...attributes, [key]: value };
|
|
27
|
+
const validation = validators.validate(newAttrs);
|
|
28
|
+
|
|
29
|
+
if (validation.success) {
|
|
30
|
+
setAttributes({ [key]: value } as Partial<{{titleCase}}Attributes>);
|
|
31
|
+
} else {
|
|
32
|
+
console.error(`Validation failed for ${String(key)}:`, validation.errors);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<ErrorBoundary>
|
|
38
|
+
<InspectorControls>
|
|
39
|
+
<PanelBody title={__('Settings', '{{textdomain}}')}>
|
|
40
|
+
<ToggleControl
|
|
41
|
+
label={__('Visible', '{{textdomain}}')}
|
|
42
|
+
checked={attributes.isVisible ?? true}
|
|
43
|
+
onChange={(isVisible) => updateAttribute('isVisible', isVisible)}
|
|
44
|
+
/>
|
|
45
|
+
<SelectControl
|
|
46
|
+
label={__('Alignment', '{{textdomain}}')}
|
|
47
|
+
value={attributes.alignment ?? 'left'}
|
|
48
|
+
options={[
|
|
49
|
+
{ label: __('Left', '{{textdomain}}'), value: 'left' },
|
|
50
|
+
{ label: __('Center', '{{textdomain}}'), value: 'center' },
|
|
51
|
+
{ label: __('Right', '{{textdomain}}'), value: 'right' },
|
|
52
|
+
{ label: __('Justify', '{{textdomain}}'), value: 'justify' },
|
|
53
|
+
]}
|
|
54
|
+
onChange={(alignment) => updateAttribute('alignment', alignment as any)}
|
|
55
|
+
/>
|
|
56
|
+
{!isValid && (
|
|
57
|
+
<div className="components-notice is-error">
|
|
58
|
+
<p><strong>{__('Validation Errors:', '{{textdomain}}')}</strong></p>
|
|
59
|
+
<ul style={{ margin: 0, paddingLeft: '1em' }}>
|
|
60
|
+
{errors.map((error, index) => (
|
|
61
|
+
<li key={index}>
|
|
62
|
+
<code>{error.path}</code>: {error.message}
|
|
63
|
+
</li>
|
|
64
|
+
))}
|
|
65
|
+
</ul>
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
</PanelBody>
|
|
69
|
+
<PanelBody title={__('Migration Manager', '{{textdomain}}')} initialOpen={false}>
|
|
70
|
+
<MigrationDashboard />
|
|
71
|
+
</PanelBody>
|
|
72
|
+
</InspectorControls>
|
|
73
|
+
|
|
74
|
+
<div {...blockProps} className={classNames(blockProps.className, {
|
|
75
|
+
'has-validation-errors': !isValid,
|
|
76
|
+
'is-hidden': !attributes.isVisible
|
|
77
|
+
})}>
|
|
78
|
+
<RichText
|
|
79
|
+
tagName="p"
|
|
80
|
+
value={attributes.content}
|
|
81
|
+
onChange={(content) => updateAttribute('content', content)}
|
|
82
|
+
placeholder={__('Enter your text...', '{{textdomain}}')}
|
|
83
|
+
className={classNames('{{dashCase}}-content', `align-${attributes.alignment}`)}
|
|
84
|
+
style={{
|
|
85
|
+
textAlign: attributes.alignment ?? 'left'
|
|
86
|
+
}}
|
|
87
|
+
/>
|
|
88
|
+
|
|
89
|
+
<div className="block-info">
|
|
90
|
+
<small>
|
|
91
|
+
{__('{{title}} – Enhanced with utilities, hooks, and error handling!', '{{textdomain}}')}
|
|
92
|
+
</small>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</ErrorBoundary>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook for debounced value
|
|
5
|
+
*/
|
|
6
|
+
export function useDebounce<T>(value: T, delay: number): T {
|
|
7
|
+
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const handler = setTimeout(() => {
|
|
11
|
+
setDebouncedValue(value);
|
|
12
|
+
}, delay);
|
|
13
|
+
|
|
14
|
+
return () => {
|
|
15
|
+
clearTimeout(handler);
|
|
16
|
+
};
|
|
17
|
+
}, [value, delay]);
|
|
18
|
+
|
|
19
|
+
return debouncedValue;
|
|
20
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hook for localStorage with type safety
|
|
5
|
+
*/
|
|
6
|
+
export function useLocalStorage<T>(
|
|
7
|
+
key: string,
|
|
8
|
+
initialValue: T
|
|
9
|
+
): [T, (value: T | ((val: T) => T)) => void] {
|
|
10
|
+
const [storedValue, setStoredValue] = useState<T>(() => {
|
|
11
|
+
try {
|
|
12
|
+
const item = window.localStorage.getItem(key);
|
|
13
|
+
return item ? JSON.parse(item) : initialValue;
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.error(`Error reading localStorage key "${key}":`, error);
|
|
16
|
+
return initialValue;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const setValue = (value: T | ((val: T) => T)) => {
|
|
21
|
+
try {
|
|
22
|
+
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
|
23
|
+
setStoredValue(valueToStore);
|
|
24
|
+
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error(`Error setting localStorage key "${key}":`, error);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return [storedValue, setValue];
|
|
31
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useEffect, useState } from '@wordpress/element';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom hooks for {{title}}
|
|
5
|
+
*/
|
|
6
|
+
export * from './hooks/useDebounce';
|
|
7
|
+
export * from './hooks/useLocalStorage';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Hook for Typia validation with real-time feedback
|
|
11
|
+
*/
|
|
12
|
+
export function useTypiaValidation<T>(
|
|
13
|
+
data: T,
|
|
14
|
+
validator: (value: T) => { success: boolean; errors?: any[] }
|
|
15
|
+
) {
|
|
16
|
+
const [isValid, setIsValid] = useState(true);
|
|
17
|
+
const [errors, setErrors] = useState<any[]>([]);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const result = validator(data);
|
|
21
|
+
setIsValid(result.success);
|
|
22
|
+
setErrors(result.errors || []);
|
|
23
|
+
}, [data, validator]);
|
|
24
|
+
|
|
25
|
+
return { isValid, errors };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Hook for generating UUID
|
|
30
|
+
*/
|
|
31
|
+
export function useUUID() {
|
|
32
|
+
const [uuid] = useState(() => {
|
|
33
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
34
|
+
return crypto.randomUUID();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
38
|
+
const r = Math.random() * 16 | 0;
|
|
39
|
+
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
40
|
+
return v.toString(16);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return uuid;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Hook for logging attribute changes in development
|
|
49
|
+
*/
|
|
50
|
+
export function useAttributeLogger(attributes: any) {
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (process.env.NODE_ENV === 'development') {
|
|
53
|
+
console.log('{{title}} attributes changed:', attributes);
|
|
54
|
+
}
|
|
55
|
+
}, [attributes]);
|
|
56
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { registerBlockType } from '@wordpress/blocks';
|
|
2
|
+
import { __ } from '@wordpress/i18n';
|
|
3
|
+
import { deprecated } from './deprecated';
|
|
4
|
+
import Edit from './edit';
|
|
5
|
+
import Save from './save';
|
|
6
|
+
import { {{titleCase}}Attributes } from './types';
|
|
7
|
+
import { validators } from './validators';
|
|
8
|
+
import './style.scss';
|
|
9
|
+
|
|
10
|
+
registerBlockType<{{titleCase}}Attributes>('{{namespace}}/{{slug}}', {
|
|
11
|
+
// Use Typia to generate example data
|
|
12
|
+
example: {
|
|
13
|
+
attributes: validators.random()
|
|
14
|
+
},
|
|
15
|
+
deprecated,
|
|
16
|
+
edit: Edit,
|
|
17
|
+
save: Save,
|
|
18
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Rename + Transform + Union Example
|
|
2
|
+
|
|
3
|
+
This folder is a reference pack for realistic migration authoring. It is **not** part of the active migration graph.
|
|
4
|
+
|
|
5
|
+
It demonstrates:
|
|
6
|
+
|
|
7
|
+
- a top-level rename
|
|
8
|
+
- a nested leaf rename
|
|
9
|
+
- a semantic transform
|
|
10
|
+
- a discriminated union branch change that still needs manual review
|
|
11
|
+
|
|
12
|
+
Use it as a guide when editing generated files in:
|
|
13
|
+
|
|
14
|
+
- `src/migrations/rules/`
|
|
15
|
+
- `src/migrations/fixtures/`
|
|
16
|
+
|
|
17
|
+
Recommended workflow:
|
|
18
|
+
|
|
19
|
+
1. scaffold the real edge with `bun run migration:scaffold -- --from <semver>`
|
|
20
|
+
2. review auto-applied `renameMap`
|
|
21
|
+
3. copy the relevant transform patterns from this example
|
|
22
|
+
4. update the generated fixture to match your real legacy payload
|
|
23
|
+
5. run `bun run migration:verify`
|