@wp-typia/project-tools 0.23.1 → 0.24.0

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 (152) hide show
  1. package/dist/runtime/built-in-block-non-ts-basic-artifacts.d.ts +9 -0
  2. package/dist/runtime/built-in-block-non-ts-basic-artifacts.js +84 -0
  3. package/dist/runtime/built-in-block-non-ts-compound-artifacts.d.ts +9 -0
  4. package/dist/runtime/built-in-block-non-ts-compound-artifacts.js +36 -0
  5. package/dist/runtime/built-in-block-non-ts-compound-templates.d.ts +23 -0
  6. package/dist/runtime/built-in-block-non-ts-compound-templates.js +453 -0
  7. package/dist/runtime/built-in-block-non-ts-family-artifacts.d.ts +8 -26
  8. package/dist/runtime/built-in-block-non-ts-family-artifacts.js +8 -1034
  9. package/dist/runtime/built-in-block-non-ts-interactivity-artifacts.d.ts +9 -0
  10. package/dist/runtime/built-in-block-non-ts-interactivity-artifacts.js +83 -0
  11. package/dist/runtime/built-in-block-non-ts-persistence-artifacts.d.ts +9 -0
  12. package/dist/runtime/built-in-block-non-ts-persistence-artifacts.js +33 -0
  13. package/dist/runtime/built-in-block-non-ts-persistence-templates.d.ts +23 -0
  14. package/dist/runtime/built-in-block-non-ts-persistence-templates.js +395 -0
  15. package/dist/runtime/cli-add-collision.js +8 -0
  16. package/dist/runtime/cli-add-help.js +10 -7
  17. package/dist/runtime/cli-add-kind-ids.d.ts +1 -1
  18. package/dist/runtime/cli-add-kind-ids.js +1 -0
  19. package/dist/runtime/cli-add-types.d.ts +28 -1
  20. package/dist/runtime/cli-add-types.js +2 -0
  21. package/dist/runtime/cli-add-workspace-ability-anchors.d.ts +24 -0
  22. package/dist/runtime/cli-add-workspace-ability-anchors.js +294 -0
  23. package/dist/runtime/cli-add-workspace-ability-registry.d.ts +10 -0
  24. package/dist/runtime/cli-add-workspace-ability-registry.js +51 -0
  25. package/dist/runtime/cli-add-workspace-ability-scaffold.d.ts +1 -1
  26. package/dist/runtime/cli-add-workspace-ability-scaffold.js +5 -311
  27. package/dist/runtime/cli-add-workspace-admin-view-scaffold.js +1 -1
  28. package/dist/runtime/cli-add-workspace-ai-anchors.d.ts +4 -4
  29. package/dist/runtime/cli-add-workspace-ai-anchors.js +4 -232
  30. package/dist/runtime/cli-add-workspace-ai-scaffold.js +4 -2
  31. package/dist/runtime/cli-add-workspace-ai-source-emitters.d.ts +1 -4
  32. package/dist/runtime/cli-add-workspace-ai-source-emitters.js +1 -145
  33. package/dist/runtime/cli-add-workspace-ai-sync-rest-anchors.d.ts +5 -0
  34. package/dist/runtime/cli-add-workspace-ai-sync-rest-anchors.js +236 -0
  35. package/dist/runtime/cli-add-workspace-ai-sync-script-source.d.ts +4 -0
  36. package/dist/runtime/cli-add-workspace-ai-sync-script-source.js +145 -0
  37. package/dist/runtime/cli-add-workspace-assets.d.ts +6 -63
  38. package/dist/runtime/cli-add-workspace-assets.js +6 -950
  39. package/dist/runtime/cli-add-workspace-binding-source-anchors.d.ts +23 -0
  40. package/dist/runtime/cli-add-workspace-binding-source-anchors.js +112 -0
  41. package/dist/runtime/cli-add-workspace-binding-source-source-emitters.d.ts +33 -0
  42. package/dist/runtime/cli-add-workspace-binding-source-source-emitters.js +436 -0
  43. package/dist/runtime/cli-add-workspace-binding-source-types.d.ts +20 -0
  44. package/dist/runtime/cli-add-workspace-binding-source-types.js +1 -0
  45. package/dist/runtime/cli-add-workspace-binding-source.d.ts +40 -0
  46. package/dist/runtime/cli-add-workspace-binding-source.js +275 -0
  47. package/dist/runtime/cli-add-workspace-block-style.d.ts +22 -0
  48. package/dist/runtime/cli-add-workspace-block-style.js +148 -0
  49. package/dist/runtime/cli-add-workspace-block-transform.d.ts +32 -0
  50. package/dist/runtime/cli-add-workspace-block-transform.js +197 -0
  51. package/dist/runtime/cli-add-workspace-contract.js +1 -1
  52. package/dist/runtime/cli-add-workspace-core-variation.d.ts +20 -0
  53. package/dist/runtime/cli-add-workspace-core-variation.js +322 -0
  54. package/dist/runtime/cli-add-workspace-editor-plugin-anchors.d.ts +37 -0
  55. package/dist/runtime/cli-add-workspace-editor-plugin-anchors.js +206 -0
  56. package/dist/runtime/cli-add-workspace-editor-plugin-source-emitters.d.ts +47 -0
  57. package/dist/runtime/cli-add-workspace-editor-plugin-source-emitters.js +219 -0
  58. package/dist/runtime/cli-add-workspace-editor-plugin.d.ts +22 -0
  59. package/dist/runtime/cli-add-workspace-editor-plugin.js +78 -0
  60. package/dist/runtime/cli-add-workspace-hooked-block.d.ts +23 -0
  61. package/dist/runtime/cli-add-workspace-hooked-block.js +57 -0
  62. package/dist/runtime/cli-add-workspace-integration-env-files.d.ts +33 -0
  63. package/dist/runtime/cli-add-workspace-integration-env-files.js +65 -0
  64. package/dist/runtime/cli-add-workspace-integration-env-package-json.d.ts +38 -0
  65. package/dist/runtime/cli-add-workspace-integration-env-package-json.js +122 -0
  66. package/dist/runtime/cli-add-workspace-integration-env-source-emitters.d.ts +44 -0
  67. package/dist/runtime/cli-add-workspace-integration-env-source-emitters.js +262 -0
  68. package/dist/runtime/cli-add-workspace-integration-env.js +5 -345
  69. package/dist/runtime/cli-add-workspace-pattern-anchors.d.ts +10 -0
  70. package/dist/runtime/cli-add-workspace-pattern-anchors.js +95 -0
  71. package/dist/runtime/cli-add-workspace-pattern-options.d.ts +20 -0
  72. package/dist/runtime/cli-add-workspace-pattern-options.js +113 -0
  73. package/dist/runtime/cli-add-workspace-pattern-source-emitters.d.ts +20 -0
  74. package/dist/runtime/cli-add-workspace-pattern-source-emitters.js +57 -0
  75. package/dist/runtime/cli-add-workspace-pattern.d.ts +42 -0
  76. package/dist/runtime/cli-add-workspace-pattern.js +99 -0
  77. package/dist/runtime/cli-add-workspace-post-meta.js +1 -1
  78. package/dist/runtime/cli-add-workspace-registration-hooks.d.ts +50 -0
  79. package/dist/runtime/cli-add-workspace-registration-hooks.js +162 -0
  80. package/dist/runtime/cli-add-workspace-rest-anchors.d.ts +6 -9
  81. package/dist/runtime/cli-add-workspace-rest-anchors.js +6 -466
  82. package/dist/runtime/cli-add-workspace-rest-bootstrap-anchors.d.ts +17 -0
  83. package/dist/runtime/cli-add-workspace-rest-bootstrap-anchors.js +108 -0
  84. package/dist/runtime/cli-add-workspace-rest-contract-sync-anchors.d.ts +9 -0
  85. package/dist/runtime/cli-add-workspace-rest-contract-sync-anchors.js +142 -0
  86. package/dist/runtime/cli-add-workspace-rest-generated-source-emitters.d.ts +51 -0
  87. package/dist/runtime/cli-add-workspace-rest-generated-source-emitters.js +415 -0
  88. package/dist/runtime/cli-add-workspace-rest-generated.js +5 -3
  89. package/dist/runtime/cli-add-workspace-rest-manual-source-emitters.d.ts +80 -0
  90. package/dist/runtime/cli-add-workspace-rest-manual-source-emitters.js +238 -0
  91. package/dist/runtime/cli-add-workspace-rest-manual.js +3 -16
  92. package/dist/runtime/cli-add-workspace-rest-php-templates.d.ts +1 -7
  93. package/dist/runtime/cli-add-workspace-rest-php-templates.js +3 -322
  94. package/dist/runtime/cli-add-workspace-rest-resource-php-routing-template.d.ts +33 -0
  95. package/dist/runtime/cli-add-workspace-rest-resource-php-routing-template.js +145 -0
  96. package/dist/runtime/cli-add-workspace-rest-resource-sync-anchors.d.ts +9 -0
  97. package/dist/runtime/cli-add-workspace-rest-resource-sync-anchors.js +162 -0
  98. package/dist/runtime/cli-add-workspace-rest-schema-helper-php-template.d.ts +7 -0
  99. package/dist/runtime/cli-add-workspace-rest-schema-helper-php-template.js +193 -0
  100. package/dist/runtime/cli-add-workspace-rest-source-emitters.d.ts +5 -99
  101. package/dist/runtime/cli-add-workspace-rest-source-emitters.js +5 -663
  102. package/dist/runtime/cli-add-workspace-rest-source-utils.d.ts +17 -0
  103. package/dist/runtime/cli-add-workspace-rest-source-utils.js +50 -0
  104. package/dist/runtime/cli-add-workspace-rest-sync-script-shared.d.ts +56 -0
  105. package/dist/runtime/cli-add-workspace-rest-sync-script-shared.js +122 -0
  106. package/dist/runtime/cli-add-workspace-rest-types.d.ts +3 -3
  107. package/dist/runtime/cli-add-workspace-variation.d.ts +22 -0
  108. package/dist/runtime/cli-add-workspace-variation.js +162 -0
  109. package/dist/runtime/cli-add-workspace.d.ts +42 -107
  110. package/dist/runtime/cli-add-workspace.js +42 -674
  111. package/dist/runtime/cli-add.d.ts +3 -3
  112. package/dist/runtime/cli-add.js +2 -2
  113. package/dist/runtime/cli-core.d.ts +2 -1
  114. package/dist/runtime/cli-core.js +1 -1
  115. package/dist/runtime/cli-doctor-workspace-bindings.js +59 -0
  116. package/dist/runtime/cli-doctor-workspace-block-addons.js +33 -5
  117. package/dist/runtime/cli-doctor.d.ts +2 -0
  118. package/dist/runtime/cli-doctor.js +13 -2
  119. package/dist/runtime/cli-help.js +6 -4
  120. package/dist/runtime/index.d.ts +5 -2
  121. package/dist/runtime/index.js +4 -2
  122. package/dist/runtime/local-dev-presets.js +2 -1
  123. package/dist/runtime/package-versions.d.ts +1 -0
  124. package/dist/runtime/package-versions.js +10 -2
  125. package/dist/runtime/pattern-catalog.d.ts +122 -0
  126. package/dist/runtime/pattern-catalog.js +471 -0
  127. package/dist/runtime/post-meta-binding-fields.d.ts +46 -0
  128. package/dist/runtime/post-meta-binding-fields.js +135 -0
  129. package/dist/runtime/typia-llm-json-schema.d.ts +24 -0
  130. package/dist/runtime/typia-llm-json-schema.js +33 -0
  131. package/dist/runtime/typia-llm-openapi-constraints.d.ts +20 -0
  132. package/dist/runtime/typia-llm-openapi-constraints.js +254 -0
  133. package/dist/runtime/typia-llm-projection.d.ts +25 -0
  134. package/dist/runtime/typia-llm-projection.js +58 -0
  135. package/dist/runtime/typia-llm-render.d.ts +21 -0
  136. package/dist/runtime/typia-llm-render.js +252 -0
  137. package/dist/runtime/typia-llm-sync.d.ts +10 -0
  138. package/dist/runtime/typia-llm-sync.js +63 -0
  139. package/dist/runtime/typia-llm-types.d.ts +197 -0
  140. package/dist/runtime/typia-llm-types.js +1 -0
  141. package/dist/runtime/typia-llm.d.ts +9 -255
  142. package/dist/runtime/typia-llm.js +5 -634
  143. package/dist/runtime/workspace-inventory-mutations.js +13 -0
  144. package/dist/runtime/workspace-inventory-section-descriptors.js +9 -1
  145. package/dist/runtime/workspace-inventory-templates.d.ts +2 -2
  146. package/dist/runtime/workspace-inventory-templates.js +9 -1
  147. package/dist/runtime/workspace-inventory-types.d.ts +9 -1
  148. package/package.json +8 -3
  149. package/templates/_shared/compound/core/scripts/block-config.ts.mustache +22 -0
  150. package/templates/_shared/compound/core/scripts/sync-types-to-block-json.ts.mustache +103 -2
  151. package/templates/_shared/compound/core/src/inner-blocks-templates.ts.mustache +13 -0
  152. package/templates/_shared/compound/persistence/scripts/block-config.ts.mustache +22 -1
