@wp-typia/project-tools 0.15.0 → 0.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/runtime/cli-add.js +26 -70
  2. package/dist/runtime/cli-doctor.js +25 -9
  3. package/dist/runtime/cli-help.js +1 -0
  4. package/dist/runtime/cli-templates.js +10 -0
  5. package/dist/runtime/json-utils.d.ts +5 -8
  6. package/dist/runtime/json-utils.js +5 -10
  7. package/dist/runtime/metadata-analysis.d.ts +7 -11
  8. package/dist/runtime/metadata-analysis.js +7 -285
  9. package/dist/runtime/metadata-model.d.ts +7 -84
  10. package/dist/runtime/metadata-model.js +7 -59
  11. package/dist/runtime/metadata-parser.d.ts +5 -51
  12. package/dist/runtime/metadata-parser.js +5 -792
  13. package/dist/runtime/metadata-php-render.d.ts +5 -27
  14. package/dist/runtime/metadata-php-render.js +5 -547
  15. package/dist/runtime/metadata-projection.d.ts +7 -7
  16. package/dist/runtime/metadata-projection.js +7 -233
  17. package/dist/runtime/object-utils.d.ts +1 -1
  18. package/dist/runtime/object-utils.js +3 -6
  19. package/dist/runtime/persistence-rest-artifacts.d.ts +76 -0
  20. package/dist/runtime/persistence-rest-artifacts.js +99 -0
  21. package/dist/runtime/scaffold.d.ts +10 -2
  22. package/dist/runtime/scaffold.js +95 -1
  23. package/dist/runtime/template-builtins.js +1 -1
  24. package/dist/runtime/template-registry.d.ts +2 -1
  25. package/dist/runtime/template-registry.js +13 -2
  26. package/package.json +9 -8
  27. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +103 -7
  28. package/templates/_shared/persistence/core/src/api-validators.ts.mustache +14 -0
  29. package/templates/_shared/persistence/core/src/api.ts.mustache +28 -9
  30. package/templates/_shared/persistence/core/src/interactivity.ts.mustache +17 -11
  31. package/templates/interactivity/src/block.json.mustache +1 -0
  32. package/templates/interactivity/src/editor.scss.mustache +8 -0
  33. package/templates/interactivity/src/index.tsx.mustache +1 -0
  34. package/templates/persistence/src/edit.tsx.mustache +6 -6
