@wp-typia/project-tools 0.15.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.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 +2 -1
- 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
package/dist/runtime/cli-add.js
CHANGED
|
@@ -2,8 +2,9 @@ import fs from "node:fs";
|
|
|
2
2
|
import { promises as fsp } from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { syncBlockMetadata, } from "@wp-typia/block-runtime/metadata-core";
|
|
6
6
|
import { ensureMigrationDirectories, parseMigrationConfig, writeInitialMigrationScaffold, writeMigrationConfig, } from "./migration-project.js";
|
|
7
|
+
import { syncPersistenceRestArtifacts, } from "./persistence-rest-artifacts.js";
|
|
7
8
|
import { snapshotProjectVersion } from "./migrations.js";
|
|
8
9
|
import { getDefaultAnswers, scaffoldProject } from "./scaffold.js";
|
|
9
10
|
import { SHARED_WORKSPACE_TEMPLATE_ROOT, } from "./template-registry.js";
|
|
@@ -84,15 +85,21 @@ function buildPersistenceBlockConfigEntry(variables) {
|
|
|
84
85
|
`\t\tattributeTypeName: ${quoteTsString(`${variables.pascalCase}Attributes`)},`,
|
|
85
86
|
"\t\trestManifest: defineEndpointManifest( {",
|
|
86
87
|
"\t\t\tcontracts: {",
|
|
88
|
+
"\t\t\t\t'bootstrap-query': {",
|
|
89
|
+
`\t\t\t\t\tsourceTypeName: ${quoteTsString(`${variables.pascalCase}BootstrapQuery`)},`,
|
|
90
|
+
"\t\t\t\t},",
|
|
91
|
+
"\t\t\t\t'bootstrap-response': {",
|
|
92
|
+
`\t\t\t\t\tsourceTypeName: ${quoteTsString(`${variables.pascalCase}BootstrapResponse`)},`,
|
|
93
|
+
"\t\t\t\t},",
|
|
87
94
|
"\t\t\t\t'state-query': {",
|
|
88
95
|
`\t\t\t\t\tsourceTypeName: ${quoteTsString(`${variables.pascalCase}StateQuery`)},`,
|
|
89
96
|
"\t\t\t\t},",
|
|
90
|
-
"\t\t\t\t'write-state-request': {",
|
|
91
|
-
`\t\t\t\t\tsourceTypeName: ${quoteTsString(`${variables.pascalCase}WriteStateRequest`)},`,
|
|
92
|
-
"\t\t\t\t},",
|
|
93
97
|
"\t\t\t\t'state-response': {",
|
|
94
98
|
`\t\t\t\t\tsourceTypeName: ${quoteTsString(`${variables.pascalCase}StateResponse`)},`,
|
|
95
99
|
"\t\t\t\t},",
|
|
100
|
+
"\t\t\t\t'write-state-request': {",
|
|
101
|
+
`\t\t\t\t\tsourceTypeName: ${quoteTsString(`${variables.pascalCase}WriteStateRequest`)},`,
|
|
102
|
+
"\t\t\t\t},",
|
|
96
103
|
"\t\t\t},",
|
|
97
104
|
"\t\t\tendpoints: [",
|
|
98
105
|
"\t\t\t\t{",
|
|
@@ -118,6 +125,16 @@ function buildPersistenceBlockConfigEntry(variables) {
|
|
|
118
125
|
`\t\t\t\t\t\tmechanism: ${quoteTsString(variables.restWriteAuthMechanism)},`,
|
|
119
126
|
"\t\t\t\t\t},",
|
|
120
127
|
"\t\t\t\t},",
|
|
128
|
+
"\t\t\t\t{",
|
|
129
|
+
"\t\t\t\t\tauth: 'public',",
|
|
130
|
+
"\t\t\t\t\tmethod: 'GET',",
|
|
131
|
+
`\t\t\t\t\toperationId: ${quoteTsString(`get${variables.pascalCase}Bootstrap`)},`,
|
|
132
|
+
`\t\t\t\t\tpath: ${quoteTsString(`/${variables.namespace}/v1/${variables.slugKebabCase}/bootstrap`)},`,
|
|
133
|
+
"\t\t\t\t\tqueryContract: 'bootstrap-query',",
|
|
134
|
+
"\t\t\t\t\tresponseContract: 'bootstrap-response',",
|
|
135
|
+
`\t\t\t\t\tsummary: 'Read fresh session bootstrap state for the current viewer.',`,
|
|
136
|
+
`\t\t\t\t\ttags: [ ${quoteTsString(variables.title)} ],`,
|
|
137
|
+
"\t\t\t\t},",
|
|
121
138
|
"\t\t\t],",
|
|
122
139
|
"\t\t\tinfo: {",
|
|
123
140
|
`\t\t\t\ttitle: ${quoteTsString(`${variables.title} REST API`)},`,
|
|
@@ -130,50 +147,6 @@ function buildPersistenceBlockConfigEntry(variables) {
|
|
|
130
147
|
"\t},",
|
|
131
148
|
].join("\n");
|
|
132
149
|
}
|
|
133
|
-
function buildPersistenceEndpointManifest(variables) {
|
|
134
|
-
return defineEndpointManifest({
|
|
135
|
-
contracts: {
|
|
136
|
-
"state-query": {
|
|
137
|
-
sourceTypeName: `${variables.pascalCase}StateQuery`,
|
|
138
|
-
},
|
|
139
|
-
"write-state-request": {
|
|
140
|
-
sourceTypeName: `${variables.pascalCase}WriteStateRequest`,
|
|
141
|
-
},
|
|
142
|
-
"state-response": {
|
|
143
|
-
sourceTypeName: `${variables.pascalCase}StateResponse`,
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
endpoints: [
|
|
147
|
-
{
|
|
148
|
-
auth: "public",
|
|
149
|
-
method: "GET",
|
|
150
|
-
operationId: `get${variables.pascalCase}State`,
|
|
151
|
-
path: `/${variables.namespace}/v1/${variables.slugKebabCase}/state`,
|
|
152
|
-
queryContract: "state-query",
|
|
153
|
-
responseContract: "state-response",
|
|
154
|
-
summary: "Read the current persisted state.",
|
|
155
|
-
tags: [variables.title],
|
|
156
|
-
},
|
|
157
|
-
{
|
|
158
|
-
auth: variables.restWriteAuthIntent,
|
|
159
|
-
bodyContract: "write-state-request",
|
|
160
|
-
method: "POST",
|
|
161
|
-
operationId: `write${variables.pascalCase}State`,
|
|
162
|
-
path: `/${variables.namespace}/v1/${variables.slugKebabCase}/state`,
|
|
163
|
-
responseContract: "state-response",
|
|
164
|
-
summary: "Write the current persisted state.",
|
|
165
|
-
tags: [variables.title],
|
|
166
|
-
wordpressAuth: {
|
|
167
|
-
mechanism: variables.restWriteAuthMechanism,
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
],
|
|
171
|
-
info: {
|
|
172
|
-
title: `${variables.title} REST API`,
|
|
173
|
-
version: "1.0.0",
|
|
174
|
-
},
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
150
|
function buildCompoundChildConfigEntry(variables) {
|
|
178
151
|
return [
|
|
179
152
|
"\t{",
|
|
@@ -732,28 +705,11 @@ async function syncWorkspaceBlockMetadata(projectDir, slug, sourceTypeName, type
|
|
|
732
705
|
});
|
|
733
706
|
}
|
|
734
707
|
async function syncWorkspacePersistenceArtifacts(projectDir, variables) {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
openApiFile: path.join("src", "blocks", variables.slugKebabCase, "api-schemas", `${baseName}.openapi.json`),
|
|
741
|
-
projectRoot: projectDir,
|
|
742
|
-
sourceTypeName: contract.sourceTypeName,
|
|
743
|
-
typesFile: apiTypesFile,
|
|
744
|
-
});
|
|
745
|
-
}
|
|
746
|
-
await syncRestOpenApi({
|
|
747
|
-
manifest,
|
|
748
|
-
openApiFile: path.join("src", "blocks", variables.slugKebabCase, "api.openapi.json"),
|
|
749
|
-
projectRoot: projectDir,
|
|
750
|
-
typesFile: apiTypesFile,
|
|
751
|
-
});
|
|
752
|
-
await syncEndpointClient({
|
|
753
|
-
clientFile: path.join("src", "blocks", variables.slugKebabCase, "api-client.ts"),
|
|
754
|
-
manifest,
|
|
755
|
-
projectRoot: projectDir,
|
|
756
|
-
typesFile: apiTypesFile,
|
|
708
|
+
await syncPersistenceRestArtifacts({
|
|
709
|
+
apiTypesFile: path.join("src", "blocks", variables.slugKebabCase, "api-types.ts"),
|
|
710
|
+
outputDir: path.join("src", "blocks", variables.slugKebabCase),
|
|
711
|
+
projectDir,
|
|
712
|
+
variables,
|
|
757
713
|
});
|
|
758
714
|
}
|
|
759
715
|
async function syncWorkspaceAddedBlockArtifacts(projectDir, templateId, variables) {
|
|
@@ -5,7 +5,7 @@ import { execFileSync } from "node:child_process";
|
|
|
5
5
|
import { access, constants as fsConstants, rm, writeFile } from "node:fs/promises";
|
|
6
6
|
import { getBuiltInTemplateLayerDirs } from "./template-builtins.js";
|
|
7
7
|
import { HOOKED_BLOCK_ANCHOR_PATTERN, HOOKED_BLOCK_POSITION_SET, } from "./hooked-blocks.js";
|
|
8
|
-
import { listTemplates } from "./template-registry.js";
|
|
8
|
+
import { isBuiltInTemplateId, listTemplates } from "./template-registry.js";
|
|
9
9
|
import { readWorkspaceInventory } from "./workspace-inventory.js";
|
|
10
10
|
import { getInvalidWorkspaceProjectReason, parseWorkspacePackageJson, WORKSPACE_TEMPLATE_PACKAGE, tryResolveWorkspaceProject, } from "./workspace-project.js";
|
|
11
11
|
const WORKSPACE_COLLECTION_IMPORT_LINE = "import '../../collection';";
|
|
@@ -285,24 +285,40 @@ export async function getDoctorChecks(cwd) {
|
|
|
285
285
|
detail: tempWritable ? "Writable" : "Not writable",
|
|
286
286
|
});
|
|
287
287
|
for (const template of listTemplates()) {
|
|
288
|
-
|
|
288
|
+
if (!isBuiltInTemplateId(template.id)) {
|
|
289
|
+
const templateDirExists = fs.existsSync(template.templateDir);
|
|
290
|
+
const hasAssets = templateDirExists &&
|
|
291
|
+
fs.existsSync(path.join(template.templateDir, "package.json.mustache"));
|
|
292
|
+
checks.push({
|
|
293
|
+
status: !templateDirExists || hasAssets ? "pass" : "fail",
|
|
294
|
+
label: `Template ${template.id}`,
|
|
295
|
+
detail: !templateDirExists
|
|
296
|
+
? "External template metadata only; local overlay package is not installed."
|
|
297
|
+
: hasAssets
|
|
298
|
+
? template.templateDir
|
|
299
|
+
: "Missing core template assets",
|
|
300
|
+
});
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const builtInTemplateId = template.id;
|
|
304
|
+
const layerDirs = builtInTemplateId === "persistence"
|
|
289
305
|
? Array.from(new Set([
|
|
290
|
-
...getBuiltInTemplateLayerDirs(
|
|
291
|
-
...getBuiltInTemplateLayerDirs(
|
|
306
|
+
...getBuiltInTemplateLayerDirs(builtInTemplateId, { persistencePolicy: "authenticated" }),
|
|
307
|
+
...getBuiltInTemplateLayerDirs(builtInTemplateId, { persistencePolicy: "public" }),
|
|
292
308
|
]))
|
|
293
|
-
:
|
|
309
|
+
: builtInTemplateId === "compound"
|
|
294
310
|
? Array.from(new Set([
|
|
295
|
-
...getBuiltInTemplateLayerDirs(
|
|
296
|
-
...getBuiltInTemplateLayerDirs(
|
|
311
|
+
...getBuiltInTemplateLayerDirs(builtInTemplateId),
|
|
312
|
+
...getBuiltInTemplateLayerDirs(builtInTemplateId, {
|
|
297
313
|
persistenceEnabled: true,
|
|
298
314
|
persistencePolicy: "authenticated",
|
|
299
315
|
}),
|
|
300
|
-
...getBuiltInTemplateLayerDirs(
|
|
316
|
+
...getBuiltInTemplateLayerDirs(builtInTemplateId, {
|
|
301
317
|
persistenceEnabled: true,
|
|
302
318
|
persistencePolicy: "public",
|
|
303
319
|
}),
|
|
304
320
|
]))
|
|
305
|
-
: getBuiltInTemplateLayerDirs(
|
|
321
|
+
: getBuiltInTemplateLayerDirs(builtInTemplateId);
|
|
306
322
|
const hasAssets = layerDirs.every((layerDir) => fs.existsSync(layerDir)) &&
|
|
307
323
|
layerDirs.some((layerDir) => fs.existsSync(path.join(layerDir, "package.json.mustache"))) &&
|
|
308
324
|
layerDirs.some((layerDir) => fs.existsSync(path.join(layerDir, "src")));
|
package/dist/runtime/cli-help.js
CHANGED
|
@@ -34,6 +34,7 @@ Package managers: ${PACKAGE_MANAGER_IDS.join(", ")}
|
|
|
34
34
|
Notes:
|
|
35
35
|
\`wp-typia create\` is the canonical scaffold command.
|
|
36
36
|
\`wp-typia <project-dir>\` remains a backward-compatible alias to \`create\`.
|
|
37
|
+
Use \`--template @wp-typia/create-workspace-template\` for the official empty workspace scaffold behind \`wp-typia add ...\`.
|
|
37
38
|
\`add variation\` uses an existing workspace block from \`scripts/block-config.ts\`.
|
|
38
39
|
\`add pattern\` scaffolds a namespaced PHP pattern shell under \`src/patterns/\`.
|
|
39
40
|
\`add binding-source\` scaffolds shared PHP and editor registration under \`src/bindings/\`.
|
|
@@ -30,6 +30,16 @@ export function formatTemplateFeatures(template) {
|
|
|
30
30
|
* @returns Multi-line template details text for CLI output.
|
|
31
31
|
*/
|
|
32
32
|
export function formatTemplateDetails(template) {
|
|
33
|
+
if (!isBuiltInTemplateId(template.id)) {
|
|
34
|
+
return [
|
|
35
|
+
template.id,
|
|
36
|
+
template.description,
|
|
37
|
+
`Category: ${template.defaultCategory}`,
|
|
38
|
+
`Overlay path: ${template.templateDir}`,
|
|
39
|
+
"Layers: workspace package scaffold",
|
|
40
|
+
`Features: ${template.features.join(", ")}`,
|
|
41
|
+
].join("\n");
|
|
42
|
+
}
|
|
33
43
|
const layers = template.id === "persistence"
|
|
34
44
|
? [
|
|
35
45
|
`authenticated: ${getBuiltInTemplateLayerDirs(template.id, { persistencePolicy: "authenticated" }).join(" -> ")}`,
|
|
@@ -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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wp-typia/project-tools",
|
|
3
|
-
"version": "0.15.
|
|
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 =
|
|
@@ -5,11 +5,17 @@ import {
|
|
|
5
5
|
type ValidationResult,
|
|
6
6
|
} from '@wp-typia/api-client';
|
|
7
7
|
import type {
|
|
8
|
+
{{pascalCase}}BootstrapQuery,
|
|
9
|
+
{{pascalCase}}BootstrapResponse,
|
|
8
10
|
{{pascalCase}}StateQuery,
|
|
9
11
|
{{pascalCase}}StateResponse,
|
|
10
12
|
{{pascalCase}}WriteStateRequest,
|
|
11
13
|
} from './api-types';
|
|
12
14
|
|
|
15
|
+
const validateBootstrapQuery =
|
|
16
|
+
typia.createValidate< {{pascalCase}}BootstrapQuery >();
|
|
17
|
+
const validateBootstrapResponse =
|
|
18
|
+
typia.createValidate< {{pascalCase}}BootstrapResponse >();
|
|
13
19
|
const validateStateQuery = typia.createValidate< {{pascalCase}}StateQuery >();
|
|
14
20
|
const validateWriteStateRequest =
|
|
15
21
|
typia.createValidate< {{pascalCase}}WriteStateRequest >();
|
|
@@ -17,6 +23,14 @@ const validateStateResponse =
|
|
|
17
23
|
typia.createValidate< {{pascalCase}}StateResponse >();
|
|
18
24
|
|
|
19
25
|
export const apiValidators = {
|
|
26
|
+
bootstrapQuery: (
|
|
27
|
+
input: unknown
|
|
28
|
+
): ValidationResult< {{pascalCase}}BootstrapQuery > =>
|
|
29
|
+
toValidationResult( validateBootstrapQuery( input ) ),
|
|
30
|
+
bootstrapResponse: (
|
|
31
|
+
input: unknown
|
|
32
|
+
): ValidationResult< {{pascalCase}}BootstrapResponse > =>
|
|
33
|
+
toValidationResult( validateBootstrapResponse( input ) ),
|
|
20
34
|
stateQuery: (
|
|
21
35
|
input: unknown
|
|
22
36
|
): ValidationResult< {{pascalCase}}StateQuery > =>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
callEndpoint,
|
|
3
|
+
type ApiEndpoint as RestApiEndpoint,
|
|
3
4
|
} from '@wp-typia/rest';
|
|
4
5
|
|
|
5
6
|
import {
|
|
@@ -17,17 +18,35 @@ import {
|
|
|
17
18
|
type PersistenceTransportOptions,
|
|
18
19
|
} from './transport';
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
function createRestEndpoint< Req, Res >(
|
|
22
|
+
endpoint: {
|
|
23
|
+
method: RestApiEndpoint< Req, Res >[ 'method' ];
|
|
24
|
+
path: string;
|
|
25
|
+
validateRequest: RestApiEndpoint< Req, Res >[ 'validateRequest' ];
|
|
26
|
+
validateResponse: RestApiEndpoint< Req, Res >[ 'validateResponse' ];
|
|
27
|
+
}
|
|
28
|
+
): RestApiEndpoint< Req, Res > {
|
|
29
|
+
// Strip generator-only helper fields so the runtime client only sees the
|
|
30
|
+
// canonical RestApiEndpoint surface it expects.
|
|
31
|
+
return {
|
|
32
|
+
method: endpoint.method,
|
|
33
|
+
path: endpoint.path,
|
|
34
|
+
validateRequest: endpoint.validateRequest,
|
|
35
|
+
validateResponse: endpoint.validateResponse,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const bootstrapEndpoint = createRestEndpoint(
|
|
40
|
+
get{{pascalCase}}BootstrapEndpoint
|
|
41
|
+
);
|
|
23
42
|
|
|
24
|
-
export const stateEndpoint =
|
|
25
|
-
|
|
26
|
-
|
|
43
|
+
export const stateEndpoint = createRestEndpoint(
|
|
44
|
+
get{{pascalCase}}StateEndpoint
|
|
45
|
+
);
|
|
27
46
|
|
|
28
|
-
export const writeStateEndpoint =
|
|
29
|
-
|
|
30
|
-
|
|
47
|
+
export const writeStateEndpoint = createRestEndpoint(
|
|
48
|
+
write{{pascalCase}}StateEndpoint
|
|
49
|
+
);
|
|
31
50
|
|
|
32
51
|
export function fetchState(
|
|
33
52
|
request: {{pascalCase}}StateQuery,
|
|
@@ -7,6 +7,9 @@ import type {
|
|
|
7
7
|
{{pascalCase}}Context,
|
|
8
8
|
{{pascalCase}}State,
|
|
9
9
|
} from './types';
|
|
10
|
+
import type {
|
|
11
|
+
{{pascalCase}}WriteStateRequest,
|
|
12
|
+
} from './api-types';
|
|
10
13
|
|
|
11
14
|
function hasExpiredPublicWriteToken(
|
|
12
15
|
expiresAt?: number
|
|
@@ -123,6 +126,7 @@ const { actions, state } = store( '{{slugKebabCase}}', {
|
|
|
123
126
|
let bootstrapSucceeded = false;
|
|
124
127
|
let lastBootstrapError =
|
|
125
128
|
'Unable to initialize write access';
|
|
129
|
+
const includeRestNonce = {{isAuthenticatedPersistencePolicy}};
|
|
126
130
|
|
|
127
131
|
for ( let attempt = 1; attempt <= BOOTSTRAP_MAX_ATTEMPTS; attempt += 1 ) {
|
|
128
132
|
try {
|
|
@@ -156,6 +160,8 @@ const { actions, state } = store( '{{slugKebabCase}}', {
|
|
|
156
160
|
? result.data.publicWriteToken
|
|
157
161
|
: '';
|
|
158
162
|
clientState.writeNonce =
|
|
163
|
+
includeRestNonce &&
|
|
164
|
+
'restNonce' in result.data &&
|
|
159
165
|
typeof result.data.restNonce === 'string' &&
|
|
160
166
|
result.data.restNonce.length > 0
|
|
161
167
|
? result.data.restNonce
|
|
@@ -231,20 +237,20 @@ const { actions, state } = store( '{{slugKebabCase}}', {
|
|
|
231
237
|
context.error = '';
|
|
232
238
|
|
|
233
239
|
try {
|
|
234
|
-
const
|
|
240
|
+
const request = {
|
|
235
241
|
delta: 1,
|
|
236
242
|
postId: context.postId,
|
|
237
|
-
publicWriteRequestId:
|
|
238
|
-
context.persistencePolicy === 'public'
|
|
239
|
-
? generatePublicWriteRequestId()
|
|
240
|
-
: undefined,
|
|
241
|
-
publicWriteToken:
|
|
242
|
-
context.persistencePolicy === 'public' &&
|
|
243
|
-
clientState.writeToken.length > 0
|
|
244
|
-
? clientState.writeToken
|
|
245
|
-
: undefined,
|
|
246
243
|
resourceKey: context.resourceKey,
|
|
247
|
-
}
|
|
244
|
+
} as {{pascalCase}}WriteStateRequest;
|
|
245
|
+
if ( {{isPublicPersistencePolicy}} ) {
|
|
246
|
+
request.publicWriteRequestId =
|
|
247
|
+
generatePublicWriteRequestId() as {{pascalCase}}WriteStateRequest[ 'publicWriteRequestId' ];
|
|
248
|
+
if ( clientState.writeToken.length > 0 ) {
|
|
249
|
+
request.publicWriteToken =
|
|
250
|
+
clientState.writeToken as {{pascalCase}}WriteStateRequest[ 'publicWriteToken' ];
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const result = await writeState( request, {
|
|
248
254
|
restNonce:
|
|
249
255
|
clientState.writeNonce.length > 0
|
|
250
256
|
? clientState.writeNonce
|
|
@@ -66,15 +66,15 @@ export default function Edit( {
|
|
|
66
66
|
validateEditorUpdate
|
|
67
67
|
);
|
|
68
68
|
const alignmentValue = editorFields.getStringValue(
|
|
69
|
-
attributes,
|
|
69
|
+
attributes as unknown as Record< string, unknown >,
|
|
70
70
|
'alignment',
|
|
71
71
|
'left'
|
|
72
72
|
);
|
|
73
73
|
const persistencePolicy = '{{persistencePolicy}}';
|
|
74
|
-
const persistencePolicyDescription =
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
const persistencePolicyDescription = __(
|
|
75
|
+
{{persistencePolicyDescriptionJson}},
|
|
76
|
+
'{{textDomain}}'
|
|
77
|
+
);
|
|
78
78
|
|
|
79
79
|
return (
|
|
80
80
|
<>
|
|
@@ -91,7 +91,7 @@ export default function Edit( {
|
|
|
91
91
|
</BlockControls>
|
|
92
92
|
<InspectorControls>
|
|
93
93
|
<InspectorFromManifest
|
|
94
|
-
attributes={ attributes }
|
|
94
|
+
attributes={ attributes as unknown as Record< string, unknown > }
|
|
95
95
|
fieldLookup={ editorFields }
|
|
96
96
|
onChange={ updateField }
|
|
97
97
|
paths={ [ 'alignment', 'isVisible', 'showCount', 'buttonLabel' ] }
|