@wp-typia/project-tools 0.14.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/runtime/cli-add.js +26 -70
- package/dist/runtime/cli-doctor.js +25 -9
- package/dist/runtime/cli-help.js +1 -0
- package/dist/runtime/cli-templates.js +10 -0
- package/dist/runtime/persistence-rest-artifacts.d.ts +76 -0
- package/dist/runtime/persistence-rest-artifacts.js +99 -0
- package/dist/runtime/scaffold-onboarding.js +3 -2
- package/dist/runtime/scaffold.d.ts +11 -2
- package/dist/runtime/scaffold.js +98 -1
- package/dist/runtime/schema-core.d.ts +1 -0
- package/dist/runtime/schema-core.js +188 -8
- package/dist/runtime/template-builtins.js +1 -1
- package/dist/runtime/template-registry.d.ts +2 -1
- package/dist/runtime/template-registry.js +13 -2
- package/package.json +2 -1
- package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +103 -7
- package/templates/_shared/compound/persistence/scripts/block-config.ts.mustache +16 -0
- package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api-types.ts.mustache +10 -0
- package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api.ts.mustache +23 -0
- package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/data.ts.mustache +45 -0
- package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/interactivity.ts.mustache +201 -42
- package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/render.php.mustache +37 -43
- package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/types.ts.mustache +13 -8
- package/templates/_shared/compound/persistence-auth/{{slugKebabCase}}.php.mustache +139 -0
- package/templates/_shared/compound/persistence-public/{{slugKebabCase}}.php.mustache +159 -0
- package/templates/_shared/persistence/auth/{{slugKebabCase}}.php.mustache +139 -0
- package/templates/_shared/persistence/core/scripts/sync-rest-contracts.ts.mustache +16 -0
- package/templates/_shared/persistence/core/src/api-types.ts.mustache +10 -0
- package/templates/_shared/persistence/core/src/api-validators.ts.mustache +14 -0
- package/templates/_shared/persistence/core/src/api.ts.mustache +48 -6
- package/templates/_shared/persistence/core/src/data.ts.mustache +45 -0
- package/templates/_shared/persistence/core/src/interactivity.ts.mustache +216 -53
- package/templates/_shared/persistence/public/{{slugKebabCase}}.php.mustache +159 -0
- package/templates/_shared/rest-helpers/public/inc/rest-public.php.mustache +1 -1
- package/templates/_shared/workspace/persistence-auth/server.php.mustache +139 -0
- package/templates/_shared/workspace/persistence-public/inc/rest-public.php.mustache +1 -1
- package/templates/_shared/workspace/persistence-public/server.php.mustache +159 -0
- package/templates/interactivity/src/block.json.mustache +1 -0
- package/templates/interactivity/src/editor.scss.mustache +8 -0
- package/templates/interactivity/src/index.tsx.mustache +1 -0
- package/templates/persistence/src/edit.tsx.mustache +7 -7
- package/templates/persistence/src/render.php.mustache +37 -43
- package/templates/persistence/src/types.ts.mustache +13 -9
|
@@ -149,15 +149,35 @@ function applyProjectedTypeTag(schema, typeTag, path) {
|
|
|
149
149
|
throw new Error(`Unsupported wp-typia schema type tag "${typeTag}" at "${path}".`);
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
|
+
function canProjectTypeTag(typeTag) {
|
|
153
|
+
switch (typeTag) {
|
|
154
|
+
case "uint32":
|
|
155
|
+
case "int32":
|
|
156
|
+
case "float":
|
|
157
|
+
case "double":
|
|
158
|
+
return true;
|
|
159
|
+
default:
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
152
163
|
function projectSchemaArrayItemsForAiStructuredOutput(items, path) {
|
|
153
164
|
return items.map((item, index) => projectSchemaObjectForAiStructuredOutput(item, `${path}/${index}`));
|
|
154
165
|
}
|
|
166
|
+
function projectSchemaArrayItemsForRest(items, path) {
|
|
167
|
+
return items.map((item, index) => projectSchemaObjectForRest(item, `${path}/${index}`));
|
|
168
|
+
}
|
|
155
169
|
function projectSchemaPropertyMapForAiStructuredOutput(properties, path) {
|
|
156
170
|
return Object.fromEntries(Object.entries(properties).map(([key, value]) => [
|
|
157
171
|
key,
|
|
158
172
|
projectSchemaObjectForAiStructuredOutput(value, `${path}/${key}`),
|
|
159
173
|
]));
|
|
160
174
|
}
|
|
175
|
+
function projectSchemaPropertyMapForRest(properties, path) {
|
|
176
|
+
return Object.fromEntries(Object.entries(properties).map(([key, value]) => [
|
|
177
|
+
key,
|
|
178
|
+
projectSchemaObjectForRest(value, `${path}/${key}`),
|
|
179
|
+
]));
|
|
180
|
+
}
|
|
161
181
|
function projectSchemaObjectForAiStructuredOutput(node, path) {
|
|
162
182
|
const projectedNode = cloneJsonSchemaNode(node);
|
|
163
183
|
const rawTypeTag = projectedNode[WP_TYPIA_OPENAPI_EXTENSION_KEYS.TYPE_TAG];
|
|
@@ -188,6 +208,126 @@ function projectSchemaObjectForAiStructuredOutput(node, path) {
|
|
|
188
208
|
}
|
|
189
209
|
return projectedNode;
|
|
190
210
|
}
|
|
211
|
+
function projectSchemaObjectForRest(node, path) {
|
|
212
|
+
const projectedNode = cloneJsonSchemaNode(node);
|
|
213
|
+
const rawTypeTag = projectedNode[WP_TYPIA_OPENAPI_EXTENSION_KEYS.TYPE_TAG];
|
|
214
|
+
if (typeof rawTypeTag === "string" && canProjectTypeTag(rawTypeTag)) {
|
|
215
|
+
applyProjectedTypeTag(projectedNode, rawTypeTag, path);
|
|
216
|
+
}
|
|
217
|
+
for (const key of Object.keys(projectedNode)) {
|
|
218
|
+
const child = projectedNode[key];
|
|
219
|
+
if (Array.isArray(child)) {
|
|
220
|
+
projectedNode[key] = child.every(isJsonSchemaObject)
|
|
221
|
+
? projectSchemaArrayItemsForRest(child, `${path}/${key}`)
|
|
222
|
+
: child;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (!isJsonSchemaObject(child)) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (key === "properties") {
|
|
229
|
+
projectedNode[key] = projectSchemaPropertyMapForRest(child, `${path}/${key}`);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
projectedNode[key] = projectSchemaObjectForRest(child, `${path}/${key}`);
|
|
233
|
+
}
|
|
234
|
+
applyProjectedBootstrapContract(projectedNode);
|
|
235
|
+
return projectedNode;
|
|
236
|
+
}
|
|
237
|
+
function applyProjectedBootstrapContract(schema) {
|
|
238
|
+
if (schema.type !== "object" || !isJsonSchemaObject(schema.properties)) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const properties = schema.properties;
|
|
242
|
+
const buildRequiredPropertyObject = (requiredKeys) => Object.fromEntries(requiredKeys.map((requiredKey) => [requiredKey, properties[requiredKey] ?? {}]));
|
|
243
|
+
if (properties.canWrite?.type !== "boolean") {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const allOf = Array.isArray(schema.allOf)
|
|
247
|
+
? [...schema.allOf]
|
|
248
|
+
: [];
|
|
249
|
+
const canWriteIsTrue = {
|
|
250
|
+
properties: {
|
|
251
|
+
canWrite: {
|
|
252
|
+
const: true,
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
required: ["canWrite"],
|
|
256
|
+
};
|
|
257
|
+
const hasRestNonce = properties.restNonce?.type === "string";
|
|
258
|
+
const hasPublicWriteCredential = properties.publicWriteToken?.type === "string" &&
|
|
259
|
+
isJsonSchemaObject(properties.publicWriteExpiresAt);
|
|
260
|
+
if (hasRestNonce && hasPublicWriteCredential) {
|
|
261
|
+
allOf.push({
|
|
262
|
+
if: canWriteIsTrue,
|
|
263
|
+
then: {
|
|
264
|
+
anyOf: [
|
|
265
|
+
{
|
|
266
|
+
properties: buildRequiredPropertyObject(["restNonce"]),
|
|
267
|
+
required: ["restNonce"],
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
properties: buildRequiredPropertyObject([
|
|
271
|
+
"publicWriteExpiresAt",
|
|
272
|
+
"publicWriteToken",
|
|
273
|
+
]),
|
|
274
|
+
required: ["publicWriteExpiresAt", "publicWriteToken"],
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
},
|
|
278
|
+
else: {
|
|
279
|
+
not: {
|
|
280
|
+
anyOf: [
|
|
281
|
+
{
|
|
282
|
+
properties: buildRequiredPropertyObject(["restNonce"]),
|
|
283
|
+
required: ["restNonce"],
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
properties: buildRequiredPropertyObject(["publicWriteToken"]),
|
|
287
|
+
required: ["publicWriteToken"],
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
if (hasRestNonce && !hasPublicWriteCredential) {
|
|
295
|
+
allOf.push({
|
|
296
|
+
if: canWriteIsTrue,
|
|
297
|
+
then: {
|
|
298
|
+
properties: buildRequiredPropertyObject(["restNonce"]),
|
|
299
|
+
required: ["restNonce"],
|
|
300
|
+
},
|
|
301
|
+
else: {
|
|
302
|
+
not: {
|
|
303
|
+
properties: buildRequiredPropertyObject(["restNonce"]),
|
|
304
|
+
required: ["restNonce"],
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
if (hasPublicWriteCredential && !hasRestNonce) {
|
|
310
|
+
allOf.push({
|
|
311
|
+
if: canWriteIsTrue,
|
|
312
|
+
then: {
|
|
313
|
+
properties: buildRequiredPropertyObject([
|
|
314
|
+
"publicWriteExpiresAt",
|
|
315
|
+
"publicWriteToken",
|
|
316
|
+
]),
|
|
317
|
+
required: ["publicWriteExpiresAt", "publicWriteToken"],
|
|
318
|
+
},
|
|
319
|
+
else: {
|
|
320
|
+
not: {
|
|
321
|
+
properties: buildRequiredPropertyObject(["publicWriteToken"]),
|
|
322
|
+
required: ["publicWriteToken"],
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
if (allOf.length > 0) {
|
|
328
|
+
schema.allOf = allOf;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
191
331
|
function manifestUnionToJsonSchema(union) {
|
|
192
332
|
const oneOf = Object.entries(union.branches).map(([branchKey, branch]) => {
|
|
193
333
|
if (branch.ts.kind !== "object") {
|
|
@@ -305,7 +445,7 @@ export function manifestToJsonSchema(doc) {
|
|
|
305
445
|
*/
|
|
306
446
|
export function projectJsonSchemaDocument(schema, options) {
|
|
307
447
|
if (options.profile === "rest") {
|
|
308
|
-
return
|
|
448
|
+
return projectSchemaObjectForRest(schema, "#");
|
|
309
449
|
}
|
|
310
450
|
if (options.profile === "ai-structured-output") {
|
|
311
451
|
return projectSchemaObjectForAiStructuredOutput(schema, "#");
|
|
@@ -321,10 +461,14 @@ export function projectJsonSchemaDocument(schema, options) {
|
|
|
321
461
|
*/
|
|
322
462
|
export function manifestToOpenApi(doc, info = {}) {
|
|
323
463
|
const schemaName = doc.sourceType ?? "TypiaDocument";
|
|
464
|
+
const projectedSchema = projectJsonSchemaDocument(manifestToJsonSchema(doc), {
|
|
465
|
+
profile: "rest",
|
|
466
|
+
});
|
|
467
|
+
delete projectedSchema.$schema;
|
|
324
468
|
return {
|
|
325
469
|
components: {
|
|
326
470
|
schemas: {
|
|
327
|
-
[schemaName]:
|
|
471
|
+
[schemaName]: projectedSchema,
|
|
328
472
|
},
|
|
329
473
|
},
|
|
330
474
|
info: {
|
|
@@ -486,7 +630,36 @@ function buildQueryParameters(contract) {
|
|
|
486
630
|
schema: manifestAttributeToJsonSchema(attribute),
|
|
487
631
|
}));
|
|
488
632
|
}
|
|
489
|
-
function
|
|
633
|
+
function createBootstrapResponseHeaders(normalizedAuth) {
|
|
634
|
+
const headers = {
|
|
635
|
+
"Cache-Control": {
|
|
636
|
+
description: "Must be non-cacheable for fresh bootstrap write/session state.",
|
|
637
|
+
schema: {
|
|
638
|
+
type: "string",
|
|
639
|
+
example: "private, no-store, no-cache, must-revalidate",
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
Pragma: {
|
|
643
|
+
description: "Legacy non-cacheable bootstrap response directive.",
|
|
644
|
+
schema: {
|
|
645
|
+
type: "string",
|
|
646
|
+
example: "no-cache",
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
};
|
|
650
|
+
if (normalizedAuth.wordpressAuth?.mechanism ===
|
|
651
|
+
WP_TYPIA_OPENAPI_LITERALS.WORDPRESS_REST_NONCE_MECHANISM) {
|
|
652
|
+
headers.Vary = {
|
|
653
|
+
description: "Viewer-aware bootstrap responses should vary on cookie-backed auth state.",
|
|
654
|
+
schema: {
|
|
655
|
+
type: "string",
|
|
656
|
+
example: "Cookie",
|
|
657
|
+
},
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
return headers;
|
|
661
|
+
}
|
|
662
|
+
function createSuccessResponse(schemaName, headers) {
|
|
490
663
|
return {
|
|
491
664
|
content: {
|
|
492
665
|
[WP_TYPIA_OPENAPI_LITERALS.JSON_CONTENT_TYPE]: {
|
|
@@ -494,14 +667,18 @@ function createSuccessResponse(schemaName) {
|
|
|
494
667
|
},
|
|
495
668
|
},
|
|
496
669
|
description: WP_TYPIA_OPENAPI_LITERALS.SUCCESS_RESPONSE_DESCRIPTION,
|
|
670
|
+
...(headers ? { headers } : {}),
|
|
497
671
|
};
|
|
498
672
|
}
|
|
499
673
|
function buildEndpointOpenApiOperation(endpoint, contracts) {
|
|
500
674
|
const normalizedAuth = normalizeEndpointAuthDefinition(endpoint);
|
|
675
|
+
const isBootstrapEndpoint = endpoint.path.endsWith("/bootstrap");
|
|
501
676
|
const operation = {
|
|
502
677
|
operationId: endpoint.operationId,
|
|
503
678
|
responses: {
|
|
504
|
-
"200": createSuccessResponse(getContractSchemaName(endpoint.responseContract, contracts[endpoint.responseContract], endpoint, "response")
|
|
679
|
+
"200": createSuccessResponse(getContractSchemaName(endpoint.responseContract, contracts[endpoint.responseContract], endpoint, "response"), isBootstrapEndpoint
|
|
680
|
+
? createBootstrapResponseHeaders(normalizedAuth)
|
|
681
|
+
: undefined),
|
|
505
682
|
},
|
|
506
683
|
tags: [...endpoint.tags],
|
|
507
684
|
[WP_TYPIA_OPENAPI_EXTENSION_KEYS.AUTH_INTENT]: normalizedAuth.auth,
|
|
@@ -554,10 +731,13 @@ function buildEndpointOpenApiOperation(endpoint, contracts) {
|
|
|
554
731
|
*/
|
|
555
732
|
export function buildEndpointOpenApiDocument(options) {
|
|
556
733
|
const contractEntries = Object.entries(options.contracts);
|
|
557
|
-
const schemas = Object.fromEntries(contractEntries.map(([contractKey, contract]) =>
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
734
|
+
const schemas = Object.fromEntries(contractEntries.map(([contractKey, contract]) => {
|
|
735
|
+
const projectedSchema = projectJsonSchemaDocument(manifestToJsonSchema(contract.document), {
|
|
736
|
+
profile: "rest",
|
|
737
|
+
});
|
|
738
|
+
delete projectedSchema.$schema;
|
|
739
|
+
return [getContractSchemaName(contractKey, contract), projectedSchema];
|
|
740
|
+
}));
|
|
561
741
|
const paths = {};
|
|
562
742
|
const topLevelTags = [...new Set(options.endpoints.flatMap((endpoint) => endpoint.tags))]
|
|
563
743
|
.filter((tag) => typeof tag === "string" && tag.length > 0)
|
|
@@ -59,7 +59,7 @@ export async function resolveBuiltInTemplateSource(templateId, options = {}) {
|
|
|
59
59
|
throw error;
|
|
60
60
|
}
|
|
61
61
|
return {
|
|
62
|
-
id:
|
|
62
|
+
id: templateId,
|
|
63
63
|
defaultCategory: template.defaultCategory,
|
|
64
64
|
description: template.description,
|
|
65
65
|
features: template.features,
|
|
@@ -15,10 +15,11 @@ export declare const SHARED_WORKSPACE_TEMPLATE_ROOT: string;
|
|
|
15
15
|
export declare const SHARED_TEST_PRESET_TEMPLATE_ROOT: string;
|
|
16
16
|
export declare const SHARED_WP_ENV_PRESET_TEMPLATE_ROOT: string;
|
|
17
17
|
export declare const BUILTIN_TEMPLATE_IDS: readonly ["basic", "interactivity", "persistence", "compound"];
|
|
18
|
+
export declare const OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE = "@wp-typia/create-workspace-template";
|
|
18
19
|
export type BuiltInTemplateId = (typeof BUILTIN_TEMPLATE_IDS)[number];
|
|
19
20
|
export type PersistencePolicy = "authenticated" | "public";
|
|
20
21
|
export interface TemplateDefinition {
|
|
21
|
-
id:
|
|
22
|
+
id: string;
|
|
22
23
|
description: string;
|
|
23
24
|
defaultCategory: string;
|
|
24
25
|
features: string[];
|
|
@@ -41,6 +41,7 @@ export const SHARED_WORKSPACE_TEMPLATE_ROOT = path.join(SHARED_TEMPLATE_ROOT, "w
|
|
|
41
41
|
export const SHARED_TEST_PRESET_TEMPLATE_ROOT = path.join(SHARED_PRESET_TEMPLATE_ROOT, "test-preset");
|
|
42
42
|
export const SHARED_WP_ENV_PRESET_TEMPLATE_ROOT = path.join(SHARED_PRESET_TEMPLATE_ROOT, "wp-env");
|
|
43
43
|
export const BUILTIN_TEMPLATE_IDS = ["basic", "interactivity", "persistence", "compound"];
|
|
44
|
+
export const OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE = "@wp-typia/create-workspace-template";
|
|
44
45
|
export const TEMPLATE_REGISTRY = Object.freeze([
|
|
45
46
|
{
|
|
46
47
|
id: "basic",
|
|
@@ -70,8 +71,15 @@ export const TEMPLATE_REGISTRY = Object.freeze([
|
|
|
70
71
|
features: ["InnerBlocks", "Hidden child blocks", "Optional persistence layer"],
|
|
71
72
|
templateDir: path.join(TEMPLATE_ROOT, "compound"),
|
|
72
73
|
},
|
|
74
|
+
{
|
|
75
|
+
id: OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE,
|
|
76
|
+
description: "The official empty workspace template that powers `wp-typia add ...` workflows",
|
|
77
|
+
defaultCategory: "workspace",
|
|
78
|
+
features: ["Workspace inventory", "Add block workflows", "Workspace doctor and migrate"],
|
|
79
|
+
templateDir: path.resolve(PROJECT_TOOLS_PACKAGE_ROOT, "..", "create-workspace-template"),
|
|
80
|
+
},
|
|
73
81
|
]);
|
|
74
|
-
export const TEMPLATE_IDS =
|
|
82
|
+
export const TEMPLATE_IDS = [...BUILTIN_TEMPLATE_IDS];
|
|
75
83
|
export function isBuiltInTemplateId(templateId) {
|
|
76
84
|
return BUILTIN_TEMPLATE_IDS.includes(templateId);
|
|
77
85
|
}
|
|
@@ -81,7 +89,10 @@ export function listTemplates() {
|
|
|
81
89
|
export function getTemplateById(templateId) {
|
|
82
90
|
const template = TEMPLATE_REGISTRY.find((entry) => entry.id === templateId);
|
|
83
91
|
if (!template) {
|
|
84
|
-
throw new Error(`Unknown template "${templateId}". Expected one of: ${
|
|
92
|
+
throw new Error(`Unknown template "${templateId}". Expected one of: ${[
|
|
93
|
+
...TEMPLATE_IDS,
|
|
94
|
+
OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE,
|
|
95
|
+
].join(", ")}`);
|
|
85
96
|
}
|
|
86
97
|
return template;
|
|
87
98
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wp-typia/project-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"description": "Project orchestration and programmatic tooling for wp-typia",
|
|
5
5
|
"packageManager": "bun@1.3.11",
|
|
6
6
|
"type": "module",
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"npm-package-arg": "^13.0.0",
|
|
70
70
|
"semver": "^7.7.3",
|
|
71
71
|
"tar": "^7.4.3",
|
|
72
|
+
"typia": "^12.0.1",
|
|
72
73
|
"typescript": "^5.9.2"
|
|
73
74
|
},
|
|
74
75
|
"devDependencies": {
|
|
@@ -2,14 +2,7 @@
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
|
|
5
|
-
const PARENT_BLOCK_NAME = '{{namespace}}/{{slugKebabCase}}';
|
|
6
|
-
const PARENT_BLOCK_NAMESPACE = '{{namespace}}';
|
|
7
|
-
const PARENT_BLOCK_SLUG = '{{slugKebabCase}}';
|
|
8
|
-
const PARENT_BLOCK_TITLE = {{titleJson}};
|
|
9
|
-
const PARENT_TYPE_NAME = '{{pascalCase}}';
|
|
10
|
-
const PARENT_STYLE_IMPORT = '../{{slugKebabCase}}/style.scss';
|
|
11
5
|
const PROJECT_ROOT = process.cwd();
|
|
12
|
-
const TEXT_DOMAIN = '{{textDomain}}';
|
|
13
6
|
|
|
14
7
|
const ALLOWED_CHILD_MARKER = '// add-child: insert new allowed child block names here';
|
|
15
8
|
const BLOCK_CONFIG_MARKER = '// add-child: insert new block config entries here';
|
|
@@ -22,6 +15,16 @@ type StarterManifestDocument = {
|
|
|
22
15
|
sourceType: string;
|
|
23
16
|
};
|
|
24
17
|
|
|
18
|
+
type CompoundParentConfig = {
|
|
19
|
+
blockName: string;
|
|
20
|
+
namespace: string;
|
|
21
|
+
slug: string;
|
|
22
|
+
styleImport: string;
|
|
23
|
+
textDomain: string;
|
|
24
|
+
title: string;
|
|
25
|
+
typeName: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
25
28
|
function parseArgs() {
|
|
26
29
|
const args = process.argv.slice( 2 );
|
|
27
30
|
const parsed: {
|
|
@@ -81,6 +84,26 @@ function toTitleCase( input: string ): string {
|
|
|
81
84
|
.join( ' ' );
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
function readJsonFile( filePath: string ): Record< string, unknown > {
|
|
88
|
+
let parsed: unknown;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
parsed = JSON.parse( fs.readFileSync( filePath, 'utf8' ) );
|
|
92
|
+
} catch ( error ) {
|
|
93
|
+
const errorMessage = error instanceof Error ? error.message : String( error );
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Unable to parse JSON from ${ filePath }: ${ errorMessage }`,
|
|
96
|
+
{ cause: error instanceof Error ? error : undefined }
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if ( ! parsed || typeof parsed !== 'object' || Array.isArray( parsed ) ) {
|
|
101
|
+
throw new Error( `${ filePath } must contain a JSON object.` );
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return parsed as Record< string, unknown >;
|
|
105
|
+
}
|
|
106
|
+
|
|
84
107
|
function resolveValidatedNamespace( value: string ): string {
|
|
85
108
|
const normalizedNamespace = toKebabCase( value );
|
|
86
109
|
|
|
@@ -101,6 +124,79 @@ function resolveValidatedBlockSlug( value: string ): string {
|
|
|
101
124
|
return normalizedSlug;
|
|
102
125
|
}
|
|
103
126
|
|
|
127
|
+
function resolveCompoundParentConfig(): CompoundParentConfig {
|
|
128
|
+
const blocksRoot = path.join( PROJECT_ROOT, 'src', 'blocks' );
|
|
129
|
+
|
|
130
|
+
if ( ! fs.existsSync( blocksRoot ) ) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
'This command expects a compound scaffold with src/blocks/<parent>/children.ts and scripts/block-config.ts.'
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const parentCandidates = fs
|
|
137
|
+
.readdirSync( blocksRoot, { withFileTypes: true } )
|
|
138
|
+
.filter( ( entry ) => entry.isDirectory() )
|
|
139
|
+
.map( ( entry ) => path.join( blocksRoot, entry.name ) )
|
|
140
|
+
.filter( ( candidateDir ) =>
|
|
141
|
+
fs.existsSync( path.join( candidateDir, 'children.ts' ) )
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
if ( parentCandidates.length !== 1 ) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Unable to resolve the compound parent block. Expected exactly one src/blocks/<parent>/children.ts entry, found ${ parentCandidates.length }.`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const parentDir = parentCandidates[ 0 ];
|
|
151
|
+
const parentSlug = resolveValidatedBlockSlug( path.basename( parentDir ) );
|
|
152
|
+
const blockJsonPath = path.join( parentDir, 'block.json' );
|
|
153
|
+
|
|
154
|
+
if ( ! fs.existsSync( blockJsonPath ) ) {
|
|
155
|
+
throw new Error( `Unable to resolve ${ blockJsonPath } for the compound parent block.` );
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const blockJson = readJsonFile( blockJsonPath );
|
|
159
|
+
const blockName = typeof blockJson.name === 'string' ? blockJson.name.trim() : '';
|
|
160
|
+
const separatorIndex = blockName.indexOf( '/' );
|
|
161
|
+
|
|
162
|
+
if ( separatorIndex <= 0 || separatorIndex === blockName.length - 1 ) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`The parent block metadata at ${ blockJsonPath } must declare a valid "name" like "namespace/slug".`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const namespace = resolveValidatedNamespace( blockName.slice( 0, separatorIndex ) );
|
|
169
|
+
const title =
|
|
170
|
+
typeof blockJson.title === 'string' && blockJson.title.trim().length > 0
|
|
171
|
+
? blockJson.title.trim()
|
|
172
|
+
: toTitleCase( parentSlug );
|
|
173
|
+
const textDomain =
|
|
174
|
+
typeof blockJson.textdomain === 'string' &&
|
|
175
|
+
blockJson.textdomain.trim().length > 0
|
|
176
|
+
? blockJson.textdomain.trim()
|
|
177
|
+
: parentSlug;
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
blockName,
|
|
181
|
+
namespace,
|
|
182
|
+
slug: parentSlug,
|
|
183
|
+
styleImport: `../${ parentSlug }/style.scss`,
|
|
184
|
+
textDomain,
|
|
185
|
+
title,
|
|
186
|
+
typeName: toPascalCase( parentSlug ),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const {
|
|
191
|
+
blockName: PARENT_BLOCK_NAME,
|
|
192
|
+
namespace: PARENT_BLOCK_NAMESPACE,
|
|
193
|
+
slug: PARENT_BLOCK_SLUG,
|
|
194
|
+
styleImport: PARENT_STYLE_IMPORT,
|
|
195
|
+
textDomain: TEXT_DOMAIN,
|
|
196
|
+
title: PARENT_BLOCK_TITLE,
|
|
197
|
+
typeName: PARENT_TYPE_NAME,
|
|
198
|
+
} = resolveCompoundParentConfig();
|
|
199
|
+
|
|
104
200
|
function buildBlockCssClassName( namespace: string, slug: string ): string {
|
|
105
201
|
const normalizedSlug = resolveValidatedBlockSlug( slug );
|
|
106
202
|
const normalizedNamespace =
|
|
@@ -9,9 +9,15 @@ export const BLOCKS = [
|
|
|
9
9
|
'state-query': {
|
|
10
10
|
sourceTypeName: '{{pascalCase}}StateQuery',
|
|
11
11
|
},
|
|
12
|
+
'bootstrap-query': {
|
|
13
|
+
sourceTypeName: '{{pascalCase}}BootstrapQuery',
|
|
14
|
+
},
|
|
12
15
|
'write-state-request': {
|
|
13
16
|
sourceTypeName: '{{pascalCase}}WriteStateRequest',
|
|
14
17
|
},
|
|
18
|
+
'bootstrap-response': {
|
|
19
|
+
sourceTypeName: '{{pascalCase}}BootstrapResponse',
|
|
20
|
+
},
|
|
15
21
|
'state-response': {
|
|
16
22
|
sourceTypeName: '{{pascalCase}}StateResponse',
|
|
17
23
|
},
|
|
@@ -40,6 +46,16 @@ export const BLOCKS = [
|
|
|
40
46
|
mechanism: '{{restWriteAuthMechanism}}',
|
|
41
47
|
},
|
|
42
48
|
},
|
|
49
|
+
{
|
|
50
|
+
auth: 'public',
|
|
51
|
+
method: 'GET',
|
|
52
|
+
operationId: 'get{{pascalCase}}Bootstrap',
|
|
53
|
+
path: '/{{namespace}}/v1/{{slugKebabCase}}/bootstrap',
|
|
54
|
+
queryContract: 'bootstrap-query',
|
|
55
|
+
responseContract: 'bootstrap-response',
|
|
56
|
+
summary: 'Read fresh session bootstrap state for the current viewer.',
|
|
57
|
+
tags: [ '{{title}}' ],
|
|
58
|
+
},
|
|
43
59
|
],
|
|
44
60
|
info: {
|
|
45
61
|
title: '{{title}} REST API',
|
package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api-types.ts.mustache
CHANGED
|
@@ -5,6 +5,11 @@ export interface {{pascalCase}}StateQuery {
|
|
|
5
5
|
resourceKey: string & tags.MinLength< 1 > & tags.MaxLength< 100 >;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
export interface {{pascalCase}}BootstrapQuery {
|
|
9
|
+
postId: number & tags.Type< 'uint32' >;
|
|
10
|
+
resourceKey: string & tags.MinLength< 1 > & tags.MaxLength< 100 >;
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
export interface {{pascalCase}}WriteStateRequest {
|
|
9
14
|
postId: number & tags.Type< 'uint32' >;
|
|
10
15
|
{{publicWriteRequestIdDeclaration}}
|
|
@@ -13,6 +18,11 @@ export interface {{pascalCase}}WriteStateRequest {
|
|
|
13
18
|
delta?: number & tags.Minimum< 1 > & tags.Type< 'uint32' > & tags.Default< 1 >;
|
|
14
19
|
}
|
|
15
20
|
|
|
21
|
+
export interface {{pascalCase}}BootstrapResponse {
|
|
22
|
+
canWrite: boolean;
|
|
23
|
+
{{bootstrapCredentialDeclarations}}
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
export interface {{pascalCase}}StateResponse {
|
|
17
27
|
postId: number & tags.Type< 'uint32' >;
|
|
18
28
|
resourceKey: string & tags.MinLength< 1 > & tags.MaxLength< 100 >;
|
|
@@ -3,10 +3,12 @@ import {
|
|
|
3
3
|
} from '@wp-typia/rest';
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
+
type {{pascalCase}}BootstrapQuery,
|
|
6
7
|
type {{pascalCase}}StateQuery,
|
|
7
8
|
type {{pascalCase}}WriteStateRequest,
|
|
8
9
|
} from './api-types';
|
|
9
10
|
import {
|
|
11
|
+
get{{pascalCase}}BootstrapEndpoint,
|
|
10
12
|
get{{pascalCase}}StateEndpoint,
|
|
11
13
|
write{{pascalCase}}StateEndpoint,
|
|
12
14
|
} from './api-client';
|
|
@@ -15,6 +17,10 @@ import {
|
|
|
15
17
|
type PersistenceTransportOptions,
|
|
16
18
|
} from './transport';
|
|
17
19
|
|
|
20
|
+
export const bootstrapEndpoint = {
|
|
21
|
+
...get{{pascalCase}}BootstrapEndpoint,
|
|
22
|
+
};
|
|
23
|
+
|
|
18
24
|
export const stateEndpoint = {
|
|
19
25
|
...get{{pascalCase}}StateEndpoint,
|
|
20
26
|
};
|
|
@@ -40,6 +46,23 @@ export function fetchState(
|
|
|
40
46
|
);
|
|
41
47
|
}
|
|
42
48
|
|
|
49
|
+
export function fetchBootstrap(
|
|
50
|
+
request: {{pascalCase}}BootstrapQuery,
|
|
51
|
+
options: PersistenceTransportOptions = {}
|
|
52
|
+
) {
|
|
53
|
+
return callEndpoint(
|
|
54
|
+
bootstrapEndpoint,
|
|
55
|
+
request,
|
|
56
|
+
resolveTransportCallOptions(
|
|
57
|
+
options.transportTarget ?? 'frontend',
|
|
58
|
+
'read',
|
|
59
|
+
bootstrapEndpoint,
|
|
60
|
+
request,
|
|
61
|
+
options
|
|
62
|
+
)
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
43
66
|
export function writeState(
|
|
44
67
|
request: {{pascalCase}}WriteStateRequest,
|
|
45
68
|
options: PersistenceTransportOptions = {}
|
package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/data.ts.mustache
CHANGED
|
@@ -6,11 +6,14 @@ import {
|
|
|
6
6
|
} from '@wp-typia/rest/react';
|
|
7
7
|
|
|
8
8
|
import type {
|
|
9
|
+
{{pascalCase}}BootstrapQuery,
|
|
10
|
+
{{pascalCase}}BootstrapResponse,
|
|
9
11
|
{{pascalCase}}StateQuery,
|
|
10
12
|
{{pascalCase}}StateResponse,
|
|
11
13
|
{{pascalCase}}WriteStateRequest,
|
|
12
14
|
} from './api-types';
|
|
13
15
|
import {
|
|
16
|
+
bootstrapEndpoint,
|
|
14
17
|
stateEndpoint,
|
|
15
18
|
writeStateEndpoint,
|
|
16
19
|
} from './api';
|
|
@@ -40,6 +43,20 @@ export interface Use{{pascalCase}}StateQueryOptions<
|
|
|
40
43
|
transportTarget?: PersistenceTransportTarget;
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
export interface Use{{pascalCase}}BootstrapQueryOptions<
|
|
47
|
+
Selected = {{pascalCase}}BootstrapResponse,
|
|
48
|
+
> extends Omit<
|
|
49
|
+
UseEndpointQueryOptions<
|
|
50
|
+
{{pascalCase}}BootstrapQuery,
|
|
51
|
+
{{pascalCase}}BootstrapResponse,
|
|
52
|
+
Selected
|
|
53
|
+
>,
|
|
54
|
+
'resolveCallOptions'
|
|
55
|
+
> {
|
|
56
|
+
restNonce?: string;
|
|
57
|
+
transportTarget?: PersistenceTransportTarget;
|
|
58
|
+
}
|
|
59
|
+
|
|
43
60
|
export interface UseWrite{{pascalCase}}StateMutationOptions<
|
|
44
61
|
Context = unknown,
|
|
45
62
|
> extends Omit<
|
|
@@ -92,6 +109,34 @@ export function use{{pascalCase}}StateQuery<
|
|
|
92
109
|
} );
|
|
93
110
|
}
|
|
94
111
|
|
|
112
|
+
export function use{{pascalCase}}BootstrapQuery<
|
|
113
|
+
Selected = {{pascalCase}}BootstrapResponse,
|
|
114
|
+
>(
|
|
115
|
+
request: {{pascalCase}}BootstrapQuery,
|
|
116
|
+
options: Use{{pascalCase}}BootstrapQueryOptions< Selected > = {}
|
|
117
|
+
) {
|
|
118
|
+
const {
|
|
119
|
+
restNonce,
|
|
120
|
+
transportTarget = 'editor',
|
|
121
|
+
...queryOptions
|
|
122
|
+
} = options;
|
|
123
|
+
|
|
124
|
+
return useEndpointQuery( bootstrapEndpoint, request, {
|
|
125
|
+
...queryOptions,
|
|
126
|
+
resolveCallOptions: () =>
|
|
127
|
+
resolveTransportCallOptions(
|
|
128
|
+
transportTarget,
|
|
129
|
+
'read',
|
|
130
|
+
bootstrapEndpoint,
|
|
131
|
+
request,
|
|
132
|
+
{
|
|
133
|
+
restNonce,
|
|
134
|
+
transportTarget,
|
|
135
|
+
}
|
|
136
|
+
),
|
|
137
|
+
} );
|
|
138
|
+
}
|
|
139
|
+
|
|
95
140
|
export function useWrite{{pascalCase}}StateMutation<
|
|
96
141
|
Context = unknown,
|
|
97
142
|
>(
|