@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.
@@ -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 { defineEndpointManifest, syncBlockMetadata, syncEndpointClient, syncRestOpenApi, syncTypeSchemas, } from "@wp-typia/block-runtime/metadata-core";
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
- const manifest = buildPersistenceEndpointManifest(variables);
736
- const apiTypesFile = path.join("src", "blocks", variables.slugKebabCase, "api-types.ts");
737
- for (const [baseName, contract] of Object.entries(manifest.contracts)) {
738
- await syncTypeSchemas({
739
- jsonSchemaFile: path.join("src", "blocks", variables.slugKebabCase, "api-schemas", `${baseName}.schema.json`),
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
- const layerDirs = template.id === "persistence"
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(template.id, { persistencePolicy: "authenticated" }),
291
- ...getBuiltInTemplateLayerDirs(template.id, { persistencePolicy: "public" }),
306
+ ...getBuiltInTemplateLayerDirs(builtInTemplateId, { persistencePolicy: "authenticated" }),
307
+ ...getBuiltInTemplateLayerDirs(builtInTemplateId, { persistencePolicy: "public" }),
292
308
  ]))
293
- : template.id === "compound"
309
+ : builtInTemplateId === "compound"
294
310
  ? Array.from(new Set([
295
- ...getBuiltInTemplateLayerDirs(template.id),
296
- ...getBuiltInTemplateLayerDirs(template.id, {
311
+ ...getBuiltInTemplateLayerDirs(builtInTemplateId),
312
+ ...getBuiltInTemplateLayerDirs(builtInTemplateId, {
297
313
  persistenceEnabled: true,
298
314
  persistencePolicy: "authenticated",
299
315
  }),
300
- ...getBuiltInTemplateLayerDirs(template.id, {
316
+ ...getBuiltInTemplateLayerDirs(builtInTemplateId, {
301
317
  persistenceEnabled: true,
302
318
  persistencePolicy: "public",
303
319
  }),
304
320
  ]))
305
- : getBuiltInTemplateLayerDirs(template.id);
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")));
@@ -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<BuiltInTemplateId>;
86
+ selectTemplate?: () => Promise<string>;
79
87
  templateId?: string;
80
88
  yes?: boolean;
81
89
  }
@@ -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: template.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: BuiltInTemplateId;
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 = TEMPLATE_REGISTRY.map((template) => template.id);
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: ${TEMPLATE_IDS.join(", ")}`);
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.0",
3
+ "version": "0.15.1",
4
4
  "description": "Project orchestration and programmatic tooling for wp-typia",
5
5
  "packageManager": "bun@1.3.11",
6
6
  "type": "module",
@@ -69,6 +69,7 @@
69
69
  "npm-package-arg": "^13.0.0",
70
70
  "semver": "^7.7.3",
71
71
  "tar": "^7.4.3",
72
+ "typia": "^12.0.1",
72
73
  "typescript": "^5.9.2"
73
74
  },
74
75
  "devDependencies": {
@@ -2,14 +2,7 @@
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
 
5
- const PARENT_BLOCK_NAME = '{{namespace}}/{{slugKebabCase}}';
6
- const PARENT_BLOCK_NAMESPACE = '{{namespace}}';
7
- const PARENT_BLOCK_SLUG = '{{slugKebabCase}}';
8
- const PARENT_BLOCK_TITLE = {{titleJson}};
9
- const PARENT_TYPE_NAME = '{{pascalCase}}';
10
- const PARENT_STYLE_IMPORT = '../{{slugKebabCase}}/style.scss';
11
5
  const PROJECT_ROOT = process.cwd();
12
- const TEXT_DOMAIN = '{{textDomain}}';
13
6
 
14
7
  const ALLOWED_CHILD_MARKER = '// add-child: insert new allowed child block names here';
15
8
  const BLOCK_CONFIG_MARKER = '// add-child: insert new block config entries here';
@@ -22,6 +15,16 @@ type StarterManifestDocument = {
22
15
  sourceType: string;
23
16
  };
24
17
 
18
+ type CompoundParentConfig = {
19
+ blockName: string;
20
+ namespace: string;
21
+ slug: string;
22
+ styleImport: string;
23
+ textDomain: string;
24
+ title: string;
25
+ typeName: string;
26
+ };
27
+
25
28
  function parseArgs() {
26
29
  const args = process.argv.slice( 2 );
27
30
  const parsed: {
@@ -81,6 +84,26 @@ function toTitleCase( input: string ): string {
81
84
  .join( ' ' );
82
85
  }
83
86
 
87
+ function readJsonFile( filePath: string ): Record< string, unknown > {
88
+ let parsed: unknown;
89
+
90
+ try {
91
+ parsed = JSON.parse( fs.readFileSync( filePath, 'utf8' ) );
92
+ } catch ( error ) {
93
+ const errorMessage = error instanceof Error ? error.message : String( error );
94
+ throw new Error(
95
+ `Unable to parse JSON from ${ filePath }: ${ errorMessage }`,
96
+ { cause: error instanceof Error ? error : undefined }
97
+ );
98
+ }
99
+
100
+ if ( ! parsed || typeof parsed !== 'object' || Array.isArray( parsed ) ) {
101
+ throw new Error( `${ filePath } must contain a JSON object.` );
102
+ }
103
+
104
+ return parsed as Record< string, unknown >;
105
+ }
106
+
84
107
  function resolveValidatedNamespace( value: string ): string {
85
108
  const normalizedNamespace = toKebabCase( value );
86
109
 
@@ -101,6 +124,79 @@ function resolveValidatedBlockSlug( value: string ): string {
101
124
  return normalizedSlug;
102
125
  }
103
126
 
127
+ function resolveCompoundParentConfig(): CompoundParentConfig {
128
+ const blocksRoot = path.join( PROJECT_ROOT, 'src', 'blocks' );
129
+
130
+ if ( ! fs.existsSync( blocksRoot ) ) {
131
+ throw new Error(
132
+ 'This command expects a compound scaffold with src/blocks/<parent>/children.ts and scripts/block-config.ts.'
133
+ );
134
+ }
135
+
136
+ const parentCandidates = fs
137
+ .readdirSync( blocksRoot, { withFileTypes: true } )
138
+ .filter( ( entry ) => entry.isDirectory() )
139
+ .map( ( entry ) => path.join( blocksRoot, entry.name ) )
140
+ .filter( ( candidateDir ) =>
141
+ fs.existsSync( path.join( candidateDir, 'children.ts' ) )
142
+ );
143
+
144
+ if ( parentCandidates.length !== 1 ) {
145
+ throw new Error(
146
+ `Unable to resolve the compound parent block. Expected exactly one src/blocks/<parent>/children.ts entry, found ${ parentCandidates.length }.`
147
+ );
148
+ }
149
+
150
+ const parentDir = parentCandidates[ 0 ];
151
+ const parentSlug = resolveValidatedBlockSlug( path.basename( parentDir ) );
152
+ const blockJsonPath = path.join( parentDir, 'block.json' );
153
+
154
+ if ( ! fs.existsSync( blockJsonPath ) ) {
155
+ throw new Error( `Unable to resolve ${ blockJsonPath } for the compound parent block.` );
156
+ }
157
+
158
+ const blockJson = readJsonFile( blockJsonPath );
159
+ const blockName = typeof blockJson.name === 'string' ? blockJson.name.trim() : '';
160
+ const separatorIndex = blockName.indexOf( '/' );
161
+
162
+ if ( separatorIndex <= 0 || separatorIndex === blockName.length - 1 ) {
163
+ throw new Error(
164
+ `The parent block metadata at ${ blockJsonPath } must declare a valid "name" like "namespace/slug".`
165
+ );
166
+ }
167
+
168
+ const namespace = resolveValidatedNamespace( blockName.slice( 0, separatorIndex ) );
169
+ const title =
170
+ typeof blockJson.title === 'string' && blockJson.title.trim().length > 0
171
+ ? blockJson.title.trim()
172
+ : toTitleCase( parentSlug );
173
+ const textDomain =
174
+ typeof blockJson.textdomain === 'string' &&
175
+ blockJson.textdomain.trim().length > 0
176
+ ? blockJson.textdomain.trim()
177
+ : parentSlug;
178
+
179
+ return {
180
+ blockName,
181
+ namespace,
182
+ slug: parentSlug,
183
+ styleImport: `../${ parentSlug }/style.scss`,
184
+ textDomain,
185
+ title,
186
+ typeName: toPascalCase( parentSlug ),
187
+ };
188
+ }
189
+
190
+ const {
191
+ blockName: PARENT_BLOCK_NAME,
192
+ namespace: PARENT_BLOCK_NAMESPACE,
193
+ slug: PARENT_BLOCK_SLUG,
194
+ styleImport: PARENT_STYLE_IMPORT,
195
+ textDomain: TEXT_DOMAIN,
196
+ title: PARENT_BLOCK_TITLE,
197
+ typeName: PARENT_TYPE_NAME,
198
+ } = resolveCompoundParentConfig();
199
+
104
200
  function buildBlockCssClassName( namespace: string, slug: string ): string {
105
201
  const normalizedSlug = resolveValidatedBlockSlug( slug );
106
202
  const normalizedNamespace =
@@ -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
- export const bootstrapEndpoint = {
21
- ...get{{pascalCase}}BootstrapEndpoint,
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
- ...get{{pascalCase}}StateEndpoint,
26
- };
43
+ export const stateEndpoint = createRestEndpoint(
44
+ get{{pascalCase}}StateEndpoint
45
+ );
27
46
 
28
- export const writeStateEndpoint = {
29
- ...write{{pascalCase}}StateEndpoint,
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 result = await writeState( {
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
@@ -68,6 +68,7 @@
68
68
  },
69
69
  "textdomain": "{{textDomain}}",
70
70
  "editorScript": "file:./index.js",
71
+ "editorStyle": "file:./index.css",
71
72
  "style": "file:./style-index.css",
72
73
  "viewScriptModule": "file:./interactivity.js"
73
74
  }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * {{title}} Block Editor Styles
3
+ */
4
+
5
+ .{{cssClassName}} {
6
+ outline: 1px dashed #ddd;
7
+ outline-offset: -1px;
8
+ }
@@ -9,6 +9,7 @@ import {
9
9
  import Edit from './edit';
10
10
  import Save from './save';
11
11
  import metadata from './block.json';
12
+ import './editor.scss';
12
13
  import './style.scss';
13
14
 
14
15
  import type { {{pascalCase}}Attributes } from './types';
@@ -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
- persistencePolicy === 'authenticated'
76
- ? __( 'Writes require a logged-in user and a valid REST nonce.', '{{textDomain}}' )
77
- : __( 'Anonymous writes use signed short-lived public tokens, per-request ids, and coarse rate limiting.', '{{textDomain}}' );
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' ] }