@@ -1,233 +1,7 @@
1
- import { cloneJsonValue } from "./json-utils.js";
2
- import { getWordPressKind, } from "./metadata-model.js";
3
- export function createBlockJsonAttribute(node, warnings) {
4
- const attribute = {
5
- type: getWordPressKind(node),
6
- };
7
- if (node.defaultValue !== undefined) {
8
- attribute.default = cloneJsonValue(node.defaultValue);
9
- }
10
- if (node.enumValues !== null && node.enumValues.length > 0) {
11
- attribute.enum = [...node.enumValues];
12
- }
13
- if (node.wp.source !== null) {
14
- attribute.source = node.wp.source;
15
- }
16
- if (node.wp.selector !== null) {
17
- attribute.selector = node.wp.selector;
18
- }
19
- const reasons = [];
20
- if (node.constraints.exclusiveMaximum !== null)
21
- reasons.push("exclusiveMaximum");
22
- if (node.constraints.exclusiveMinimum !== null)
23
- reasons.push("exclusiveMinimum");
24
- if (node.constraints.format !== null)
25
- reasons.push("format");
26
- if (node.constraints.maxLength !== null)
27
- reasons.push("maxLength");
28
- if (node.constraints.maxItems !== null)
29
- reasons.push("maxItems");
30
- if (node.constraints.maximum !== null)
31
- reasons.push("maximum");
32
- if (node.constraints.minLength !== null)
33
- reasons.push("minLength");
34
- if (node.constraints.minItems !== null)
35
- reasons.push("minItems");
36
- if (node.constraints.minimum !== null)
37
- reasons.push("minimum");
38
- if (node.constraints.multipleOf !== null)
39
- reasons.push("multipleOf");
40
- if (node.constraints.pattern !== null)
41
- reasons.push("pattern");
42
- if (node.constraints.typeTag !== null)
43
- reasons.push("typeTag");
44
- if (node.kind === "array" && node.items !== undefined)
45
- reasons.push("items");
46
- if (node.kind === "object" && node.properties !== undefined)
47
- reasons.push("properties");
48
- if (node.kind === "union" && node.union !== null)
49
- reasons.push("union");
50
- if (reasons.length > 0) {
51
- warnings.push(`${node.path}: ${reasons.join(", ")}`);
52
- }
53
- return attribute;
54
- }
55
- export function createManifestAttribute(node) {
56
- return {
57
- typia: {
58
- constraints: { ...node.constraints },
59
- defaultValue: node.defaultValue === undefined ? null : cloneJsonValue(node.defaultValue),
60
- hasDefault: node.defaultValue !== undefined,
61
- },
62
- ts: {
63
- items: node.items ? createManifestAttribute(node.items) : null,
64
- kind: node.kind,
65
- properties: node.properties
66
- ? Object.fromEntries(Object.entries(node.properties).map(([key, property]) => [
67
- key,
68
- createManifestAttribute(property),
69
- ]))
70
- : null,
71
- required: node.required,
72
- union: node.union
73
- ? {
74
- branches: Object.fromEntries(Object.entries(node.union.branches).map(([key, branch]) => [
75
- key,
76
- createManifestAttribute(branch),
77
- ])),
78
- discriminator: node.union.discriminator,
79
- }
80
- : null,
81
- },
82
- wp: {
83
- defaultValue: node.defaultValue === undefined ? null : cloneJsonValue(node.defaultValue),
84
- enum: node.enumValues ? [...node.enumValues] : null,
85
- hasDefault: node.defaultValue !== undefined,
86
- ...(node.wp.selector !== null ? { selector: node.wp.selector } : {}),
87
- ...(node.wp.source !== null ? { source: node.wp.source } : {}),
88
- type: getWordPressKind(node),
89
- },
90
- };
91
- }
92
- export function createManifestDocument(sourceTypeName, attributes) {
93
- return {
94
- attributes: Object.fromEntries(Object.entries(attributes).map(([key, node]) => [
95
- key,
96
- createManifestAttribute(node),
97
- ])),
98
- manifestVersion: 2,
99
- sourceType: sourceTypeName,
100
- };
101
- }
102
- export function validateWordPressExtractionAttributes(attributes) {
103
- for (const attribute of Object.values(attributes)) {
104
- validateWordPressExtractionAttribute(attribute, true);
105
- }
106
- }
107
- export function validateWordPressExtractionAttribute(node, isTopLevel = false) {
108
- const hasSelector = node.wp.selector !== null;
109
- const hasSource = node.wp.source !== null;
110
- if (hasSelector || hasSource) {
111
- if (!isTopLevel) {
112
- throw new Error(`WordPress extraction tags are only supported on top-level block attributes at ${node.path}`);
113
- }
114
- if (!hasSelector || !hasSource) {
115
- throw new Error(`WordPress extraction tags require both Source and Selector at ${node.path}`);
116
- }
117
- if (node.kind !== "string") {
118
- throw new Error(`WordPress extraction tags are only supported on string attributes at ${node.path}`);
119
- }
120
- }
121
- if (node.items !== undefined) {
122
- validateWordPressExtractionAttribute(node.items, false);
123
- }
124
- if (node.properties !== undefined) {
125
- for (const property of Object.values(node.properties)) {
126
- validateWordPressExtractionAttribute(property, false);
127
- }
128
- }
129
- if (node.union?.branches) {
130
- for (const branch of Object.values(node.union.branches)) {
131
- validateWordPressExtractionAttribute(branch, false);
132
- }
133
- }
134
- }
135
- export function createExampleValue(node, key) {
136
- if (node.defaultValue !== undefined) {
137
- return cloneJsonValue(node.defaultValue);
138
- }
139
- if (node.enumValues !== null && node.enumValues.length > 0) {
140
- return cloneJsonValue(node.enumValues[0]);
141
- }
142
- switch (node.kind) {
143
- case "string":
144
- return fitStringExampleToConstraints(createFormattedStringExample(node.constraints.format, key), node.constraints);
145
- case "number":
146
- return createNumericExample(node);
147
- case "boolean":
148
- return true;
149
- case "array":
150
- return createArrayExample(node, key);
151
- case "object":
152
- return Object.fromEntries(Object.entries(node.properties ?? {}).map(([propertyKey, propertyNode]) => [
153
- propertyKey,
154
- createExampleValue(propertyNode, propertyKey),
155
- ]));
156
- case "union": {
157
- const firstBranch = node.union
158
- ? Object.values(node.union.branches)[0]
159
- : undefined;
160
- if (!firstBranch || firstBranch.kind !== "object") {
161
- return {};
162
- }
163
- return Object.fromEntries(Object.entries(firstBranch.properties ?? {}).map(([propertyKey, propertyNode]) => [
164
- propertyKey,
165
- createExampleValue(propertyNode, propertyKey),
166
- ]));
167
- }
168
- }
169
- }
170
- function createFormattedStringExample(format, key) {
171
- switch (format) {
172
- case "uuid":
173
- return "00000000-0000-4000-8000-000000000000";
174
- case "email":
175
- return "example@example.com";
176
- case "url":
177
- case "uri":
178
- return "https://example.com";
179
- case "ipv4":
180
- return "127.0.0.1";
181
- case "ipv6":
182
- return "::1";
183
- case "date-time":
184
- return "2024-01-01T00:00:00Z";
185
- default:
186
- return `Example ${key}`;
187
- }
188
- }
189
- function fitStringExampleToConstraints(value, constraints) {
190
- let nextValue = value;
191
- if (constraints.maxLength !== null &&
192
- nextValue.length > constraints.maxLength) {
193
- nextValue = nextValue.slice(0, constraints.maxLength);
194
- }
195
- if (constraints.minLength !== null &&
196
- nextValue.length < constraints.minLength) {
197
- nextValue = nextValue.padEnd(constraints.minLength, "x");
198
- }
199
- return nextValue;
200
- }
201
- function createNumericExample(node) {
202
- const { exclusiveMaximum, exclusiveMinimum, maximum, minimum, multipleOf, } = node.constraints;
203
- const step = multipleOf && multipleOf !== 0 ? multipleOf : 1;
204
- const minCandidate = minimum ?? (exclusiveMinimum !== null ? exclusiveMinimum + step : null);
205
- const maxCandidate = maximum ?? (exclusiveMaximum !== null ? exclusiveMaximum - step : null);
206
- let candidate = multipleOf && multipleOf !== 0
207
- ? minCandidate !== null
208
- ? Math.ceil(minCandidate / multipleOf) * multipleOf
209
- : maxCandidate !== null
210
- ? Math.floor(maxCandidate / multipleOf) * multipleOf
211
- : multipleOf
212
- : minCandidate ?? maxCandidate ?? 42;
213
- if (maxCandidate !== null && candidate > maxCandidate) {
214
- candidate =
215
- multipleOf && multipleOf !== 0
216
- ? Math.floor(maxCandidate / multipleOf) * multipleOf
217
- : maxCandidate;
218
- }
219
- if (minCandidate !== null && candidate < minCandidate) {
220
- candidate =
221
- multipleOf && multipleOf !== 0
222
- ? Math.ceil(minCandidate / multipleOf) * multipleOf
223
- : minCandidate;
224
- }
225
- return candidate;
226
- }
227
- function createArrayExample(node, key) {
228
- const minimumItems = Math.max(node.constraints.minItems ?? 0, 0);
229
- if (minimumItems === 0 || node.items === undefined) {
230
- return [];
231
- }
232
- return Array.from({ length: minimumItems }, (_value, index) => createExampleValue(node.items, `${key}${index + 1}`));
233
- }
1
+ /**
2
+ * Re-exports metadata projection utilities from `@wp-typia/block-runtime`.
3
+ * This adapter keeps the public project-tools runtime path stable while the
4
+ * implementation is consolidated in block-runtime.
5
+ * @module
6
+ */
7
+ export * from "@wp-typia/block-runtime/metadata-projection";
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export type UnknownRecord = Record<string, unknown>;
5
5
  /**
6
- * Check whether a value is a plain object record.
6
+ * Delegate plain-object detection to the shared runtime primitive owner.
7
7
  *
8
8
  * @param value Runtime value to inspect.
9
9
  * @returns `true` when the value is a non-null plain object with an
@@ -1,14 +1,11 @@
1
+ import { isPlainObject as isSharedPlainObject } from "@wp-typia/api-client/runtime-primitives";
1
2
  /**
2
- * Check whether a value is a plain object record.
3
+ * Delegate plain-object detection to the shared runtime primitive owner.
3
4
  *
4
5
  * @param value Runtime value to inspect.
5
6
  * @returns `true` when the value is a non-null plain object with an
6
7
  * `Object.prototype` or `null` prototype.
7
8
  */
