@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
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
|
+
}
|
|
@@ -42,7 +42,7 @@ export function getTemplateSourceOfTruthNote(templateId, { compoundPersistenceEn
|
|
|
42
42
|
return compoundBase;
|
|
43
43
|
}
|
|
44
44
|
if (templateId === "persistence") {
|
|
45
|
-
return "`src/types.ts` remains the source of truth for `block.json`, `typia.manifest.json`, and `typia-validator.php`. Fresh scaffolds include a starter `typia.manifest.json` so editor imports resolve before the first sync. `src/api-types.ts` remains the source of truth for `src/api-schemas/*` when you run `sync-rest`, while `src/transport.ts` is the first-class transport seam for editor and frontend requests. This scaffold is intentionally server-rendered: `src/render.php` is the canonical frontend entry,
|
|
45
|
+
return "`src/types.ts` remains the source of truth for `block.json`, `typia.manifest.json`, and `typia-validator.php`. Fresh scaffolds include a starter `typia.manifest.json` so editor imports resolve before the first sync. `src/api-types.ts` remains the source of truth for `src/api-schemas/*` when you run `sync-rest`, while `src/transport.ts` is the first-class transport seam for editor and frontend requests. This scaffold is intentionally server-rendered: `src/render.php` is the canonical frontend entry, `src/save.tsx` returns `null`, and session-only write data now refreshes through the dedicated `/bootstrap` endpoint after hydration instead of being frozen into markup.";
|
|
46
46
|
}
|
|
47
47
|
return "`src/types.ts` remains the source of truth for `block.json`, `typia.manifest.json`, and `typia-validator.php`. Fresh scaffolds include a starter `typia.manifest.json` so editor imports resolve before the first sync. The basic scaffold stays static by design: `src/render.php` is only an opt-in server placeholder, `src/save.tsx` remains the canonical frontend output, and the generated webpack config keeps the current `@wordpress/scripts` CommonJS baseline unless you intentionally add `render` to `block.json`.";
|
|
48
48
|
}
|
|
@@ -87,12 +87,13 @@ export function getPhpRestExtensionPointsSection(templateId, { compoundPersisten
|
|
|
87
87
|
mainPhpPath: `${slug}.php`,
|
|
88
88
|
mainPhpScope: "change storage helpers, route handlers, response shaping, or route registration",
|
|
89
89
|
transportPath: "src/transport.ts",
|
|
90
|
+
extraNote: "Keep durable state on the `/state` endpoints and treat the dedicated `/bootstrap` endpoint as the place to return fresh session-only write access data such as nonces or public write tokens.",
|
|
90
91
|
});
|
|
91
92
|
}
|
|
92
93
|
if (templateId === "compound" && compoundPersistenceEnabled) {
|
|
93
94
|
return formatPhpRestExtensionPointsSection({
|
|
94
95
|
apiTypesPath: `src/blocks/${slug}/api-types.ts`,
|
|
95
|
-
extraNote: "The hidden child block does not own REST routes or storage.",
|
|
96
|
+
extraNote: "The hidden child block does not own REST routes or storage. Keep durable parent-block state on the `/state` endpoints and return fresh session-only write access data from the dedicated `/bootstrap` endpoint.",
|
|
96
97
|
mainPhpPath: `${slug}.php`,
|
|
97
98
|
mainPhpScope: "change parent-block storage helpers, route handlers, response shaping, or route registration",
|
|
98
99
|
transportPath: `src/blocks/${slug}/transport.ts`,
|
|
@@ -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
|
*
|
|
@@ -56,6 +55,8 @@ export interface ScaffoldTemplateVariables extends Record<string, string> {
|
|
|
56
55
|
phpPrefixUpper: string;
|
|
57
56
|
isAuthenticatedPersistencePolicy: "false" | "true";
|
|
58
57
|
isPublicPersistencePolicy: "false" | "true";
|
|
58
|
+
bootstrapCredentialDeclarations: string;
|
|
59
|
+
persistencePolicyDescriptionJson: string;
|
|
59
60
|
publicWriteRequestIdDeclaration: string;
|
|
60
61
|
restPackageVersion: string;
|
|
61
62
|
restWriteAuthIntent: "authenticated" | "public-write-protected";
|
|
@@ -72,9 +73,17 @@ export interface ScaffoldTemplateVariables extends Record<string, string> {
|
|
|
72
73
|
titleCase: string;
|
|
73
74
|
persistencePolicy: PersistencePolicy;
|
|
74
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
|
+
*/
|
|
75
84
|
interface ResolveTemplateOptions {
|
|
76
85
|
isInteractive?: boolean;
|
|
77
|
-
selectTemplate?: () => Promise<
|
|
86
|
+
selectTemplate?: () => Promise<string>;
|
|
78
87
|
templateId?: string;
|
|
79
88
|
yes?: boolean;
|
|
80
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"],
|
|
@@ -262,6 +264,12 @@ export function getTemplateVariables(templateId, answers) {
|
|
|
262
264
|
frontendCssClassName: buildFrontendCssClassName(cssClassName),
|
|
263
265
|
isAuthenticatedPersistencePolicy: persistencePolicy === "authenticated" ? "true" : "false",
|
|
264
266
|
isPublicPersistencePolicy: persistencePolicy === "public" ? "true" : "false",
|
|
267
|
+
bootstrapCredentialDeclarations: persistencePolicy === "public"
|
|
268
|
+
? "publicWriteExpiresAt?: number & tags.Type< 'uint32' >;\n\tpublicWriteToken?: string & tags.MinLength< 1 > & tags.MaxLength< 512 >;"
|
|
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."),
|
|
265
273
|
keyword: slug.replace(/-/g, " "),
|
|
266
274
|
namespace,
|
|
267
275
|
needsMigration: "{{needsMigration}}",
|
|
@@ -409,6 +417,94 @@ async function writeStarterManifestFiles(targetDir, templateId, variables) {
|
|
|
409
417
|
await fsp.writeFile(destinationPath, stringifyStarterManifest(document), "utf8");
|
|
410
418
|
}
|
|
411
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
|
+
}
|
|
412
508
|
async function normalizePackageManagerFiles(targetDir, packageManagerId) {
|
|
413
509
|
const yarnRcPath = path.join(targetDir, ".yarnrc.yml");
|
|
414
510
|
if (packageManagerId === "yarn") {
|
|
@@ -554,6 +650,7 @@ export async function scaffoldProject({ projectDir, templateId, answers, dataSto
|
|
|
554
650
|
const isOfficialWorkspace = isOfficialWorkspaceProject(projectDir);
|
|
555
651
|
if (isBuiltInTemplate) {
|
|
556
652
|
await writeStarterManifestFiles(projectDir, templateId, variables);
|
|
653
|
+
await seedBuiltInPersistenceArtifacts(projectDir, templateId, variables);
|
|
557
654
|
await applyLocalDevPresetFiles({
|
|
558
655
|
projectDir,
|
|
559
656
|
variables,
|
|
@@ -59,6 +59,7 @@ export interface OpenApiResponse extends JsonSchemaObject {
|
|
|
59
59
|
"application/json": OpenApiMediaType;
|
|
60
60
|
};
|
|
61
61
|
description: string;
|
|
62
|
+
headers?: Record<string, JsonSchemaObject>;
|
|
62
63
|
}
|
|
63
64
|
/**
|
|
64
65
|
* Header-based security scheme used by authenticated WordPress REST routes.
|