@@ -1,351 +1,11 @@
1
- import { promises as fsp } from "node:fs";
2
1
  import path from "node:path";
3
2
  import { assertValidGeneratedSlug, assertValidIntegrationEnvService, normalizeBlockSlug, } from "./cli-add-shared.js";
4
- import { formatRunScript, } from "./package-managers.js";
5
- import { pathExists, readOptionalUtf8File } from "./fs-async.js";
6
- import { readJsonFile } from "./json-utils.js";
3
+ import { appendMissingLines, writeFileIfAbsent, writeNewScaffoldFile, } from "./cli-add-workspace-integration-env-files.js";
4
+ import { addIntegrationEnvPackageJsonEntries, mutateIntegrationEnvPackageJson, } from "./cli-add-workspace-integration-env-package-json.js";
5
+ import { buildDockerComposeSource, buildEnvExampleSource, buildIntegrationEnvReadmeSource, buildIntegrationSmokeScriptSource, buildWpEnvConfigSource, } from "./cli-add-workspace-integration-env-source-emitters.js";
7
6
  import { executeWorkspaceMutationPlan } from "./cli-add-workspace-mutation.js";
7
+ import { pathExists } from "./fs-async.js";
8
8
  import { resolveWorkspaceProject } from "./workspace-project.js";