8
9
  export function isPlainObject(value) {
9
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
10
- return false;
11
- }
12
- const prototype = Object.getPrototypeOf(value);
13
- return prototype === Object.prototype || prototype === null;
10
+ return isSharedPlainObject(value);
14
11
  }
@@ -0,0 +1,76 @@
1
+ interface PersistenceTemplateVariablesLike {
2
+ namespace: string;
3
+ pascalCase: string;
4
+ restWriteAuthIntent: "authenticated" | "public-write-protected";
5
+ restWriteAuthMechanism: "public-signed-token" | "rest-nonce";
6
+ slugKebabCase: string;
7
+ title: string;
8
+ }
9
+ interface SyncPersistenceRestArtifactsOptions {
10
+ apiTypesFile: string;
11
+ outputDir: string;
12
+ projectDir: string;
13
+ variables: PersistenceTemplateVariablesLike;
14
+ }
15
+ /**
16
+ * Build the canonical persistence REST endpoint manifest for scaffold-time
17
+ * schema, OpenAPI, and client generation.
18
+ *
19
+ * @param variables Persistence template naming and auth metadata.
20
+ * @returns Endpoint manifest covering bootstrap, state read, and state write operations.
21
+ */
22
+ export declare function buildPersistenceEndpointManifest(variables: PersistenceTemplateVariablesLike): import("@wp-typia/block-runtime/metadata-core").EndpointManifestDefinition<{
23
+ readonly "bootstrap-query": {
24
+ readonly sourceTypeName: `${string}BootstrapQuery`;
25
+ };
26
+ readonly "bootstrap-response": {
27
+ readonly sourceTypeName: `${string}BootstrapResponse`;
28
+ };
29
+ readonly "state-query": {
30
+ readonly sourceTypeName: `${string}StateQuery`;
31
+ };
32
+ readonly "state-response": {
33
+ readonly sourceTypeName: `${string}StateResponse`;
34
+ };
35
+ readonly "write-state-request": {
36
+ readonly sourceTypeName: `${string}WriteStateRequest`;
37
+ };
38
+ }, readonly [{
39
+ readonly auth: "public";
40
+ readonly method: "GET";
41
+ readonly operationId: `get${string}State`;
42
+ readonly path: `/${string}/v1/${string}/state`;
43
+ readonly queryContract: "state-query";
44
+ readonly responseContract: "state-response";
45
+ readonly summary: "Read the current persisted state.";
46
+ readonly tags: readonly [string];
47
+ }, {
48
+ readonly auth: "authenticated" | "public-write-protected";
49
+ readonly bodyContract: "write-state-request";
50
+ readonly method: "POST";
51
+ readonly operationId: `write${string}State`;
52
+ readonly path: `/${string}/v1/${string}/state`;
53
+ readonly responseContract: "state-response";
54
+ readonly summary: "Write the current persisted state.";
55
+ readonly tags: readonly [string];
56
+ readonly wordpressAuth: {
57
+ readonly mechanism: "public-signed-token" | "rest-nonce";
58
+ };
59
+ }, {
60
+ readonly auth: "public";
61
+ readonly method: "GET";
62
+ readonly operationId: `get${string}Bootstrap`;
63
+ readonly path: `/${string}/v1/${string}/bootstrap`;
64
+ readonly queryContract: "bootstrap-query";
65
+ readonly responseContract: "bootstrap-response";
66
+ readonly summary: "Read fresh session bootstrap state for the current viewer.";
67
+ readonly tags: readonly [string];
68
+ }]>;
69
+ /**
70
+ * Generate the REST-derived persistence artifacts for a scaffolded block.
71
+ *
72
+ * @param options Scaffold output paths plus persistence template variables.
73
+ * @returns A promise that resolves after schema, OpenAPI, and client files are written.
74
+ */
75
+ export declare function syncPersistenceRestArtifacts({ apiTypesFile, outputDir, projectDir, variables, }: SyncPersistenceRestArtifactsOptions): Promise<void>;
76
+ export {};
@@ -0,0 +1,99 @@
1
+ import path from "node:path";
2
+ import { defineEndpointManifest, syncEndpointClient, syncRestOpenApi, syncTypeSchemas, } from "@wp-typia/block-runtime/metadata-core";
3
+ /**
4
+ * Build the canonical persistence REST endpoint manifest for scaffold-time
5
+ * schema, OpenAPI, and client generation.
6
+ *
7
+ * @param variables Persistence template naming and auth metadata.
8
+ * @returns Endpoint manifest covering bootstrap, state read, and state write operations.
9
+ */
10
+ export function buildPersistenceEndpointManifest(variables) {
11
+ return defineEndpointManifest({
12
+ contracts: {
13
+ "bootstrap-query": {
14
+ sourceTypeName: `${variables.pascalCase}BootstrapQuery`,
15
+ },
16
+ "bootstrap-response": {
17
+ sourceTypeName: `${variables.pascalCase}BootstrapResponse`,
18
+ },
19
+ "state-query": {
20
+ sourceTypeName: `${variables.pascalCase}StateQuery`,
21
+ },
22
+ "state-response": {
23
+ sourceTypeName: `${variables.pascalCase}StateResponse`,
24
+ },
25
+ "write-state-request": {
26
+ sourceTypeName: `${variables.pascalCase}WriteStateRequest`,
27
+ },
28
+ },
29
+ endpoints: [
30
+ {
31
+ auth: "public",
32
+ method: "GET",
33
+ operationId: `get${variables.pascalCase}State`,
34
+ path: `/${variables.namespace}/v1/${variables.slugKebabCase}/state`,
35
+ queryContract: "state-query",
36
+ responseContract: "state-response",
37
+ summary: "Read the current persisted state.",
38
+ tags: [variables.title],
39
+ },
40
+ {
41
+ auth: variables.restWriteAuthIntent,
42
+ bodyContract: "write-state-request",
43
+ method: "POST",
44
+ operationId: `write${variables.pascalCase}State`,
45
+ path: `/${variables.namespace}/v1/${variables.slugKebabCase}/state`,
46
+ responseContract: "state-response",
47
+ summary: "Write the current persisted state.",
48
+ tags: [variables.title],
49
+ wordpressAuth: {
50
+ mechanism: variables.restWriteAuthMechanism,
51
+ },
52
+ },
53
+ {
54
+ auth: "public",
55
+ method: "GET",
56
+ operationId: `get${variables.pascalCase}Bootstrap`,
57
+ path: `/${variables.namespace}/v1/${variables.slugKebabCase}/bootstrap`,
58
+ queryContract: "bootstrap-query",
59
+ responseContract: "bootstrap-response",
60
+ summary: "Read fresh session bootstrap state for the current viewer.",
61
+ tags: [variables.title],
62
+ },
63
+ ],
64
+ info: {
65
+ title: `${variables.title} REST API`,
66
+ version: "1.0.0",
67
+ },
68
+ });
69
+ }
70
+ /**
71
+ * Generate the REST-derived persistence artifacts for a scaffolded block.
72
+ *
73
+ * @param options Scaffold output paths plus persistence template variables.
74
+ * @returns A promise that resolves after schema, OpenAPI, and client files are written.
75
+ */
76
+ export async function syncPersistenceRestArtifacts({ apiTypesFile, outputDir, projectDir, variables, }) {
77
+ const manifest = buildPersistenceEndpointManifest(variables);
78
+ for (const [baseName, contract] of Object.entries(manifest.contracts)) {
79
+ await syncTypeSchemas({
80
+ jsonSchemaFile: path.join(outputDir, "api-schemas", `${baseName}.schema.json`),
81
+ openApiFile: path.join(outputDir, "api-schemas", `${baseName}.openapi.json`),
82
+ projectRoot: projectDir,
83
+ sourceTypeName: contract.sourceTypeName,
84
+ typesFile: apiTypesFile,
85
+ });
86
+ }
87
+ await syncRestOpenApi({
88
+ manifest,
89
+ openApiFile: path.join(outputDir, "api.openapi.json"),
90
+ projectRoot: projectDir,
91
+ typesFile: apiTypesFile,
92
+ });
93
+ await syncEndpointClient({
94
+ clientFile: path.join(outputDir, "api-client.ts"),
95
+ manifest,
96
+ projectRoot: projectDir,
97
+ typesFile: apiTypesFile,
98
+ });
99
+ }
@@ -1,5 +1,4 @@
1
1
  import type { PackageManagerId } from "./package-managers.js";
