@wp-typia/project-tools 0.15.0 → 0.15.2
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/json-utils.d.ts +5 -8
- package/dist/runtime/json-utils.js +5 -10
- package/dist/runtime/metadata-analysis.d.ts +7 -11
- package/dist/runtime/metadata-analysis.js +7 -285
- package/dist/runtime/metadata-model.d.ts +7 -84
- package/dist/runtime/metadata-model.js +7 -59
- package/dist/runtime/metadata-parser.d.ts +5 -51
- package/dist/runtime/metadata-parser.js +5 -792
- package/dist/runtime/metadata-php-render.d.ts +5 -27
- package/dist/runtime/metadata-php-render.js +5 -547
- package/dist/runtime/metadata-projection.d.ts +7 -7
- package/dist/runtime/metadata-projection.js +7 -233
- package/dist/runtime/object-utils.d.ts +1 -1
- package/dist/runtime/object-utils.js +3 -6
- package/dist/runtime/persistence-rest-artifacts.d.ts +76 -0
- package/dist/runtime/persistence-rest-artifacts.js +99 -0
- package/dist/runtime/scaffold.d.ts +10 -2
- package/dist/runtime/scaffold.js +95 -1
- 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 +9 -8
- package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +103 -7
- package/templates/_shared/persistence/core/src/api-validators.ts.mustache +14 -0
- package/templates/_shared/persistence/core/src/api.ts.mustache +28 -9
- package/templates/_shared/persistence/core/src/interactivity.ts.mustache +17 -11
- 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 +6 -6
|
@@ -1,233 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
attribute.default = cloneJsonValue(node.defaultValue);
|
|
9
|
-
}
|
|
10
|
-
if (node.enumValues !== null && node.enumValues.length > 0) {
|
|
11
|
-
attribute.enum = [...node.enumValues];
|
|
12
|
-
}
|
|
13
|
-
if (node.wp.source !== null) {
|
|
14
|
-
attribute.source = node.wp.source;
|
|
15
|
-
}
|
|
16
|
-
if (node.wp.selector !== null) {
|
|
17
|
-
attribute.selector = node.wp.selector;
|
|
18
|
-
}
|
|
19
|
-
const reasons = [];
|
|
20
|
-
if (node.constraints.exclusiveMaximum !== null)
|
|
21
|
-
reasons.push("exclusiveMaximum");
|
|
22
|
-
if (node.constraints.exclusiveMinimum !== null)
|
|
23
|
-
reasons.push("exclusiveMinimum");
|
|
24
|
-
if (node.constraints.format !== null)
|
|
25
|
-
reasons.push("format");
|
|
26
|
-
if (node.constraints.maxLength !== null)
|
|
27
|
-
reasons.push("maxLength");
|
|
28
|
-
if (node.constraints.maxItems !== null)
|
|
29
|
-
reasons.push("maxItems");
|
|
30
|
-
if (node.constraints.maximum !== null)
|
|
31
|
-
reasons.push("maximum");
|
|
32
|
-
if (node.constraints.minLength !== null)
|
|
33
|
-
reasons.push("minLength");
|
|
34
|
-
if (node.constraints.minItems !== null)
|
|
35
|
-
reasons.push("minItems");
|
|
36
|
-
if (node.constraints.minimum !== null)
|
|
37
|
-
reasons.push("minimum");
|
|
38
|
-
if (node.constraints.multipleOf !== null)
|
|
39
|
-
reasons.push("multipleOf");
|
|
40
|
-
if (node.constraints.pattern !== null)
|
|
41
|
-
reasons.push("pattern");
|
|
42
|
-
if (node.constraints.typeTag !== null)
|
|
43
|
-
reasons.push("typeTag");
|
|
44
|
-
if (node.kind === "array" && node.items !== undefined)
|
|
45
|
-
reasons.push("items");
|
|
46
|
-
if (node.kind === "object" && node.properties !== undefined)
|
|
47
|
-
reasons.push("properties");
|
|
48
|
-
if (node.kind === "union" && node.union !== null)
|
|
49
|
-
reasons.push("union");
|
|
50
|
-
if (reasons.length > 0) {
|
|
51
|
-
warnings.push(`${node.path}: ${reasons.join(", ")}`);
|
|
52
|
-
}
|
|
53
|
-
return attribute;
|
|
54
|
-
}
|
|
55
|
-
export function createManifestAttribute(node) {
|
|
56
|
-
return {
|
|
57
|
-
typia: {
|
|
58
|
-
constraints: { ...node.constraints },
|
|
59
|
-
defaultValue: node.defaultValue === undefined ? null : cloneJsonValue(node.defaultValue),
|
|
60
|
-
hasDefault: node.defaultValue !== undefined,
|
|
61
|
-
},
|
|
62
|
-
ts: {
|
|
63
|
-
items: node.items ? createManifestAttribute(node.items) : null,
|
|
64
|
-
kind: node.kind,
|
|
65
|
-
properties: node.properties
|
|
66
|
-
? Object.fromEntries(Object.entries(node.properties).map(([key, property]) => [
|
|
67
|
-
key,
|
|
68
|
-
createManifestAttribute(property),
|
|
69
|
-
]))
|
|
70
|
-
: null,
|
|
71
|
-
required: node.required,
|
|
72
|
-
union: node.union
|
|
73
|
-
? {
|
|
74
|
-
branches: Object.fromEntries(Object.entries(node.union.branches).map(([key, branch]) => [
|
|
75
|
-
key,
|
|
76
|
-
createManifestAttribute(branch),
|
|
77
|
-
])),
|
|
78
|
-
discriminator: node.union.discriminator,
|
|
79
|
-
}
|
|
80
|
-
: null,
|
|
81
|
-
},
|
|
82
|
-
wp: {
|
|
83
|
-
defaultValue: node.defaultValue === undefined ? null : cloneJsonValue(node.defaultValue),
|
|
84
|
-
enum: node.enumValues ? [...node.enumValues] : null,
|
|
85
|
-
hasDefault: node.defaultValue !== undefined,
|
|
86
|
-
...(node.wp.selector !== null ? { selector: node.wp.selector } : {}),
|
|
87
|
-
...(node.wp.source !== null ? { source: node.wp.source } : {}),
|
|
88
|
-
type: getWordPressKind(node),
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
export function createManifestDocument(sourceTypeName, attributes) {
|
|
93
|
-
return {
|
|
94
|
-
attributes: Object.fromEntries(Object.entries(attributes).map(([key, node]) => [
|
|
95
|
-
key,
|
|
96
|
-
createManifestAttribute(node),
|
|
97
|
-
])),
|
|
98
|
-
manifestVersion: 2,
|
|
99
|
-
sourceType: sourceTypeName,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
export function validateWordPressExtractionAttributes(attributes) {
|
|
103
|
-
for (const attribute of Object.values(attributes)) {
|
|
104
|
-
validateWordPressExtractionAttribute(attribute, true);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
export function validateWordPressExtractionAttribute(node, isTopLevel = false) {
|
|
108
|
-
const hasSelector = node.wp.selector !== null;
|
|
109
|
-
const hasSource = node.wp.source !== null;
|
|
110
|
-
if (hasSelector || hasSource) {
|
|
111
|
-
if (!isTopLevel) {
|
|
112
|
-
throw new Error(`WordPress extraction tags are only supported on top-level block attributes at ${node.path}`);
|
|
113
|
-
}
|
|
114
|
-
if (!hasSelector || !hasSource) {
|
|
115
|
-
throw new Error(`WordPress extraction tags require both Source and Selector at ${node.path}`);
|
|
116
|
-
}
|
|
117
|
-
if (node.kind !== "string") {
|
|
118
|
-
throw new Error(`WordPress extraction tags are only supported on string attributes at ${node.path}`);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (node.items !== undefined) {
|
|
122
|
-
validateWordPressExtractionAttribute(node.items, false);
|
|
123
|
-
}
|
|
124
|
-
if (node.properties !== undefined) {
|
|
125
|
-
for (const property of Object.values(node.properties)) {
|
|
126
|
-
validateWordPressExtractionAttribute(property, false);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
if (node.union?.branches) {
|
|
130
|
-
for (const branch of Object.values(node.union.branches)) {
|
|
131
|
-
validateWordPressExtractionAttribute(branch, false);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
export function createExampleValue(node, key) {
|
|
136
|
-
if (node.defaultValue !== undefined) {
|
|
137
|
-
return cloneJsonValue(node.defaultValue);
|
|
138
|
-
}
|
|
139
|
-
if (node.enumValues !== null && node.enumValues.length > 0) {
|
|
140
|
-
return cloneJsonValue(node.enumValues[0]);
|
|
141
|
-
}
|
|
142
|
-
switch (node.kind) {
|
|
143
|
-
case "string":
|
|
144
|
-
return fitStringExampleToConstraints(createFormattedStringExample(node.constraints.format, key), node.constraints);
|
|
145
|
-
case "number":
|
|
146
|
-
return createNumericExample(node);
|
|
147
|
-
case "boolean":
|
|
148
|
-
return true;
|
|
149
|
-
case "array":
|
|
150
|
-
return createArrayExample(node, key);
|
|
151
|
-
case "object":
|
|
152
|
-
return Object.fromEntries(Object.entries(node.properties ?? {}).map(([propertyKey, propertyNode]) => [
|
|
153
|
-
propertyKey,
|
|
154
|
-
createExampleValue(propertyNode, propertyKey),
|
|
155
|
-
]));
|
|
156
|
-
case "union": {
|
|
157
|
-
const firstBranch = node.union
|
|
158
|
-
? Object.values(node.union.branches)[0]
|
|
159
|
-
: undefined;
|
|
160
|
-
if (!firstBranch || firstBranch.kind !== "object") {
|
|
161
|
-
return {};
|
|
162
|
-
}
|
|
163
|
-
return Object.fromEntries(Object.entries(firstBranch.properties ?? {}).map(([propertyKey, propertyNode]) => [
|
|
164
|
-
propertyKey,
|
|
165
|
-
createExampleValue(propertyNode, propertyKey),
|
|
166
|
-
]));
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
function createFormattedStringExample(format, key) {
|
|
171
|
-
switch (format) {
|
|
172
|
-
case "uuid":
|
|
173
|
-
return "00000000-0000-4000-8000-000000000000";
|
|
174
|
-
case "email":
|
|
175
|
-
return "example@example.com";
|
|
176
|
-
case "url":
|
|
177
|
-
case "uri":
|
|
178
|
-
return "https://example.com";
|
|
179
|
-
case "ipv4":
|
|
180
|
-
return "127.0.0.1";
|
|
181
|
-
case "ipv6":
|
|
182
|
-
return "::1";
|
|
183
|
-
case "date-time":
|
|
184
|
-
return "2024-01-01T00:00:00Z";
|
|
185
|
-
default:
|
|
186
|
-
return `Example ${key}`;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
function fitStringExampleToConstraints(value, constraints) {
|
|
190
|
-
let nextValue = value;
|
|
191
|
-
if (constraints.maxLength !== null &&
|
|
192
|
-
nextValue.length > constraints.maxLength) {
|
|
193
|
-
nextValue = nextValue.slice(0, constraints.maxLength);
|
|
194
|
-
}
|
|
195
|
-
if (constraints.minLength !== null &&
|
|
196
|
-
nextValue.length < constraints.minLength) {
|
|
197
|
-
nextValue = nextValue.padEnd(constraints.minLength, "x");
|
|
198
|
-
}
|
|
199
|
-
return nextValue;
|
|
200
|
-
}
|
|
201
|
-
function createNumericExample(node) {
|
|
202
|
-
const { exclusiveMaximum, exclusiveMinimum, maximum, minimum, multipleOf, } = node.constraints;
|
|
203
|
-
const step = multipleOf && multipleOf !== 0 ? multipleOf : 1;
|
|
204
|
-
const minCandidate = minimum ?? (exclusiveMinimum !== null ? exclusiveMinimum + step : null);
|
|
205
|
-
const maxCandidate = maximum ?? (exclusiveMaximum !== null ? exclusiveMaximum - step : null);
|
|
206
|
-
let candidate = multipleOf && multipleOf !== 0
|
|
207
|
-
? minCandidate !== null
|
|
208
|
-
? Math.ceil(minCandidate / multipleOf) * multipleOf
|
|
209
|
-
: maxCandidate !== null
|
|
210
|
-
? Math.floor(maxCandidate / multipleOf) * multipleOf
|
|
211
|
-
: multipleOf
|
|
212
|
-
: minCandidate ?? maxCandidate ?? 42;
|
|
213
|
-
if (maxCandidate !== null && candidate > maxCandidate) {
|
|
214
|
-
candidate =
|
|
215
|
-
multipleOf && multipleOf !== 0
|
|
216
|
-
? Math.floor(maxCandidate / multipleOf) * multipleOf
|
|
217
|
-
: maxCandidate;
|
|
218
|
-
}
|
|
219
|
-
if (minCandidate !== null && candidate < minCandidate) {
|
|
220
|
-
candidate =
|
|
221
|
-
multipleOf && multipleOf !== 0
|
|
222
|
-
? Math.ceil(minCandidate / multipleOf) * multipleOf
|
|
223
|
-
: minCandidate;
|
|
224
|
-
}
|
|
225
|
-
return candidate;
|
|
226
|
-
}
|
|
227
|
-
function createArrayExample(node, key) {
|
|
228
|
-
const minimumItems = Math.max(node.constraints.minItems ?? 0, 0);
|
|
229
|
-
if (minimumItems === 0 || node.items === undefined) {
|
|
230
|
-
return [];
|
|
231
|
-
}
|
|
232
|
-
return Array.from({ length: minimumItems }, (_value, index) => createExampleValue(node.items, `${key}${index + 1}`));
|
|
233
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Re-exports metadata projection utilities from `@wp-typia/block-runtime`.
|
|
3
|
+
* This adapter keeps the public project-tools runtime path stable while the
|
|
4
|
+
* implementation is consolidated in block-runtime.
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
export * from "@wp-typia/block-runtime/metadata-projection";
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export type UnknownRecord = Record<string, unknown>;
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Delegate plain-object detection to the shared runtime primitive owner.
|
|
7
7
|
*
|
|
8
8
|
* @param value Runtime value to inspect.
|
|
9
9
|
* @returns `true` when the value is a non-null plain object with an
|
|
@@ -1,14 +1,11 @@
|
|
|
1
|
+
import { isPlainObject as isSharedPlainObject } from "@wp-typia/api-client/runtime-primitives";
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
+
* Delegate plain-object detection to the shared runtime primitive owner.
|
|
3
4
|
*
|
|
4
5
|
* @param value Runtime value to inspect.
|
|
5
6
|
* @returns `true` when the value is a non-null plain object with an
|
|
6
7
|
* `Object.prototype` or `null` prototype.
|
|
7
8
|
*/
|
|
8
9
|
export function isPlainObject(value) {
|
|
9
|
-
|
|
10
|
-
return false;
|
|
11
|
-
}
|
|
12
|
-
const prototype = Object.getPrototypeOf(value);
|
|
13
|
-
return prototype === Object.prototype || prototype === null;
|
|
10
|
+
return isSharedPlainObject(value);
|
|
14
11
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
interface PersistenceTemplateVariablesLike {
|
|
2
|
+
namespace: string;
|
|
3
|
+
pascalCase: string;
|
|
4
|
+
restWriteAuthIntent: "authenticated" | "public-write-protected";
|
|
5
|
+
restWriteAuthMechanism: "public-signed-token" | "rest-nonce";
|
|
6
|
+
slugKebabCase: string;
|
|
7
|
+
title: string;
|
|
8
|
+
}
|
|
9
|
+
interface SyncPersistenceRestArtifactsOptions {
|
|
10
|
+
apiTypesFile: string;
|
|
11
|
+
outputDir: string;
|
|
12
|
+
projectDir: string;
|
|
13
|
+
variables: PersistenceTemplateVariablesLike;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Build the canonical persistence REST endpoint manifest for scaffold-time
|
|
17
|
+
* schema, OpenAPI, and client generation.
|
|
18
|
+
*
|
|
19
|
+
* @param variables Persistence template naming and auth metadata.
|
|
20
|
+
* @returns Endpoint manifest covering bootstrap, state read, and state write operations.
|
|
21
|
+
*/
|
|
22
|
+
export declare function buildPersistenceEndpointManifest(variables: PersistenceTemplateVariablesLike): import("@wp-typia/block-runtime/metadata-core").EndpointManifestDefinition<{
|
|
23
|
+
readonly "bootstrap-query": {
|
|
24
|
+
readonly sourceTypeName: `${string}BootstrapQuery`;
|
|
25
|
+
};
|
|
26
|
+
readonly "bootstrap-response": {
|
|
27
|
+
readonly sourceTypeName: `${string}BootstrapResponse`;
|
|
28
|
+
};
|
|
29
|
+
readonly "state-query": {
|
|
30
|
+
readonly sourceTypeName: `${string}StateQuery`;
|
|
31
|
+
};
|
|
32
|
+
readonly "state-response": {
|
|
33
|
+
readonly sourceTypeName: `${string}StateResponse`;
|
|
34
|
+
};
|
|
35
|
+
readonly "write-state-request": {
|
|
36
|
+
readonly sourceTypeName: `${string}WriteStateRequest`;
|
|
37
|
+
};
|
|
38
|
+
}, readonly [{
|
|
39
|
+
readonly auth: "public";
|
|
40
|
+
readonly method: "GET";
|
|
41
|
+
readonly operationId: `get${string}State`;
|
|
42
|
+
readonly path: `/${string}/v1/${string}/state`;
|
|
43
|
+
readonly queryContract: "state-query";
|
|
44
|
+
readonly responseContract: "state-response";
|
|
45
|
+
readonly summary: "Read the current persisted state.";
|
|
46
|
+
readonly tags: readonly [string];
|
|
47
|
+
}, {
|
|
48
|
+
readonly auth: "authenticated" | "public-write-protected";
|
|
49
|
+
readonly bodyContract: "write-state-request";
|
|
50
|
+
readonly method: "POST";
|
|
51
|
+
readonly operationId: `write${string}State`;
|
|
52
|
+
readonly path: `/${string}/v1/${string}/state`;
|
|
53
|
+
readonly responseContract: "state-response";
|
|
54
|
+
readonly summary: "Write the current persisted state.";
|
|
55
|
+
readonly tags: readonly [string];
|
|
56
|
+
readonly wordpressAuth: {
|
|
57
|
+
readonly mechanism: "public-signed-token" | "rest-nonce";
|
|
58
|
+
};
|
|
59
|
+
}, {
|
|
60
|
+
readonly auth: "public";
|
|
61
|
+
readonly method: "GET";
|
|
62
|
+
readonly operationId: `get${string}Bootstrap`;
|
|
63
|
+
readonly path: `/${string}/v1/${string}/bootstrap`;
|
|
64
|
+
readonly queryContract: "bootstrap-query";
|
|
65
|
+
readonly responseContract: "bootstrap-response";
|
|
66
|
+
readonly summary: "Read fresh session bootstrap state for the current viewer.";
|
|
67
|
+
readonly tags: readonly [string];
|
|
68
|
+
}]>;
|
|
69
|
+
/**
|
|
70
|
+
* Generate the REST-derived persistence artifacts for a scaffolded block.
|
|
71
|
+
*
|
|
72
|
+
* @param options Scaffold output paths plus persistence template variables.
|
|
73
|
+
* @returns A promise that resolves after schema, OpenAPI, and client files are written.
|
|
74
|
+
*/
|
|
75
|
+
export declare function syncPersistenceRestArtifacts({ apiTypesFile, outputDir, projectDir, variables, }: SyncPersistenceRestArtifactsOptions): Promise<void>;
|
|
76
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { defineEndpointManifest, syncEndpointClient, syncRestOpenApi, syncTypeSchemas, } from "@wp-typia/block-runtime/metadata-core";
|
|
3
|
+
/**
|
|
4
|
+
* Build the canonical persistence REST endpoint manifest for scaffold-time
|
|
5
|
+
* schema, OpenAPI, and client generation.
|
|
6
|
+
*
|
|
7
|
+
* @param variables Persistence template naming and auth metadata.
|
|
8
|
+
* @returns Endpoint manifest covering bootstrap, state read, and state write operations.
|
|
9
|
+
*/
|
|
10
|
+
export function buildPersistenceEndpointManifest(variables) {
|
|
11
|
+
return defineEndpointManifest({
|
|
12
|
+
contracts: {
|
|
13
|
+
"bootstrap-query": {
|
|
14
|
+
sourceTypeName: `${variables.pascalCase}BootstrapQuery`,
|
|
15
|
+
},
|
|
16
|
+
"bootstrap-response": {
|
|
17
|
+
sourceTypeName: `${variables.pascalCase}BootstrapResponse`,
|
|
18
|
+
},
|
|
19
|
+
"state-query": {
|
|
20
|
+
sourceTypeName: `${variables.pascalCase}StateQuery`,
|
|
21
|
+
},
|
|
22
|
+
"state-response": {
|
|
23
|
+
sourceTypeName: `${variables.pascalCase}StateResponse`,
|
|
24
|
+
},
|
|
25
|
+
"write-state-request": {
|
|
26
|
+
sourceTypeName: `${variables.pascalCase}WriteStateRequest`,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
endpoints: [
|
|
30
|
+
{
|
|
31
|
+
auth: "public",
|
|
32
|
+
method: "GET",
|
|
33
|
+
operationId: `get${variables.pascalCase}State`,
|
|
34
|
+
path: `/${variables.namespace}/v1/${variables.slugKebabCase}/state`,
|
|
35
|
+
queryContract: "state-query",
|
|
36
|
+
responseContract: "state-response",
|
|
37
|
+
summary: "Read the current persisted state.",
|
|
38
|
+
tags: [variables.title],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
auth: variables.restWriteAuthIntent,
|
|
42
|
+
bodyContract: "write-state-request",
|
|
43
|
+
method: "POST",
|
|
44
|
+
operationId: `write${variables.pascalCase}State`,
|
|
45
|
+
path: `/${variables.namespace}/v1/${variables.slugKebabCase}/state`,
|
|
46
|
+
responseContract: "state-response",
|
|
47
|
+
summary: "Write the current persisted state.",
|
|
48
|
+
tags: [variables.title],
|
|
49
|
+
wordpressAuth: {
|
|
50
|
+
mechanism: variables.restWriteAuthMechanism,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
auth: "public",
|
|
55
|
+
method: "GET",
|
|
56
|
+
operationId: `get${variables.pascalCase}Bootstrap`,
|
|
57
|
+
path: `/${variables.namespace}/v1/${variables.slugKebabCase}/bootstrap`,
|
|
58
|
+
queryContract: "bootstrap-query",
|
|
59
|
+
responseContract: "bootstrap-response",
|
|
60
|
+
summary: "Read fresh session bootstrap state for the current viewer.",
|
|
61
|
+
tags: [variables.title],
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
info: {
|
|
65
|
+
title: `${variables.title} REST API`,
|
|
66
|
+
version: "1.0.0",
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Generate the REST-derived persistence artifacts for a scaffolded block.
|
|
72
|
+
*
|
|
73
|
+
* @param options Scaffold output paths plus persistence template variables.
|
|
74
|
+
* @returns A promise that resolves after schema, OpenAPI, and client files are written.
|
|
75
|
+
*/
|
|
76
|
+
export async function syncPersistenceRestArtifacts({ apiTypesFile, outputDir, projectDir, variables, }) {
|
|
77
|
+
const manifest = buildPersistenceEndpointManifest(variables);
|
|
78
|
+
for (const [baseName, contract] of Object.entries(manifest.contracts)) {
|
|
79
|
+
await syncTypeSchemas({
|
|
80
|
+
jsonSchemaFile: path.join(outputDir, "api-schemas", `${baseName}.schema.json`),
|
|
81
|
+
openApiFile: path.join(outputDir, "api-schemas", `${baseName}.openapi.json`),
|
|
82
|
+
projectRoot: projectDir,
|
|
83
|
+
sourceTypeName: contract.sourceTypeName,
|
|
84
|
+
typesFile: apiTypesFile,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
await syncRestOpenApi({
|
|
88
|
+
manifest,
|
|
89
|
+
openApiFile: path.join(outputDir, "api.openapi.json"),
|
|
90
|
+
projectRoot: projectDir,
|
|
91
|
+
typesFile: apiTypesFile,
|
|
92
|
+
});
|
|
93
|
+
await syncEndpointClient({
|
|
94
|
+
clientFile: path.join(outputDir, "api-client.ts"),
|
|
95
|
+
manifest,
|
|
96
|
+
projectRoot: projectDir,
|
|
97
|
+
typesFile: apiTypesFile,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { PackageManagerId } from "./package-managers.js";
|
|
2
|
-
import type { BuiltInTemplateId } from "./template-registry.js";
|
|
3
2
|
/**
|
|
4
3
|
* User-facing scaffold answers before template rendering.
|
|
5
4
|
*
|
|
@@ -57,6 +56,7 @@ export interface ScaffoldTemplateVariables extends Record<string, string> {
|
|
|
57
56
|
isAuthenticatedPersistencePolicy: "false" | "true";
|
|
58
57
|
isPublicPersistencePolicy: "false" | "true";
|
|
59
58
|
bootstrapCredentialDeclarations: string;
|
|
59
|
+
persistencePolicyDescriptionJson: string;
|
|
60
60
|
publicWriteRequestIdDeclaration: string;
|
|
61
61
|
restPackageVersion: string;
|
|
62
62
|
restWriteAuthIntent: "authenticated" | "public-write-protected";
|
|
@@ -73,9 +73,17 @@ export interface ScaffoldTemplateVariables extends Record<string, string> {
|
|
|
73
73
|
titleCase: string;
|
|
74
74
|
persistencePolicy: PersistencePolicy;
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolve scaffold template input from either built-in template ids or custom
|
|
78
|
+
* template identifiers such as local paths, GitHub refs, and npm packages.
|
|
79
|
+
*
|
|
80
|
+
* The callback returns `Promise<string>` on purpose so interactive selection
|
|
81
|
+
* can surface custom ids. Downstream code uses `isBuiltInTemplateId()` to
|
|
82
|
+
* distinguish built-in templates from custom sources.
|
|
83
|
+
*/
|
|
76
84
|
interface ResolveTemplateOptions {
|
|
77
85
|
isInteractive?: boolean;
|
|
78
|
-
selectTemplate?: () => Promise<
|
|
86
|
+
selectTemplate?: () => Promise<string>;
|
|
79
87
|
templateId?: string;
|
|
80
88
|
yes?: boolean;
|
|
81
89
|
}
|
package/dist/runtime/scaffold.js
CHANGED
|
@@ -7,17 +7,19 @@ import { applyGeneratedProjectDxPackageJson, applyLocalDevPresetFiles, getPrimar
|
|
|
7
7
|
import { applyMigrationUiCapability } from "./migration-ui-capability.js";
|
|
8
8
|
import { getPackageVersions } from "./package-versions.js";
|
|
9
9
|
import { ensureMigrationDirectories, writeInitialMigrationScaffold, writeMigrationConfig, } from "./migration-project.js";
|
|
10
|
+
import { syncPersistenceRestArtifacts } from "./persistence-rest-artifacts.js";
|
|
10
11
|
import { getCompoundExtensionWorkflowSection, getOptionalOnboardingNote, getOptionalOnboardingSteps, getPhpRestExtensionPointsSection, getTemplateSourceOfTruthNote, } from "./scaffold-onboarding.js";
|
|
11
12
|
import { getStarterManifestFiles, stringifyStarterManifest } from "./starter-manifests.js";
|
|
12
13
|
import { toKebabCase, toPascalCase, toSnakeCase, toTitleCase, } from "./string-case.js";
|
|
13
14
|
import { BUILTIN_BLOCK_METADATA_VERSION, COMPOUND_CHILD_BLOCK_METADATA_DEFAULTS, getBuiltInTemplateMetadataDefaults, getRemovedBuiltInTemplateMessage, isRemovedBuiltInTemplateId, } from "./template-defaults.js";
|
|
14
15
|
import { copyInterpolatedDirectory } from "./template-render.js";
|
|
15
|
-
import { TEMPLATE_IDS, getTemplateById, isBuiltInTemplateId } from "./template-registry.js";
|
|
16
|
+
import { PROJECT_TOOLS_PACKAGE_ROOT, TEMPLATE_IDS, getTemplateById, isBuiltInTemplateId, } from "./template-registry.js";
|
|
16
17
|
import { resolveTemplateSource } from "./template-source.js";
|
|
17
18
|
const BLOCK_SLUG_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
18
19
|
const PHP_PREFIX_PATTERN = /^[a-z_][a-z0-9_]*$/;
|
|
19
20
|
const PHP_PREFIX_MAX_LENGTH = 50;
|
|
20
21
|
const OFFICIAL_WORKSPACE_TEMPLATE_PACKAGE = "@wp-typia/create-workspace-template";
|
|
22
|
+
const EPHEMERAL_NODE_MODULES_LINK_TYPE = process.platform === "win32" ? "junction" : "dir";
|
|
21
23
|
const LOCKFILES = {
|
|
22
24
|
bun: ["bun.lock", "bun.lockb"],
|
|
23
25
|
npm: ["package-lock.json"],
|
|
@@ -265,6 +267,9 @@ export function getTemplateVariables(templateId, answers) {
|
|
|
265
267
|
bootstrapCredentialDeclarations: persistencePolicy === "public"
|
|
266
268
|
? "publicWriteExpiresAt?: number & tags.Type< 'uint32' >;\n\tpublicWriteToken?: string & tags.MinLength< 1 > & tags.MaxLength< 512 >;"
|
|
267
269
|
: "restNonce?: string & tags.MinLength< 1 > & tags.MaxLength< 128 >;",
|
|
270
|
+
persistencePolicyDescriptionJson: JSON.stringify(persistencePolicy === "authenticated"
|
|
271
|
+
? "Writes require a logged-in user and a valid REST nonce."
|
|
272
|
+
: "Anonymous writes use signed short-lived public tokens, per-request ids, and coarse rate limiting."),
|
|
268
273
|
keyword: slug.replace(/-/g, " "),
|
|
269
274
|
namespace,
|
|
270
275
|
needsMigration: "{{needsMigration}}",
|
|
@@ -412,6 +417,94 @@ async function writeStarterManifestFiles(targetDir, templateId, variables) {
|
|
|
412
417
|
await fsp.writeFile(destinationPath, stringifyStarterManifest(document), "utf8");
|
|
413
418
|
}
|
|
414
419
|
}
|
|
420
|
+
/**
|
|
421
|
+
* Seed REST-derived persistence artifacts into a newly scaffolded built-in
|
|
422
|
+
* project before the first manual `sync-rest` run.
|
|
423
|
+
*
|
|
424
|
+
* @param targetDir Absolute scaffold target directory.
|
|
425
|
+
* @param templateId Built-in template id being scaffolded.
|
|
426
|
+
* @param variables Resolved scaffold template variables for the project.
|
|
427
|
+
* @returns A promise that resolves after any required persistence artifacts are generated.
|
|
428
|
+
*/
|
|
429
|
+
async function seedBuiltInPersistenceArtifacts(targetDir, templateId, variables) {
|
|
430
|
+
const needsPersistenceArtifacts = templateId === "persistence" ||
|
|
431
|
+
(templateId === "compound" && variables.compoundPersistenceEnabled === "true");
|
|
432
|
+
if (!needsPersistenceArtifacts) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
await withEphemeralScaffoldNodeModules(targetDir, async () => {
|
|
436
|
+
if (templateId === "persistence") {
|
|
437
|
+
await syncPersistenceRestArtifacts({
|
|
438
|
+
apiTypesFile: path.join("src", "api-types.ts"),
|
|
439
|
+
outputDir: "src",
|
|
440
|
+
projectDir: targetDir,
|
|
441
|
+
variables,
|
|
442
|
+
});
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
await syncPersistenceRestArtifacts({
|
|
446
|
+
apiTypesFile: path.join("src", "blocks", variables.slugKebabCase, "api-types.ts"),
|
|
447
|
+
outputDir: path.join("src", "blocks", variables.slugKebabCase),
|
|
448
|
+
projectDir: targetDir,
|
|
449
|
+
variables,
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Locate a node_modules directory containing `typia` relative to the project
|
|
455
|
+
* tools package root.
|
|
456
|
+
*
|
|
457
|
+
* Search order:
|
|
458
|
+
* 1. `PROJECT_TOOLS_PACKAGE_ROOT/node_modules`
|
|
459
|
+
* 2. The monorepo root resolved from `PROJECT_TOOLS_PACKAGE_ROOT`
|
|
460
|
+
* 3. The monorepo root `node_modules`
|
|
461
|
+
*
|
|
462
|
+
* @returns The first matching path, or `null` when no candidate contains `typia`.
|
|
463
|
+
*/
|
|
464
|
+
function resolveScaffoldGeneratorNodeModulesPath() {
|
|
465
|
+
const candidates = [
|
|
466
|
+
path.join(PROJECT_TOOLS_PACKAGE_ROOT, "node_modules"),
|
|
467
|
+
path.resolve(PROJECT_TOOLS_PACKAGE_ROOT, "..", ".."),
|
|
468
|
+
path.resolve(PROJECT_TOOLS_PACKAGE_ROOT, "..", "..", "node_modules"),
|
|
469
|
+
];
|
|
470
|
+
for (const candidate of candidates) {
|
|
471
|
+
if (fs.existsSync(path.join(candidate, "typia", "package.json"))) {
|
|
472
|
+
return candidate;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Temporarily symlink a scaffold generator node_modules directory into the
|
|
479
|
+
* target project while running an async callback.
|
|
480
|
+
*
|
|
481
|
+
* The helper resolves the source path via `resolveScaffoldGeneratorNodeModulesPath()`
|
|
482
|
+
* and uses `EPHEMERAL_NODE_MODULES_LINK_TYPE` for the symlink. The temporary
|
|
483
|
+
* link is removed in the `finally` block so cleanup still happens if the
|
|
484
|
+
* callback throws.
|
|
485
|
+
*
|
|
486
|
+
* @param targetDir Absolute scaffold target directory.
|
|
487
|
+
* @param callback Async work that requires a resolvable `node_modules`.
|
|
488
|
+
* @returns A promise that resolves after the callback and cleanup complete.
|
|
489
|
+
*/
|
|
490
|
+
async function withEphemeralScaffoldNodeModules(targetDir, callback) {
|
|
491
|
+
const targetNodeModulesPath = path.join(targetDir, "node_modules");
|
|
492
|
+
if (fs.existsSync(targetNodeModulesPath)) {
|
|
493
|
+
await callback();
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const sourceNodeModulesPath = resolveScaffoldGeneratorNodeModulesPath();
|
|
497
|
+
if (!sourceNodeModulesPath) {
|
|
498
|
+
throw new Error("Unable to resolve a node_modules directory with typia for scaffold-time REST artifact generation.");
|
|
499
|
+
}
|
|
500
|
+
await fsp.symlink(sourceNodeModulesPath, targetNodeModulesPath, EPHEMERAL_NODE_MODULES_LINK_TYPE);
|
|
501
|
+
try {
|
|
502
|
+
await callback();
|
|
503
|
+
}
|
|
504
|
+
finally {
|
|
505
|
+
await fsp.rm(targetNodeModulesPath, { force: true, recursive: true });
|
|
506
|
+
}
|
|
507
|
+
}
|
|
415
508
|
async function normalizePackageManagerFiles(targetDir, packageManagerId) {
|
|
416
509
|
const yarnRcPath = path.join(targetDir, ".yarnrc.yml");
|
|
417
510
|
if (packageManagerId === "yarn") {
|
|
@@ -557,6 +650,7 @@ export async function scaffoldProject({ projectDir, templateId, answers, dataSto
|
|
|
557
650
|
const isOfficialWorkspace = isOfficialWorkspaceProject(projectDir);
|
|
558
651
|
if (isBuiltInTemplate) {
|
|
559
652
|
await writeStarterManifestFiles(projectDir, templateId, variables);
|
|
653
|
+
await seedBuiltInPersistenceArtifacts(projectDir, templateId, variables);
|
|
560
654
|
await applyLocalDevPresetFiles({
|
|
561
655
|
projectDir,
|
|
562
656
|
variables,
|
|
@@ -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
|
}
|