9
- import { toTitleCase } from "./string-case.js";
10
- const WP_ENV_PACKAGE_VERSION = "^11.2.0";
11
- function buildWpEnvConfigSource() {
12
- return `${JSON.stringify({
13
- $schema: "https://schemas.wp.org/trunk/wp-env.json",
14
- core: null,
15
- port: 8888,
16
- testsEnvironment: false,
17
- plugins: ["."],
18
- config: {
19
- WP_DEBUG: true,
20
- WP_DEBUG_LOG: true,
21
- WP_DEBUG_DISPLAY: false,
22
- SCRIPT_DEBUG: true,
23
- WP_ENVIRONMENT_TYPE: "local",
24
- },
25
- }, null, 2)}\n`;
26
- }
27
- function buildDockerComposeSource() {
28
- return `services:
29
- integration-service:
30
- image: node:22-alpine
31
- working_dir: /workspace
32
- volumes:
33
- - .:/workspace
34
- command: >
35
- node -e "require('node:http').createServer((request, response) => {
36
- response.writeHead(request.url === '/health' ? 200 : 404, {
37
- 'content-type': 'application/json'
38
- });
39
- response.end(JSON.stringify({
40
- ok: request.url === '/health',
41
- service: 'wp-typia-integration-starter'
42
- }));
43
- }).listen(3000, '0.0.0.0')"
44
- ports:
45
- - "3000:3000"
46
- `;
47
- }
48
- function buildIntegrationSmokeScriptSource(integrationEnvSlug) {
49
- return `import fs from "node:fs";
50
- import path from "node:path";
51
- import { fileURLToPath } from "node:url";
52
-
53
- const ROOT_DIR = path.resolve(
54
- fileURLToPath(new URL("../..", import.meta.url)),
55
- );
56
- const ENV_FILE = path.join(ROOT_DIR, ".env");
57
-
58
- function parseEnvValue(value) {
59
- const trimmed = value.trim();
60
- if (
61
- (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
62
- (trimmed.startsWith("'") && trimmed.endsWith("'"))
63
- ) {
64
- return trimmed.slice(1, -1);
65
- }
66
-
67
- return trimmed;
68
- }
69
-
70
- function readEnvFile(filePath) {
71
- if (!fs.existsSync(filePath)) {
72
- return {};
73
- }
74
-
75
- return Object.fromEntries(
76
- fs
77
- .readFileSync(filePath, "utf8")
78
- .split(/\\r?\\n/u)
79
- .map((line) => line.trim())
80
- .filter((line) => line.length > 0 && !line.startsWith("#"))
81
- .map((line) => {
82
- const separatorIndex = line.indexOf("=");
83
- if (separatorIndex === -1) {
84
- return null;
85
- }
86
-
87
- return [
88
- line.slice(0, separatorIndex).trim(),
89
- parseEnvValue(line.slice(separatorIndex + 1)),
90
- ];
91
- })
92
- .filter(Boolean),
93
- );
94
- }
95
-
96
- const envFile = readEnvFile(ENV_FILE);
97
-
98
- function getEnv(name, fallback) {
99
- return process.env[name] ?? envFile[name] ?? fallback;
100
- }
101
-
102
- function resolveEndpointUrl(baseUrl, endpointPath) {
103
- const normalizedBaseUrl = new URL(baseUrl);
104
- if (!normalizedBaseUrl.pathname.endsWith("/")) {
105
- normalizedBaseUrl.pathname = \`\${normalizedBaseUrl.pathname}/\`;
106
- }
107
-
108
- const relativePath = endpointPath.replace(/^\\/+/u, "");
109
- return new URL(relativePath, normalizedBaseUrl);
110
- }
111
-
112
- async function assertJsonEndpoint(label, url) {
113
- const response = await fetch(url, {
114
- headers: {
115
- accept: "application/json",
116
- },
117
- });
118
-
119
- if (!response.ok) {
120
- throw new Error(
121
- \`\${label} failed at \${url} with HTTP \${response.status}.\`,
122
- );
123
- }
124
-
125
- const contentType = response.headers.get("content-type") ?? "";
126
- if (!contentType.includes("application/json")) {
127
- throw new Error(
128
- \`\${label} at \${url} did not return JSON (content-type: \${contentType || "missing"}).\`,
129
- );
130
- }
131
-
132
- return response.json();
133
- }
134
-
135
- const baseUrl = new URL(
136
- getEnv("WP_TYPIA_SMOKE_BASE_URL", "http://localhost:8888"),
137
- );
138
- const serviceUrl = getEnv("WP_TYPIA_SERVICE_URL", "").trim();
139
-
140
- // Extend this starter with project-specific generated REST clients or schema
141
- // checks as the workspace grows. For example, read JSON schemas under
142
- // src/rest/<resource>/api-schemas or import TS clients through a tsx-powered
143
- // smoke runner when you need authenticated route coverage.
144
-
145
- await assertJsonEndpoint(
146
- "WordPress REST index",
147
- resolveEndpointUrl(baseUrl, "wp-json/"),
148
- );
149
-
150
- if (serviceUrl.length > 0) {
151
- await assertJsonEndpoint(
152
- "Local integration service healthcheck",
153
- resolveEndpointUrl(serviceUrl, "health"),
154
- );
155
- }
156
-
157
- console.log("wp-typia integration smoke passed: ${integrationEnvSlug}");
158
- `;
159
- }
160
- function buildEnvExampleSource(service) {
161
- return [
162
- "# wp-typia integration smoke settings",
163
- "WP_TYPIA_SMOKE_BASE_URL=http://localhost:8888",
164
- "WP_TYPIA_SMOKE_USERNAME=admin",
165
- "WP_TYPIA_SMOKE_PASSWORD=password",
166
- ...(service === "docker-compose"
167
- ? [
168
- "",
169
- "# Optional docker-compose integration service starter.",
170
- "WP_TYPIA_SERVICE_URL=http://localhost:3000",
171
- ]
172
- : [
173
- "",
174
- "# Set this when your smoke test needs a project-specific service.",
175
- "# WP_TYPIA_SERVICE_URL=http://localhost:3000",
176
- ]),
177
- "",
178
- ].join("\n");
179
- }
180
- function buildIntegrationEnvReadmeSource({ integrationEnvSlug, service, withReleaseZip, withWpEnv, }) {
181
- const title = toTitleCase(integrationEnvSlug);
182
- const setupSteps = [
183
- "Copy `.env.example` to `.env` and adjust the URLs or credentials for your local project.",
184
- ...(withWpEnv
185
- ? [
186
- "Run `npm run wp-env:start` to start the generated WordPress environment.",
187
- ]
188
- : [
189
- "Point `WP_TYPIA_SMOKE_BASE_URL` at the WordPress environment you already run locally.",
190
- ]),
191
- ...(service === "docker-compose"
192
- ? [
193
- "Run `npm run service:start` if you want the placeholder docker-compose service available at `WP_TYPIA_SERVICE_URL`.",
194
- ]
195
- : [
196
- "Set `WP_TYPIA_SERVICE_URL` only when your integration smoke needs a local service dependency.",
197
- ]),
198
- `Run \`npm run smoke:${integrationEnvSlug}\` to execute the starter smoke check.`,
199
- ...(withReleaseZip
200
- ? [
201
- "Run `npm run release:zip` after smoke checks pass to build a distributable plugin zip.",
202
- ]
203
- : []),
204
- ];
205
- return `# ${title} Integration Environment
206
-
207
- This starter keeps local WordPress integration smoke checks opt-in and editable.
208
- It does not change default block scaffolds or require wp-env unless this add
209
- workflow was run with \`--wp-env\`.
210
-
211
- ## Setup
212
-
213
- ${setupSteps.map((step, index) => `${index + 1}. ${step}`).join("\n")}
214
-
215
- ## Adapting the Starter
216
-
217
- - Extend \`scripts/integration-smoke/${integrationEnvSlug}.mjs\` with the REST,
218
- editor, or service assertions that matter for this project.
219
- - Keep secrets in \`.env\`; \`.env.example\` should document only safe defaults.
220
- - If your project uses a real service stack, replace the placeholder
221
- \`docker-compose.integration.yml\` service with your database, queue, API, or
222
- emulator containers.
223
- - Keep the smoke script focused on high-signal integration checks so CI and
224
- local debugging stay fast.
225
- ${withReleaseZip ? "- Treat `release:zip:check` as a CI guard before packaging release artifacts.\n" : ""}
226
- `;
227
- }
228
- async function appendMissingLines(filePath, lines) {
229
- const current = (await readOptionalUtf8File(filePath)) ?? "";
230
- const missingLines = lines.filter((line) => !current.includes(`${line}\n`) && !current.endsWith(line));
231
- if (missingLines.length === 0) {
232
- return;
233
- }
234
- const separator = current.length === 0 || current.endsWith("\n") ? "" : "\n";
235
- await fsp.mkdir(path.dirname(filePath), { recursive: true });
236
- await fsp.writeFile(filePath, `${current}${separator}${missingLines.join("\n")}\n`, "utf8");
237
- }
238
- async function writeFileIfAbsent({ filePath, source, warnings, }) {
239
- if (await pathExists(filePath)) {
240
- warnings.push(`Preserved existing ${path.basename(filePath)}; review it manually if you need different local integration settings.`);
241
- return;
242
- }
243
- await fsp.mkdir(path.dirname(filePath), { recursive: true });
244
- await fsp.writeFile(filePath, source, "utf8");
245
- }
246
- async function writeNewScaffoldFile(filePath, source) {
247
- if (await pathExists(filePath)) {
248
- throw new Error(`An integration environment scaffold already exists at ${filePath}. Choose a different name.`);
249
- }
250
- await fsp.mkdir(path.dirname(filePath), { recursive: true });
251
- await fsp.writeFile(filePath, source, "utf8");
252
- }
253
- function addScriptIfMissing({ scriptName, scripts, scriptValue, warnings, }) {
254
- if (scripts[scriptName] === undefined) {
255
- scripts[scriptName] = scriptValue;
256
- return;
257
- }
258
- if (scripts[scriptName] !== scriptValue) {
259
- warnings.push(`Preserved existing package script "${scriptName}"; add "${scriptValue}" manually if you want the generated integration command.`);
260
- }
261
- }
262
- async function mutatePackageJson(projectDir, mutate) {
263
- const packageJsonPath = path.join(projectDir, "package.json");
264
- const packageJson = await readJsonFile(packageJsonPath, {
265
- context: "integration env package manifest",
266
- });
267
- mutate(packageJson);
268
- await fsp.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, "\t")}\n`, "utf8");
269
- }
270
- function addIntegrationEnvPackageJsonEntries({ integrationEnvSlug, packageManager, packageJson, withReleaseZip, service, warnings, withWpEnv, }) {
271
- const devDependencies = {
272
- ...(packageJson.devDependencies ?? {}),
273
- };
274
- if (withWpEnv && devDependencies["@wordpress/env"] === undefined) {
275
- devDependencies["@wordpress/env"] = WP_ENV_PACKAGE_VERSION;
276
- }
277
- packageJson.devDependencies = devDependencies;
278
- const scripts = {
279
- ...(packageJson.scripts ?? {}),
280
- };
281
- addScriptIfMissing({
282
- scriptName: `smoke:${integrationEnvSlug}`,
283
- scriptValue: `node scripts/integration-smoke/${integrationEnvSlug}.mjs`,
284
- scripts,
285
- warnings,
286
- });
287
- addScriptIfMissing({
288
- scriptName: "smoke:integration",
289
- scriptValue: formatRunScript(packageManager, `smoke:${integrationEnvSlug}`),
290
- scripts,
291
- warnings,
292
- });
293
- if (withWpEnv) {
294
- addScriptIfMissing({
295
- scriptName: "wp-env:start",
296
- scriptValue: "wp-env start",
297
- scripts,
298
- warnings,
299
- });
300
- addScriptIfMissing({
301
- scriptName: "wp-env:stop",
302
- scriptValue: "wp-env stop",
303
- scripts,
304
- warnings,
305
- });
306
- addScriptIfMissing({
307
- scriptName: "wp-env:reset",
308
- scriptValue: "wp-env destroy all && wp-env start",
309
- scripts,
310
- warnings,
311
- });
312
- }
313
- if (withReleaseZip) {
314
- addScriptIfMissing({
315
- scriptName: "release:zip",
316
- scriptValue: `${formatRunScript(packageManager, "sync-rest:package")} && ${formatRunScript(packageManager, "build")} && wp-scripts plugin-zip`,
317
- scripts,
318
- warnings,
319
- });
320
- addScriptIfMissing({
321
- scriptName: "release:zip:check",
322
- scriptValue: `${formatRunScript(packageManager, "sync-rest:package:check")} && ${formatRunScript(packageManager, "build")}`,
323
- scripts,
324
- warnings,
325
- });
326
- addScriptIfMissing({
327
- scriptName: "qa:check",
328
- scriptValue: `${formatRunScript(packageManager, "wp-typia:doctor:workspace")} && ${formatRunScript(packageManager, "release:zip:check")}`,
329
- scripts,
330
- warnings,
331
- });
332
- }
333
- if (service === "docker-compose") {
334
- addScriptIfMissing({
335
- scriptName: "service:start",
336
- scriptValue: "docker compose -f docker-compose.integration.yml up -d",
337
- scripts,
338
- warnings,
339
- });
340
- addScriptIfMissing({
341
- scriptName: "service:stop",
342
- scriptValue: "docker compose -f docker-compose.integration.yml down",
343
- scripts,
344
- warnings,
345
- });
346
- }
347
- packageJson.scripts = scripts;
348
- }
349
9
  /**
350
10
  * Add an opt-in local WordPress integration environment starter to an official
351
11
  * workspace.
@@ -406,7 +66,7 @@ export async function runAddIntegrationEnvCommand({ cwd = process.cwd(), integra
406
66
  warnings,
407
67
  });
408
68
  }
409
- await mutatePackageJson(workspace.projectDir, (packageJson) => addIntegrationEnvPackageJsonEntries({
69
+ await mutateIntegrationEnvPackageJson(workspace.projectDir, (packageJson) => addIntegrationEnvPackageJsonEntries({
410
70
  integrationEnvSlug,
411
71
  packageManager: workspace.packageManager,
412
72
  packageJson,
@@ -0,0 +1,10 @@
1
+ import type { WorkspaceProject } from "./workspace-project.js";
2
+ /**
3
+ * Ensure workspace bootstrap PHP registers pattern categories and loads
4
+ * generated pattern modules from both flat and nested pattern directories.
5
+ *
6
+ * @param workspace Resolved official workspace project metadata.
7
+ * @returns A promise that resolves after the workspace bootstrap is patched.
8
+ * @throws {Error} When existing bootstrap source cannot be safely patched.
9
+ */
10
+ export declare function ensurePatternBootstrapAnchors(workspace: WorkspaceProject): Promise<void>;
@@ -0,0 +1,95 @@
1
+ import path from "node:path";
2
+ import { getWorkspaceBootstrapPath, patchFile } from "./cli-add-shared.js";
3
+ import { appendPhpSnippetBeforeClosingTag, insertPhpSnippetBeforeWorkspaceAnchors, } from "./cli-add-workspace-mutation.js";
4
+ import { hasPhpFunctionDefinition } from "./php-utils.js";
5
+ import { toTitleCase } from "./string-case.js";
6
+ const FLAT_PATTERN_GLOB = "glob( __DIR__ . '/src/patterns/*.php' ) ?: array()";
7
+ const NESTED_PATTERN_GLOB = "glob( __DIR__ . '/src/patterns/*/*.php' ) ?: array()";
8
+ const LEGACY_FLAT_PATTERN_MODULES_ASSIGNMENT_PATTERN = /^[ \t]*\$pattern_modules\s*=\s*glob\( __DIR__ \. '\/src\/patterns\/\*\.php' \) \?: array\(\);\s*$/mu;
9
+ const LEGACY_FLAT_PATTERN_FOREACH_PATTERN = /^[ \t]*foreach\s*\(\s*glob\( __DIR__ \. '\/src\/patterns\/\*\.php' \) \?: array\(\)\s+as\s+\$pattern_module\s*\)\s*\{\r?\n[ \t]*require\s+\$pattern_module;\r?\n[ \t]*\}/mu;
10
+ function buildNestedPatternModulesAssignment() {
11
+ return [
12
+ "\t$pattern_modules = array_merge(",
13
+ `\t\t${FLAT_PATTERN_GLOB},`,
14
+ `\t\t${NESTED_PATTERN_GLOB}`,
15
+ "\t);",
16
+ ].join("\n");
17
+ }
18
+ function ensureNestedPatternLoaderSource(source, bootstrapPath) {
19
+ if (source.includes(NESTED_PATTERN_GLOB)) {
20
+ return source;
21
+ }
22
+ if (LEGACY_FLAT_PATTERN_FOREACH_PATTERN.test(source)) {
23
+ return source.replace(LEGACY_FLAT_PATTERN_FOREACH_PATTERN, [
24
+ buildNestedPatternModulesAssignment(),
25
+ "\tforeach ( $pattern_modules as $pattern_module ) {",
26
+ "\t\trequire $pattern_module;",
27
+ "\t}",
28
+ ].join("\n"));
29
+ }
30
+ if (LEGACY_FLAT_PATTERN_MODULES_ASSIGNMENT_PATTERN.test(source)) {
31
+ return source.replace(LEGACY_FLAT_PATTERN_MODULES_ASSIGNMENT_PATTERN, buildNestedPatternModulesAssignment());
32
+ }
33
+ if (source.includes("array_merge(") && source.includes(FLAT_PATTERN_GLOB)) {
34
+ return source.replace(FLAT_PATTERN_GLOB, `${FLAT_PATTERN_GLOB},\n\t\t${NESTED_PATTERN_GLOB}`);
35
+ }
36
+ throw new Error(`Unable to repair ${path.basename(bootstrapPath)} pattern loader for nested src/patterns directories.`);
37
+ }
38
+ /**
39
+ * Ensure workspace bootstrap PHP registers pattern categories and loads
40
+ * generated pattern modules from both flat and nested pattern directories.
41
+ *
42
+ * @param workspace Resolved official workspace project metadata.
43
+ * @returns A promise that resolves after the workspace bootstrap is patched.
44
+ * @throws {Error} When existing bootstrap source cannot be safely patched.
45
+ */
46
+ export async function ensurePatternBootstrapAnchors(workspace) {
47
+ const workspaceBaseName = workspace.packageName.split("/").pop() ?? workspace.packageName;
48
+ const bootstrapPath = getWorkspaceBootstrapPath(workspace);
49
+ await patchFile(bootstrapPath, (source) => {
50
+ let nextSource = source;
51
+ const patternCategoryFunctionName = `${workspace.workspace.phpPrefix}_register_pattern_category`;
52
+ const patternRegistrationFunctionName = `${workspace.workspace.phpPrefix}_register_patterns`;
53
+ const patternCategoryHook = `add_action( 'init', '${patternCategoryFunctionName}' );`;
54
+ const patternRegistrationHook = `add_action( 'init', '${patternRegistrationFunctionName}', 20 );`;
55
+ const patternFunctions = `
56
+
57
+ function ${patternCategoryFunctionName}() {
58
+ if ( function_exists( 'register_block_pattern_category' ) ) {
59
+ register_block_pattern_category(
60
+ '${workspace.workspace.namespace}',
61
+ array(
62
+ 'label' => __( ${JSON.stringify(`${toTitleCase(workspaceBaseName)} Patterns`)}, '${workspace.workspace.textDomain}' ),
63
+ )
64
+ );
65
+ }
66
+ }
67
+
68
+ function ${patternRegistrationFunctionName}() {
69
+ $pattern_modules = array_merge(
70
+ ${FLAT_PATTERN_GLOB},
71
+ ${NESTED_PATTERN_GLOB}
72
+ );
73
+ foreach ( $pattern_modules as $pattern_module ) {
74
+ require $pattern_module;
75
+ }
76
+ }
77
+ `;
78
+ if (!hasPhpFunctionDefinition(nextSource, patternCategoryFunctionName) &&
79
+ !hasPhpFunctionDefinition(nextSource, patternRegistrationFunctionName)) {
80
+ nextSource = insertPhpSnippetBeforeWorkspaceAnchors(nextSource, patternFunctions);
81
+ }
82
+ if (!hasPhpFunctionDefinition(nextSource, patternCategoryFunctionName) ||
83
+ !hasPhpFunctionDefinition(nextSource, patternRegistrationFunctionName)) {
84
+ throw new Error(`Unable to inject pattern bootstrap functions into ${path.basename(bootstrapPath)}.`);
85
+ }
86
+ nextSource = ensureNestedPatternLoaderSource(nextSource, bootstrapPath);
87
+ if (!nextSource.includes(patternCategoryHook)) {
88
+ nextSource = appendPhpSnippetBeforeClosingTag(nextSource, patternCategoryHook);
89
+ }
90
+ if (!nextSource.includes(patternRegistrationHook)) {
91
+ nextSource = appendPhpSnippetBeforeClosingTag(nextSource, patternRegistrationHook);
92
+ }
93
+ return nextSource;
94
+ });
95
+ }
@@ -0,0 +1,20 @@
1
+ import { type RunAddPatternCommandOptions } from "./cli-add-shared.js";
2
+ import { type PatternCatalogScope } from "./pattern-catalog.js";
3
+ export type ResolvedPatternCatalogOptions = {
4
+ contentFile: string;
5
+ patternScope: PatternCatalogScope;
6
+ sectionRole?: string;
7
+ tags: string[];
8
+ thumbnailUrl?: string;
9
+ title: string;
10
+ };
11
+ /**
12
+ * Resolve and validate catalog metadata for a generated workspace pattern.
13
+ *
14
+ * @param patternSlug Normalized and validated pattern slug.
15
+ * @param options Raw add-pattern command options from the CLI layer.
16
+ * @returns Resolved catalog metadata and safe relative content path.
17
+ * @throws {Error} When scope, section-role coupling, tags, thumbnail URL, or
18
+ * content file paths are invalid.
19
+ */
20
+ export declare function resolvePatternCatalogOptions(patternSlug: string, options: RunAddPatternCommandOptions): ResolvedPatternCatalogOptions;
@@ -0,0 +1,113 @@
1
+ import path from "node:path";
2
+ import { assertValidGeneratedSlug, normalizeBlockSlug, } from "./cli-add-shared.js";
3
+ import { isValidPatternThumbnailUrl, PATTERN_CATALOG_SCOPE_IDS, } from "./pattern-catalog.js";
4
+ import { toTitleCase } from "./string-case.js";
5
+ const PATTERN_CONTENT_FILE_ROOT = "src/patterns/";
6
+ const PATTERN_TAG_PATTERN = /^[a-z0-9][a-z0-9-]*$/u;
7
+ function assertValidPatternRelativePath(label, value, usage) {
8
+ const normalizedPath = value.trim().replace(/\\/gu, "/");
9
+ if (normalizedPath.length === 0 ||
10
+ path.isAbsolute(normalizedPath) ||
11
+ normalizedPath.split("/").includes("..") ||
12
+ /[<>:"|?*\u0000-\u001F]/u.test(normalizedPath)) {
13
+ throw new Error(`${label} must be a safe relative project path. Use \`${usage}\`.`);
14
+ }
15
+ return normalizedPath;
16
+ }
17
+ function assertValidPatternContentFile(value, usage) {
18
+ const normalizedPath = assertValidPatternRelativePath("Pattern content file", value, usage);
19
+ if (!isLoadablePatternContentFilePath(normalizedPath)) {
20
+ throw new Error("Pattern content file must live directly under `src/patterns/` or one nested directory under `src/patterns/` and end in `.php` so the generated PHP loader can require it.");
21
+ }
22
+ return normalizedPath;
23
+ }
24
+ function isLoadablePatternContentFilePath(normalizedPath) {
25
+ if (!normalizedPath.startsWith(PATTERN_CONTENT_FILE_ROOT) ||
26
+ !normalizedPath.endsWith(".php")) {
27
+ return false;
28
+ }
29
+ const patternRelativePath = normalizedPath.slice(PATTERN_CONTENT_FILE_ROOT.length);
30
+ const segments = patternRelativePath.split("/");
31
+ return ((segments.length === 1 || segments.length === 2) &&
32
+ segments.every((segment) => segment.length > 0));
33
+ }
34
+ function resolvePatternScope(scope) {
35
+ if (scope === undefined || scope.trim() === "") {
36
+ return "full";
37
+ }
38
+ const normalizedScope = scope.trim();
39
+ if (PATTERN_CATALOG_SCOPE_IDS.includes(normalizedScope)) {
40
+ return normalizedScope;
41
+ }
42
+ throw new Error(`Pattern scope must be one of: ${PATTERN_CATALOG_SCOPE_IDS.join(", ")}.`);
43
+ }
44
+ function normalizeOptionalSlug(label, value, usage) {
45
+ if (value === undefined || value.trim() === "") {
46
+ return undefined;
47
+ }
48
+ return assertValidGeneratedSlug(label, normalizeBlockSlug(value), usage);
49
+ }
50
+ function normalizePatternTags(tags) {
51
+ const rawTags = typeof tags === "string"
52
+ ? tags.split(",")
53
+ : Array.isArray(tags)
54
+ ? [...tags]
55
+ : [];
56
+ const normalizedTags = rawTags
57
+ .map((tag) => normalizeBlockSlug(tag))
58
+ .filter((tag) => tag.length > 0);
59
+ for (const tag of normalizedTags) {
60
+ if (!PATTERN_TAG_PATTERN.test(tag)) {
61
+ throw new Error(`Pattern tag "${tag}" must contain only lowercase letters, numbers, and hyphens.`);
62
+ }
63
+ }
64
+ return [...new Set(normalizedTags)].sort();
65
+ }
66
+ function normalizePatternThumbnailUrl(value) {
67
+ if (value === undefined || value.trim() === "") {
68
+ return undefined;
69
+ }
70
+ const thumbnailUrl = value.trim();
71
+ if (!isValidPatternThumbnailUrl(thumbnailUrl)) {
72
+ throw new Error("Pattern thumbnail URL must be an http(s) URL or safe relative project path.");
73
+ }
74
+ return thumbnailUrl;
75
+ }
76
+ function resolvePatternContentFile(patternSlug, patternScope, contentFile) {
77
+ if (contentFile && contentFile.trim() !== "") {
78
+ return assertValidPatternContentFile(contentFile, "wp-typia add pattern <name> [--scope <full|section>]");
79
+ }
80
+ const scopeDirectory = patternScope === "section" ? "sections" : "full";
81
+ return path.posix.join("src", "patterns", scopeDirectory, `${patternSlug}.php`);
82
+ }
83
+ /**
84
+ * Resolve and validate catalog metadata for a generated workspace pattern.
85
+ *
86
+ * @param patternSlug Normalized and validated pattern slug.
87
+ * @param options Raw add-pattern command options from the CLI layer.
88
+ * @returns Resolved catalog metadata and safe relative content path.
89
+ * @throws {Error} When scope, section-role coupling, tags, thumbnail URL, or
90
+ * content file paths are invalid.
91
+ */
92
+ export function resolvePatternCatalogOptions(patternSlug, options) {
93
+ const patternScope = resolvePatternScope(options.patternScope);
94
+ const sectionRole = normalizeOptionalSlug("Pattern section role", options.sectionRole, "wp-typia add pattern <name> --scope section --section-role <role>");
95
+ if (patternScope === "section" && !sectionRole) {
96
+ throw new Error("`wp-typia add pattern --scope section` requires --section-role <role>.");
97
+ }
98
+ if (patternScope !== "section" && sectionRole) {
99
+ throw new Error("`--section-role` requires `--scope section`.");
100
+ }
101
+ const title = options.catalogTitle && options.catalogTitle.trim() !== ""
102
+ ? options.catalogTitle.trim()
103
+ : toTitleCase(patternSlug);
104
+ const thumbnailUrl = normalizePatternThumbnailUrl(options.thumbnailUrl);
105
+ return {
106
+ contentFile: resolvePatternContentFile(patternSlug, patternScope, options.contentFile),
107
+ patternScope,
108
+ ...(sectionRole ? { sectionRole } : {}),
109
+ tags: normalizePatternTags(options.tags),
110
+ ...(thumbnailUrl ? { thumbnailUrl } : {}),
111
+ title,
112
+ };
113
+ }
@@ -0,0 +1,20 @@
1
+ import type { ResolvedPatternCatalogOptions } from "./cli-add-workspace-pattern-options.js";
2
+ /**
3
+ * Render the block-config inventory entry for a generated pattern.
4
+ *
5
+ * @param patternSlug Normalized and validated pattern slug.
6
+ * @param options Resolved pattern catalog metadata.
7
+ * @returns A TypeScript object literal snippet for the workspace inventory.
8
+ */
9
+ export declare function buildPatternConfigEntry(patternSlug: string, options: ResolvedPatternCatalogOptions): string;
10
+ /**
11
+ * Render the PHP pattern module registered by the workspace add command.
12
+ *
13
+ * @param patternSlug Normalized and validated pattern slug.
14
+ * @param namespace WordPress block namespace for the workspace.
15
+ * @param sectionRole Optional section role for section-scoped patterns.
16
+ * @param textDomain Translation text domain for generated labels.
17
+ * @param title Human-readable pattern title.
18
+ * @returns PHP source for a single generated block pattern module.
19
+ */
20
+ export declare function buildPatternSource(patternSlug: string, namespace: string, sectionRole: string | undefined, textDomain: string, title: string): string;
@@ -0,0 +1,57 @@
1
+ import { quoteTsString } from "./cli-add-shared.js";
2
+ /**
3
+ * Render the block-config inventory entry for a generated pattern.
4
+ *
5
+ * @param patternSlug Normalized and validated pattern slug.
6
+ * @param options Resolved pattern catalog metadata.
7
+ * @returns A TypeScript object literal snippet for the workspace inventory.
8
+ */
9
+ export function buildPatternConfigEntry(patternSlug, options) {
10
+ const lines = [
11
+ "\t{",
12
+ `\t\tcontentFile: ${quoteTsString(options.contentFile)},`,
13
+ `\t\tfile: ${quoteTsString(options.contentFile)},`,
14
+ `\t\tscope: ${quoteTsString(options.patternScope)},`,
15
+ ...(options.sectionRole
16
+ ? [`\t\tsectionRole: ${quoteTsString(options.sectionRole)},`]
17
+ : []),
18
+ `\t\tslug: ${quoteTsString(patternSlug)},`,
19
+ `\t\ttags: [${options.tags.map((tag) => quoteTsString(tag)).join(", ")}],`,
20
+ ...(options.thumbnailUrl
21
+ ? [`\t\tthumbnailUrl: ${quoteTsString(options.thumbnailUrl)},`]
22
+ : []),
23
+ `\t\ttitle: ${quoteTsString(options.title)},`,
24
+ "\t},",
25
+ ];
26
+ return lines.join("\n");
27
+ }
28
+ /**
29
+ * Render the PHP pattern module registered by the workspace add command.
30
+ *
31
+ * @param patternSlug Normalized and validated pattern slug.
32
+ * @param namespace WordPress block namespace for the workspace.
33
+ * @param sectionRole Optional section role for section-scoped patterns.
34
+ * @param textDomain Translation text domain for generated labels.
35
+ * @param title Human-readable pattern title.
36
+ * @returns PHP source for a single generated block pattern module.
37
+ */
38
+ export function buildPatternSource(patternSlug, namespace, sectionRole, textDomain, title) {
39
+ const content = sectionRole
40
+ ? `'<!-- wp:group {"className":"section section--${sectionRole}"} --><div class="wp-block-group section section--${sectionRole}"><!-- wp:paragraph --><p>' . esc_html__( 'Describe this section pattern here.', '${textDomain}' ) . '</p><!-- /wp:paragraph --></div><!-- /wp:group -->'`
41
+ : `'<!-- wp:paragraph --><p>' . esc_html__( 'Describe this pattern here.', '${textDomain}' ) . '</p><!-- /wp:paragraph -->'`;
42
+ return `<?php
43
+ if ( ! defined( 'ABSPATH' ) ) {
44
+ return;
45
+ }
46
+
47
+ register_block_pattern(
48
+ '${namespace}/${patternSlug}',
49
+ array(
50
+ 'title' => __( ${JSON.stringify(title)}, '${textDomain}' ),
51
+ 'description' => __( ${JSON.stringify(`A starter pattern for ${title}.`)}, '${textDomain}' ),
52
+ 'categories' => array( '${namespace}' ),
53
+ 'content' => ${content},
54
+ )
55
+ );
56
+ `;
57
+ }