2
- import type { BuiltInTemplateId } from "./template-registry.js";
3
2
  /**
4
3
  * User-facing scaffold answers before template rendering.
5
4
  *
@@ -57,6 +56,7 @@ export interface ScaffoldTemplateVariables extends Record<string, string> {
57
56
  isAuthenticatedPersistencePolicy: "false" | "true";
58
57
  isPublicPersistencePolicy: "false" | "true";
59
58
  bootstrapCredentialDeclarations: string;
59
+ persistencePolicyDescriptionJson: string;
60
60
  publicWriteRequestIdDeclaration: string;
61
61
  restPackageVersion: string;
62
62
  restWriteAuthIntent: "authenticated" | "public-write-protected";
@@ -73,9 +73,17 @@ export interface ScaffoldTemplateVariables extends Record<string, string> {
73
73
  titleCase: string;
74
74
  persistencePolicy: PersistencePolicy;
75
75
  }
76
+ /**
77
+ * Resolve scaffold template input from either built-in template ids or custom
78
+ * template identifiers such as local paths, GitHub refs, and npm packages.
79
+ *
80
+ * The callback returns `Promise<string>` on purpose so interactive selection
81
+ * can surface custom ids. Downstream code uses `isBuiltInTemplateId()` to
82
+ * distinguish built-in templates from custom sources.
83
+ */
76
84
  interface ResolveTemplateOptions {
77
85
  isInteractive?: boolean;
78
- selectTemplate?: () => Promise<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
  }