@wp-typia/project-tools 0.22.3 → 0.22.5

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 (43) hide show
  1. package/dist/runtime/cli-add-block-json.d.ts +31 -0
  2. package/dist/runtime/cli-add-block-json.js +65 -0
  3. package/dist/runtime/cli-add-collision.d.ts +129 -0
  4. package/dist/runtime/cli-add-collision.js +293 -0
  5. package/dist/runtime/cli-add-filesystem.d.ts +29 -0
  6. package/dist/runtime/cli-add-filesystem.js +77 -0
  7. package/dist/runtime/cli-add-help.d.ts +4 -0
  8. package/dist/runtime/cli-add-help.js +41 -0
  9. package/dist/runtime/cli-add-shared.d.ts +6 -304
  10. package/dist/runtime/cli-add-shared.js +6 -524
  11. package/dist/runtime/cli-add-types.d.ts +247 -0
  12. package/dist/runtime/cli-add-types.js +64 -0
  13. package/dist/runtime/cli-add-validation.d.ts +87 -0
  14. package/dist/runtime/cli-add-validation.js +147 -0
  15. package/dist/runtime/cli-add-workspace-ability-scaffold.js +46 -72
  16. package/dist/runtime/cli-add-workspace-admin-view-scaffold.js +35 -61
  17. package/dist/runtime/cli-add-workspace-ai-anchors.js +3 -24
  18. package/dist/runtime/cli-add-workspace-ai-scaffold.js +53 -57
  19. package/dist/runtime/cli-add-workspace-ai-templates.js +2 -0
  20. package/dist/runtime/cli-add-workspace-assets.js +7 -50
  21. package/dist/runtime/cli-add-workspace-mutation.d.ts +30 -0
  22. package/dist/runtime/cli-add-workspace-mutation.js +60 -0
  23. package/dist/runtime/cli-add-workspace-rest-anchors.js +3 -24
  24. package/dist/runtime/cli-add-workspace.js +1 -79
  25. package/dist/runtime/cli-add.d.ts +2 -2
  26. package/dist/runtime/cli-add.js +2 -2
  27. package/dist/runtime/cli-doctor-workspace-blocks.js +1 -66
  28. package/dist/runtime/index.d.ts +2 -0
  29. package/dist/runtime/index.js +1 -0
  30. package/dist/runtime/migration-utils.d.ts +2 -1
  31. package/dist/runtime/migration-utils.js +3 -11
  32. package/dist/runtime/package-managers.d.ts +19 -0
  33. package/dist/runtime/package-managers.js +62 -0
  34. package/dist/runtime/template-source-cache.d.ts +59 -0
  35. package/dist/runtime/template-source-cache.js +160 -0
  36. package/dist/runtime/ts-source-masking.d.ts +28 -0
  37. package/dist/runtime/ts-source-masking.js +104 -0
  38. package/dist/runtime/typia-llm.d.ts +9 -1
  39. package/dist/runtime/typia-llm.js +20 -5
  40. package/dist/runtime/workspace-inventory.js +368 -284
  41. package/dist/runtime/workspace-project.d.ts +1 -1
  42. package/dist/runtime/workspace-project.js +2 -10
  43. package/package.json +2 -2
@@ -6,7 +6,8 @@ import semver from "semver";
6
6
  import { appendWorkspaceInventoryEntries, readWorkspaceInventory, } from "./workspace-inventory.js";
7
7
  import { buildAbilityClientSource, buildAbilityConfigEntry, buildAbilityConfigSource, buildAbilityDataSource, buildAbilityPhpSource, buildAbilityRegistrySource, buildAbilitySyncScriptSource, buildAbilityTypesSource, } from "./cli-add-workspace-ability-templates.js";
8
8
  import { ABILITY_EDITOR_ASSET, ABILITY_EDITOR_SCRIPT, ABILITY_REGISTRY_END_MARKER, ABILITY_REGISTRY_START_MARKER, ABILITY_SERVER_GLOB, WP_ABILITIES_SCRIPT_MODULE_ID, WP_CORE_ABILITIES_SCRIPT_MODULE_ID, } from "./cli-add-workspace-ability-types.js";
9
- import { getWorkspaceBootstrapPath, patchFile, rollbackWorkspaceMutation, snapshotWorkspaceFiles, } from "./cli-add-shared.js";
9
+ import { getWorkspaceBootstrapPath, patchFile, } from "./cli-add-shared.js";
10
+ import { appendPhpSnippetBeforeClosingTag, executeWorkspaceMutationPlan, insertPhpSnippetBeforeWorkspaceAnchors, } from "./cli-add-workspace-mutation.js";
10
11
  import { updatePluginHeaderCompatibility, } from "./scaffold-compatibility.js";
11
12
  import { DEFAULT_WORDPRESS_ABILITIES_VERSION, DEFAULT_WORDPRESS_CORE_ABILITIES_VERSION, } from "./package-versions.js";
12
13
  import { escapeRegex, findPhpFunctionRange, hasPhpFunctionDefinition, replacePhpFunctionDefinition, } from "./php-utils.js";
@@ -123,46 +124,24 @@ function ${enqueueFunctionName}() {
123
124
  \t);
124
125
  }
125
126
  `;
126
- const insertionAnchors = [
127
- /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
128
- /\?>\s*$/u,
129
- ];
130
- const insertPhpSnippet = (snippet) => {
131
- for (const anchor of insertionAnchors) {
132
- const candidate = nextSource.replace(anchor, (match) => `${snippet}\n${match}`);
133
- if (candidate !== nextSource) {
134
- nextSource = candidate;
135
- return;
136
- }
137
- }
138
- nextSource = `${nextSource.trimEnd()}\n${snippet}\n`;
139
- };
140
- const appendPhpSnippet = (snippet) => {
141
- const closingTagPattern = /\?>\s*$/u;
142
- if (closingTagPattern.test(nextSource)) {
143
- nextSource = nextSource.replace(closingTagPattern, `${snippet}\n?>`);
144
- return;
145
- }
146
- nextSource = `${nextSource.trimEnd()}\n${snippet}\n`;
147
- };
148
127
  if (!hasPhpFunctionDefinition(nextSource, loadFunctionName)) {
149
- insertPhpSnippet(loadFunction);
128
+ nextSource = insertPhpSnippetBeforeWorkspaceAnchors(nextSource, loadFunction);
150
129
  }
151
130
  if (!hasPhpFunctionDefinition(nextSource, enqueueFunctionName)) {
152
- insertPhpSnippet(enqueueFunction);
131
+ nextSource = insertPhpSnippetBeforeWorkspaceAnchors(nextSource, enqueueFunction);
153
132
  }
154
133
  else if (!findPhpFunctionRange(nextSource, enqueueFunctionName)?.source.includes("wp_enqueue_script_module")) {
155
134
  nextSource =
156
135
  replacePhpFunctionDefinition(nextSource, enqueueFunctionName, enqueueFunction, { trimReplacementStart: true }) ?? nextSource;
157
136
  }
158
137
  if (!nextSource.includes(loadHook)) {
159
- appendPhpSnippet(loadHook);
138
+ nextSource = appendPhpSnippetBeforeClosingTag(nextSource, loadHook);
160
139
  }
161
140
  if (!nextSource.includes(adminEnqueueHook)) {
162
- appendPhpSnippet(adminEnqueueHook);
141
+ nextSource = appendPhpSnippetBeforeClosingTag(nextSource, adminEnqueueHook);
163
142
  }
164
143
  if (!nextSource.includes(editorEnqueueHook)) {
165
- appendPhpSnippet(editorEnqueueHook);
144
+ nextSource = appendPhpSnippetBeforeClosingTag(nextSource, editorEnqueueHook);
166
145
  }
167
146
  return nextSource;
168
147
  });
@@ -336,8 +315,8 @@ export async function scaffoldAbilityWorkspace({ abilitySlug, compatibilityPolic
336
315
  const dataFilePath = path.join(abilityDir, "data.ts");
337
316
  const clientFilePath = path.join(abilityDir, "client.ts");
338
317
  const phpFilePath = path.join(workspace.projectDir, "inc", "abilities", `${abilitySlug}.php`);
339
- const mutationSnapshot = {
340
- fileSources: await snapshotWorkspaceFiles([
318
+ await executeWorkspaceMutationPlan({
319
+ filePaths: [
341
320
  blockConfigPath,
342
321
  bootstrapPath,
343
322
  buildScriptPath,
@@ -346,47 +325,42 @@ export async function scaffoldAbilityWorkspace({ abilitySlug, compatibilityPolic
346
325
  syncProjectScriptPath,
347
326
  webpackConfigPath,
348
327
  abilitiesIndexPath,
349
- ]),
350
- snapshotDirs: [],
328
+ ],
351
329
  targetPaths: [abilityDir, phpFilePath, syncAbilitiesScriptPath],
352
- };
353
- try {
354
- await fsp.mkdir(abilityDir, { recursive: true });
355
- await fsp.mkdir(path.dirname(phpFilePath), { recursive: true });
356
- await ensureAbilityBootstrapAnchors(workspace);
357
- await patchFile(bootstrapPath, (source) => updatePluginHeaderCompatibility(source, compatibilityPolicy));
358
- await ensureAbilityPackageScripts(workspace);
359
- await ensureAbilitySyncProjectAnchors(workspace);
360
- await ensureAbilityBuildScriptAnchors(workspace);
361
- await ensureAbilityWebpackAnchors(workspace);
362
- await fsp.writeFile(syncAbilitiesScriptPath, buildAbilitySyncScriptSource(), "utf8");
363
- await fsp.writeFile(configFilePath, buildAbilityConfigSource(abilitySlug, workspace.workspace.namespace), "utf8");
364
- await fsp.writeFile(typesFilePath, buildAbilityTypesSource(abilitySlug), "utf8");
365
- await fsp.writeFile(dataFilePath, buildAbilityDataSource(abilitySlug), "utf8");
366
- await fsp.writeFile(clientFilePath, buildAbilityClientSource(abilitySlug), "utf8");
367
- await fsp.writeFile(phpFilePath, buildAbilityPhpSource(abilitySlug, workspace), "utf8");
368
- const pascalCase = toPascalCase(abilitySlug);
369
- await syncTypeSchemas({
370
- jsonSchemaFile: `src/abilities/${abilitySlug}/input.schema.json`,
371
- projectRoot: workspace.projectDir,
372
- sourceTypeName: `${pascalCase}AbilityInput`,
373
- typesFile: `src/abilities/${abilitySlug}/types.ts`,
374
- });
375
- await syncTypeSchemas({
376
- jsonSchemaFile: `src/abilities/${abilitySlug}/output.schema.json`,
377
- projectRoot: workspace.projectDir,
378
- sourceTypeName: `${pascalCase}AbilityOutput`,
379
- typesFile: `src/abilities/${abilitySlug}/types.ts`,
380
- });
381
- await writeAbilityRegistry(workspace.projectDir, abilitySlug);
382
- await appendWorkspaceInventoryEntries(workspace.projectDir, {
383
- abilityEntries: [
384
- buildAbilityConfigEntry(abilitySlug, compatibilityPolicy),
385
- ],
386
- });
387
- }
388
- catch (error) {
389
- await rollbackWorkspaceMutation(mutationSnapshot);
390
- throw error;
391
- }
330
+ run: async () => {
331
+ await fsp.mkdir(abilityDir, { recursive: true });
332
+ await fsp.mkdir(path.dirname(phpFilePath), { recursive: true });
333
+ await ensureAbilityBootstrapAnchors(workspace);
334
+ await patchFile(bootstrapPath, (source) => updatePluginHeaderCompatibility(source, compatibilityPolicy));
335
+ await ensureAbilityPackageScripts(workspace);
336
+ await ensureAbilitySyncProjectAnchors(workspace);
337
+ await ensureAbilityBuildScriptAnchors(workspace);
338
+ await ensureAbilityWebpackAnchors(workspace);
339
+ await fsp.writeFile(syncAbilitiesScriptPath, buildAbilitySyncScriptSource(), "utf8");
340
+ await fsp.writeFile(configFilePath, buildAbilityConfigSource(abilitySlug, workspace.workspace.namespace), "utf8");
341
+ await fsp.writeFile(typesFilePath, buildAbilityTypesSource(abilitySlug), "utf8");
342
+ await fsp.writeFile(dataFilePath, buildAbilityDataSource(abilitySlug), "utf8");
343
+ await fsp.writeFile(clientFilePath, buildAbilityClientSource(abilitySlug), "utf8");
344
+ await fsp.writeFile(phpFilePath, buildAbilityPhpSource(abilitySlug, workspace), "utf8");
345
+ const pascalCase = toPascalCase(abilitySlug);
346
+ await syncTypeSchemas({
347
+ jsonSchemaFile: `src/abilities/${abilitySlug}/input.schema.json`,
348
+ projectRoot: workspace.projectDir,
349
+ sourceTypeName: `${pascalCase}AbilityInput`,
350
+ typesFile: `src/abilities/${abilitySlug}/types.ts`,
351
+ });
352
+ await syncTypeSchemas({
353
+ jsonSchemaFile: `src/abilities/${abilitySlug}/output.schema.json`,
354
+ projectRoot: workspace.projectDir,
355
+ sourceTypeName: `${pascalCase}AbilityOutput`,
356
+ typesFile: `src/abilities/${abilitySlug}/types.ts`,
357
+ });
358
+ await writeAbilityRegistry(workspace.projectDir, abilitySlug);
359
+ await appendWorkspaceInventoryEntries(workspace.projectDir, {
360
+ abilityEntries: [
361
+ buildAbilityConfigEntry(abilitySlug, compatibilityPolicy),
362
+ ],
363
+ });
364
+ },
365
+ });
392
366
  }
@@ -4,7 +4,8 @@ import path from 'node:path';
4
4
  import { appendWorkspaceInventoryEntries, readWorkspaceInventory, } from './workspace-inventory.js';
5
5
  import { buildAdminViewConfigEntry, buildAdminViewConfigSource, buildAdminViewEntrySource, buildAdminViewPhpSource, buildAdminViewRegistrySource, buildAdminViewScreenSource, buildAdminViewStyleSource, buildAdminViewTypesSource, buildCoreDataAdminViewDataSource, buildCoreDataAdminViewScreenSource, buildDefaultAdminViewDataSource, buildRestAdminViewDataSource, } from './cli-add-workspace-admin-view-templates.js';
6
6
  import { ADMIN_VIEWS_PHP_GLOB, isAdminViewCoreDataSource, } from './cli-add-workspace-admin-view-types.js';
7
- import { getWorkspaceBootstrapPath, patchFile, rollbackWorkspaceMutation, snapshotWorkspaceFiles, } from './cli-add-shared.js';
7
+ import { getWorkspaceBootstrapPath, patchFile, } from './cli-add-shared.js';
8
+ import { appendPhpSnippetBeforeClosingTag, executeWorkspaceMutationPlan, insertPhpSnippetBeforeWorkspaceAnchors, } from './cli-add-workspace-mutation.js';
8
9
  import { DEFAULT_WORDPRESS_CORE_DATA_VERSION, DEFAULT_WORDPRESS_DATA_VERSION, DEFAULT_WORDPRESS_DATAVIEWS_VERSION, DEFAULT_WP_TYPIA_DATAVIEWS_VERSION, resolveManagedPackageVersionRange, } from './package-versions.js';
9
10
  import { findPhpFunctionRange, hasPhpFunctionDefinition, replacePhpFunctionDefinition, } from './php-utils.js';
10
11
  function detectJsonIndent(source) {
@@ -77,30 +78,8 @@ function ${loadFunctionName}() {
77
78
  \t}
78
79
  }
79
80
  `;
80
- const insertionAnchors = [
81
- /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
82
- /\?>\s*$/u,
83
- ];
84
- const insertPhpSnippet = (snippet) => {
85
- for (const anchor of insertionAnchors) {
86
- const candidate = nextSource.replace(anchor, (match) => `${snippet}\n${match}`);
87
- if (candidate !== nextSource) {
88
- nextSource = candidate;
89
- return;
90
- }
91
- }
92
- nextSource = `${nextSource.trimEnd()}\n${snippet}\n`;
93
- };
94
- const appendPhpSnippet = (snippet) => {
95
- const closingTagPattern = /\?>\s*$/u;
96
- if (closingTagPattern.test(nextSource)) {
97
- nextSource = nextSource.replace(closingTagPattern, `${snippet}\n?>`);
98
- return;
99
- }
100
- nextSource = `${nextSource.trimEnd()}\n${snippet}\n`;
101
- };
102
81
  if (!hasPhpFunctionDefinition(nextSource, loadFunctionName)) {
103
- insertPhpSnippet(loadFunction);
82
+ nextSource = insertPhpSnippetBeforeWorkspaceAnchors(nextSource, loadFunction);
104
83
  }
105
84
  else {
106
85
  const functionRange = findPhpFunctionRange(nextSource, loadFunctionName);
@@ -116,7 +95,7 @@ function ${loadFunctionName}() {
116
95
  }
117
96
  }
118
97
  if (!loadHookPattern.test(nextSource)) {
119
- appendPhpSnippet(loadHook);
98
+ nextSource = appendPhpSnippetBeforeClosingTag(nextSource, loadHook);
120
99
  }
121
100
  return nextSource;
122
101
  });
@@ -211,47 +190,42 @@ export async function scaffoldAdminViewWorkspace(options) {
211
190
  const adminViewsIndexPath = resolveAdminViewRegistryPath(workspace.projectDir);
212
191
  const adminViewDir = path.join(workspace.projectDir, 'src', 'admin-views', adminViewSlug);
213
192
  const adminViewPhpPath = path.join(workspace.projectDir, 'inc', 'admin-views', `${adminViewSlug}.php`);
214
- const mutationSnapshot = {
215
- fileSources: await snapshotWorkspaceFiles([
193
+ await executeWorkspaceMutationPlan({
194
+ filePaths: [
216
195
  adminViewsIndexPath,
217
196
  blockConfigPath,
218
197
  bootstrapPath,
219
198
  buildScriptPath,
220
199
  packageJsonPath,
221
200
  webpackConfigPath,
222
- ]),
223
- snapshotDirs: [],
201
+ ],
224
202
  targetPaths: [adminViewDir, adminViewPhpPath],
225
- };
226
- try {
227
- await fsp.mkdir(adminViewDir, { recursive: true });
228
- await fsp.mkdir(path.dirname(adminViewPhpPath), { recursive: true });
229
- await ensureAdminViewPackageDependencies(workspace, parsedSource);
230
- await ensureAdminViewBootstrapAnchors(workspace);
231
- await ensureAdminViewBuildScriptAnchors(workspace);
232
- await ensureAdminViewWebpackAnchors(workspace);
233
- await fsp.writeFile(path.join(adminViewDir, 'types.ts'), buildAdminViewTypesSource(adminViewSlug, restResource, coreDataSource), 'utf8');
234
- await fsp.writeFile(path.join(adminViewDir, 'config.ts'), buildAdminViewConfigSource(adminViewSlug, workspace.workspace.textDomain, parsedSource, restResource), 'utf8');
235
- await fsp.writeFile(path.join(adminViewDir, 'data.ts'), coreDataSource
236
- ? buildCoreDataAdminViewDataSource(adminViewSlug, coreDataSource)
237
- : restResource
238
- ? buildRestAdminViewDataSource(adminViewSlug, restResource)
239
- : buildDefaultAdminViewDataSource(adminViewSlug), 'utf8');
240
- await fsp.writeFile(path.join(adminViewDir, 'Screen.tsx'), coreDataSource
241
- ? buildCoreDataAdminViewScreenSource(adminViewSlug, workspace.workspace.textDomain)
242
- : buildAdminViewScreenSource(adminViewSlug, workspace.workspace.textDomain), 'utf8');
243
- await fsp.writeFile(path.join(adminViewDir, 'index.tsx'), buildAdminViewEntrySource(adminViewSlug), 'utf8');
244
- await fsp.writeFile(path.join(adminViewDir, 'style.scss'), buildAdminViewStyleSource(), 'utf8');
245
- await fsp.writeFile(adminViewPhpPath, buildAdminViewPhpSource(adminViewSlug, workspace), 'utf8');
246
- await writeAdminViewRegistry(workspace.projectDir, adminViewSlug);
247
- await appendWorkspaceInventoryEntries(workspace.projectDir, {
248
- adminViewEntries: [
249
- buildAdminViewConfigEntry(adminViewSlug, parsedSource),
250
- ],
251
- });
252
- }
253
- catch (error) {
254
- await rollbackWorkspaceMutation(mutationSnapshot);
255
- throw error;
256
- }
203
+ run: async () => {
204
+ await fsp.mkdir(adminViewDir, { recursive: true });
205
+ await fsp.mkdir(path.dirname(adminViewPhpPath), { recursive: true });
206
+ await ensureAdminViewPackageDependencies(workspace, parsedSource);
207
+ await ensureAdminViewBootstrapAnchors(workspace);
208
+ await ensureAdminViewBuildScriptAnchors(workspace);
209
+ await ensureAdminViewWebpackAnchors(workspace);
210
+ await fsp.writeFile(path.join(adminViewDir, 'types.ts'), buildAdminViewTypesSource(adminViewSlug, restResource, coreDataSource), 'utf8');
211
+ await fsp.writeFile(path.join(adminViewDir, 'config.ts'), buildAdminViewConfigSource(adminViewSlug, workspace.workspace.textDomain, parsedSource, restResource), 'utf8');
212
+ await fsp.writeFile(path.join(adminViewDir, 'data.ts'), coreDataSource
213
+ ? buildCoreDataAdminViewDataSource(adminViewSlug, coreDataSource)
214
+ : restResource
215
+ ? buildRestAdminViewDataSource(adminViewSlug, restResource)
216
+ : buildDefaultAdminViewDataSource(adminViewSlug), 'utf8');
217
+ await fsp.writeFile(path.join(adminViewDir, 'Screen.tsx'), coreDataSource
218
+ ? buildCoreDataAdminViewScreenSource(adminViewSlug, workspace.workspace.textDomain)
219
+ : buildAdminViewScreenSource(adminViewSlug, workspace.workspace.textDomain), 'utf8');
220
+ await fsp.writeFile(path.join(adminViewDir, 'index.tsx'), buildAdminViewEntrySource(adminViewSlug), 'utf8');
221
+ await fsp.writeFile(path.join(adminViewDir, 'style.scss'), buildAdminViewStyleSource(), 'utf8');
222
+ await fsp.writeFile(adminViewPhpPath, buildAdminViewPhpSource(adminViewSlug, workspace), 'utf8');
223
+ await writeAdminViewRegistry(workspace.projectDir, adminViewSlug);
224
+ await appendWorkspaceInventoryEntries(workspace.projectDir, {
225
+ adminViewEntries: [
226
+ buildAdminViewConfigEntry(adminViewSlug, parsedSource),
227
+ ],
228
+ });
229
+ },
230
+ });
257
231
  }
@@ -2,6 +2,7 @@ import { promises as fsp } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { getPackageVersions } from "./package-versions.js";
4
4
  import { getWorkspaceBootstrapPath, patchFile, } from "./cli-add-shared.js";
5
+ import { appendPhpSnippetBeforeClosingTag, insertPhpSnippetBeforeWorkspaceAnchors, } from "./cli-add-workspace-mutation.js";
5
6
  import { hasPhpFunctionDefinition } from "./php-utils.js";
6
7
  const AI_FEATURE_SERVER_GLOB = "/inc/ai-features/*.php";
7
8
  /**
@@ -21,30 +22,8 @@ function ${registerFunctionName}() {
21
22
  \t}
22
23
  }
23
24
  `;
24
- const insertionAnchors = [
25
- /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
26
- /\?>\s*$/u,
27
- ];
28
- const insertPhpSnippet = (snippet) => {
29
- for (const anchor of insertionAnchors) {
30
- const candidate = nextSource.replace(anchor, (match) => `${snippet}\n${match}`);
31
- if (candidate !== nextSource) {
32
- nextSource = candidate;
33
- return;
34
- }
35
- }
36
- nextSource = `${nextSource.trimEnd()}\n${snippet}\n`;
37
- };
38
- const appendPhpSnippet = (snippet) => {
39
- const closingTagPattern = /\?>\s*$/u;
40
- if (closingTagPattern.test(nextSource)) {
41
- nextSource = nextSource.replace(closingTagPattern, `${snippet}\n?>`);
42
- return;
43
- }
44
- nextSource = `${nextSource.trimEnd()}\n${snippet}\n`;
45
- };
46
25
  if (!hasPhpFunctionDefinition(nextSource, registerFunctionName)) {
47
- insertPhpSnippet(registerFunction);
26
+ nextSource = insertPhpSnippetBeforeWorkspaceAnchors(nextSource, registerFunction);
48
27
  }
49
28
  else if (!nextSource.includes(AI_FEATURE_SERVER_GLOB)) {
50
29
  throw new Error([
@@ -54,7 +33,7 @@ function ${registerFunctionName}() {
54
33
  ].join(" "));
55
34
  }
56
35
  if (!nextSource.includes(registerHook)) {
57
- appendPhpSnippet(registerHook);
36
+ nextSource = appendPhpSnippetBeforeClosingTag(nextSource, registerHook);
58
37
  }
59
38
  return nextSource;
60
39
  });
@@ -5,7 +5,8 @@ import { buildAiFeatureConfigEntry, buildAiFeatureDataSource, buildAiFeatureSync
5
5
  import { ensureAiFeatureBootstrapAnchors, ensureAiFeaturePackageScripts, ensureAiFeatureSyncProjectAnchors, ensureAiFeatureSyncRestAnchors, } from "./cli-add-workspace-ai-anchors.js";
6
6
  import { buildAiFeaturePhpSource } from "./cli-add-workspace-ai-templates.js";
7
7
  import { appendWorkspaceInventoryEntries } from "./workspace-inventory.js";
8
- import { getWorkspaceBootstrapPath, patchFile, rollbackWorkspaceMutation, snapshotWorkspaceFiles, } from "./cli-add-shared.js";
8
+ import { getWorkspaceBootstrapPath, patchFile, } from "./cli-add-shared.js";
9
+ import { executeWorkspaceMutationPlan } from "./cli-add-workspace-mutation.js";
9
10
  import { updatePluginHeaderCompatibility } from "./scaffold-compatibility.js";
10
11
  import { toPascalCase, toTitleCase } from "./string-case.js";
11
12
  import { syncAiFeatureRestArtifacts, syncAiFeatureSchemaArtifact, } from "./ai-feature-artifacts.js";
@@ -25,67 +26,62 @@ export async function scaffoldAiFeatureWorkspace({ aiFeatureSlug, compatibilityP
25
26
  const apiFilePath = path.join(aiFeatureDir, "api.ts");
26
27
  const dataFilePath = path.join(aiFeatureDir, "data.ts");
27
28
  const phpFilePath = path.join(workspace.projectDir, "inc", "ai-features", `${aiFeatureSlug}.php`);
28
- const mutationSnapshot = {
29
- fileSources: await snapshotWorkspaceFiles([
29
+ return executeWorkspaceMutationPlan({
30
+ filePaths: [
30
31
  blockConfigPath,
31
32
  bootstrapPath,
32
33
  packageJsonPath,
33
34
  syncAiScriptPath,
34
35
  syncProjectScriptPath,
35
36
  syncRestScriptPath,
36
- ]),
37
- snapshotDirs: [],
37
+ ],
38
38
  targetPaths: [aiFeatureDir, phpFilePath, syncAiScriptPath],
39
- };
40
- try {
41
- await fsp.mkdir(aiFeatureDir, { recursive: true });
42
- await fsp.mkdir(path.dirname(phpFilePath), { recursive: true });
43
- await ensureAiFeatureBootstrapAnchors(workspace);
44
- await patchFile(bootstrapPath, (source) => updatePluginHeaderCompatibility(source, compatibilityPolicy));
45
- const packageScriptChanges = await ensureAiFeaturePackageScripts(workspace);
46
- await ensureAiFeatureSyncProjectAnchors(workspace);
47
- await ensureAiFeatureSyncRestAnchors(workspace);
48
- await fsp.writeFile(syncAiScriptPath, buildAiFeatureSyncScriptSource(), "utf8");
49
- await fsp.writeFile(typesFilePath, buildAiFeatureTypesSource(aiFeatureSlug), "utf8");
50
- await fsp.writeFile(validatorsFilePath, buildAiFeatureValidatorsSource(aiFeatureSlug), "utf8");
51
- await fsp.writeFile(apiFilePath, buildAiFeatureApiSource(aiFeatureSlug), "utf8");
52
- await fsp.writeFile(dataFilePath, buildAiFeatureDataSource(aiFeatureSlug), "utf8");
53
- await fsp.writeFile(phpFilePath, buildAiFeaturePhpSource(aiFeatureSlug, namespace, workspace.workspace.phpPrefix, workspace.workspace.textDomain), "utf8");
54
- const pascalCase = toPascalCase(aiFeatureSlug);
55
- await syncAiFeatureRestArtifacts({
56
- clientFile: `src/ai-features/${aiFeatureSlug}/api-client.ts`,
57
- outputDir: path.join("src", "ai-features", aiFeatureSlug),
58
- projectDir: workspace.projectDir,
59
- typesFile: `src/ai-features/${aiFeatureSlug}/api-types.ts`,
60
- validatorsFile: `src/ai-features/${aiFeatureSlug}/api-validators.ts`,
61
- variables: {
62
- namespace,
63
- pascalCase,
64
- slugKebabCase: aiFeatureSlug,
65
- title: toTitleCase(aiFeatureSlug),
66
- },
67
- });
68
- await syncAiFeatureSchemaArtifact({
69
- aiSchemaFile: `src/ai-features/${aiFeatureSlug}/ai-schemas/feature-result.ai.schema.json`,
70
- outputDir: path.join("src", "ai-features", aiFeatureSlug),
71
- projectDir: workspace.projectDir,
72
- });
73
- await appendWorkspaceInventoryEntries(workspace.projectDir, {
74
- aiFeatureEntries: [
75
- buildAiFeatureConfigEntry(aiFeatureSlug, namespace),
76
- ],
77
- transformSource: ensureBlockConfigCanAddRestManifests,
78
- });
79
- return {
80
- warnings: packageScriptChanges.addedProjectToolsDependency
81
- ? [
82
- "Added `@wp-typia/project-tools` to devDependencies for `sync-ai`. If this workspace was already installed, rerun your package manager install command before the first `wp-typia sync ai`.",
83
- ]
84
- : [],
85
- };
86
- }
87
- catch (error) {
88
- await rollbackWorkspaceMutation(mutationSnapshot);
89
- throw error;
90
- }
39
+ run: async () => {
40
+ await fsp.mkdir(aiFeatureDir, { recursive: true });
41
+ await fsp.mkdir(path.dirname(phpFilePath), { recursive: true });
42
+ await ensureAiFeatureBootstrapAnchors(workspace);
43
+ await patchFile(bootstrapPath, (source) => updatePluginHeaderCompatibility(source, compatibilityPolicy));
44
+ const packageScriptChanges = await ensureAiFeaturePackageScripts(workspace);
45
+ await ensureAiFeatureSyncProjectAnchors(workspace);
46
+ await ensureAiFeatureSyncRestAnchors(workspace);
47
+ await fsp.writeFile(syncAiScriptPath, buildAiFeatureSyncScriptSource(), "utf8");
48
+ await fsp.writeFile(typesFilePath, buildAiFeatureTypesSource(aiFeatureSlug), "utf8");
49
+ await fsp.writeFile(validatorsFilePath, buildAiFeatureValidatorsSource(aiFeatureSlug), "utf8");
50
+ await fsp.writeFile(apiFilePath, buildAiFeatureApiSource(aiFeatureSlug), "utf8");
51
+ await fsp.writeFile(dataFilePath, buildAiFeatureDataSource(aiFeatureSlug), "utf8");
52
+ await fsp.writeFile(phpFilePath, buildAiFeaturePhpSource(aiFeatureSlug, namespace, workspace.workspace.phpPrefix, workspace.workspace.textDomain), "utf8");
53
+ const pascalCase = toPascalCase(aiFeatureSlug);
54
+ await syncAiFeatureRestArtifacts({
55
+ clientFile: `src/ai-features/${aiFeatureSlug}/api-client.ts`,
56
+ outputDir: path.join("src", "ai-features", aiFeatureSlug),
57
+ projectDir: workspace.projectDir,
58
+ typesFile: `src/ai-features/${aiFeatureSlug}/api-types.ts`,
59
+ validatorsFile: `src/ai-features/${aiFeatureSlug}/api-validators.ts`,
60
+ variables: {
61
+ namespace,
62
+ pascalCase,
63
+ slugKebabCase: aiFeatureSlug,
64
+ title: toTitleCase(aiFeatureSlug),
65
+ },
66
+ });
67
+ await syncAiFeatureSchemaArtifact({
68
+ aiSchemaFile: `src/ai-features/${aiFeatureSlug}/ai-schemas/feature-result.ai.schema.json`,
69
+ outputDir: path.join("src", "ai-features", aiFeatureSlug),
70
+ projectDir: workspace.projectDir,
71
+ });
72
+ await appendWorkspaceInventoryEntries(workspace.projectDir, {
73
+ aiFeatureEntries: [
74
+ buildAiFeatureConfigEntry(aiFeatureSlug, namespace),
75
+ ],
76
+ transformSource: ensureBlockConfigCanAddRestManifests,
77
+ });
78
+ return {
79
+ warnings: packageScriptChanges.addedProjectToolsDependency
80
+ ? [
81
+ "Added `@wp-typia/project-tools` to devDependencies for `sync-ai`. If this workspace was already installed, rerun your package manager install command before the first `wp-typia sync ai`.",
82
+ ]
83
+ : [],
84
+ };
85
+ },
86
+ });
91
87
  }
@@ -43,6 +43,8 @@ if ( ! defined( 'ABSPATH' ) ) {
43
43
  * - ${quotePhpString(adminNoticeMessageFilterHook)} filters the wp-admin notice shown when AI support is unavailable.
44
44
  * - ${quotePhpString(unavailableMessageFilterHook)} filters REST-facing unavailable messages by reason code.
45
45
  * - ${quotePhpString(telemetryFilterHook)} filters the response telemetry array before schema validation. Return a schema-compatible array.
46
+ *
47
+ * Compatibility note: this server-only endpoint avoids WordPress script-module enqueue APIs so older sites can load the generated feature file safely.
46
48
  */
47
49
 
48
50
  if ( ! function_exists( '${loadSchemaFunctionName}' ) ) {
@@ -8,6 +8,7 @@ import { readWorkspaceInventory, appendWorkspaceInventoryEntries, } from "./work
8
8
  import { toPascalCase, toTitleCase } from "./string-case.js";
9
9
  import { findPhpFunctionRange, hasPhpFunctionDefinition, quotePhpString, replacePhpFunctionDefinition, } from "./php-utils.js";
10
10
  import { assertBindingSourceDoesNotExist, assertEditorPluginDoesNotExist, assertPatternDoesNotExist, assertValidEditorPluginSlot, assertValidGeneratedSlug, getWorkspaceBootstrapPath, normalizeBlockSlug, patchFile, quoteTsString, resolveWorkspaceBlock, rollbackWorkspaceMutation, snapshotWorkspaceFiles, } from "./cli-add-shared.js";
11
+ import { appendPhpSnippetBeforeClosingTag, insertPhpSnippetBeforeWorkspaceAnchors, } from "./cli-add-workspace-mutation.js";
11
12
  import { normalizeOptionalCliString } from "./cli-validation.js";
12
13
  const PATTERN_BOOTSTRAP_CATEGORY = "register_block_pattern_category";
13
14
  const BINDING_SOURCE_SERVER_GLOB = "/src/bindings/*/server.php";
@@ -597,39 +598,17 @@ function ${bindingEditorEnqueueFunctionName}() {
597
598
  \t);
598
599
  }
599
600
  `;
600
- const insertionAnchors = [
601
- /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
602
- /\?>\s*$/u,
603
- ];
604
- const insertPhpSnippet = (snippet) => {
605
- for (const anchor of insertionAnchors) {
606
- const candidate = nextSource.replace(anchor, (match) => `${snippet}\n${match}`);
607
- if (candidate !== nextSource) {
608
- nextSource = candidate;
609
- return;
610
- }
611
- }
612
- nextSource = `${nextSource.trimEnd()}\n${snippet}\n`;
613
- };
614
- const appendPhpSnippet = (snippet) => {
615
- const closingTagPattern = /\?>\s*$/u;
616
- if (closingTagPattern.test(nextSource)) {
617
- nextSource = nextSource.replace(closingTagPattern, `${snippet}\n?>`);
618
- return;
619
- }
620
- nextSource = `${nextSource.trimEnd()}\n${snippet}\n`;
621
- };
622
601
  if (!hasPhpFunctionDefinition(nextSource, bindingRegistrationFunctionName)) {
623
- insertPhpSnippet(bindingRegistrationFunction);
602
+ nextSource = insertPhpSnippetBeforeWorkspaceAnchors(nextSource, bindingRegistrationFunction);
624
603
  }
625
604
  if (!hasPhpFunctionDefinition(nextSource, bindingEditorEnqueueFunctionName)) {
626
- insertPhpSnippet(bindingEditorEnqueueFunction);
605
+ nextSource = insertPhpSnippetBeforeWorkspaceAnchors(nextSource, bindingEditorEnqueueFunction);
627
606
  }
628
607
  if (!nextSource.includes(bindingRegistrationHook)) {
629
- appendPhpSnippet(bindingRegistrationHook);
608
+ nextSource = appendPhpSnippetBeforeClosingTag(nextSource, bindingRegistrationHook);
630
609
  }
631
610
  if (!nextSource.includes(bindingEditorEnqueueHook)) {
632
- appendPhpSnippet(bindingEditorEnqueueHook);
611
+ nextSource = appendPhpSnippetBeforeClosingTag(nextSource, bindingEditorEnqueueHook);
633
612
  }
634
613
  return nextSource;
635
614
  });
@@ -679,30 +658,8 @@ function ${enqueueFunctionName}() {
679
658
  \t}
680
659
  }
681
660
  `;
682
- const insertionAnchors = [
683
- /add_action\(\s*["']init["']\s*,\s*["'][^"']+_load_textdomain["']\s*\);\s*\n/u,
684
- /\?>\s*$/u,
685
- ];
686
- const insertPhpSnippet = (snippet) => {
687
- for (const anchor of insertionAnchors) {
688
- const candidate = nextSource.replace(anchor, (match) => `${snippet}\n${match}`);
689
- if (candidate !== nextSource) {
690
- nextSource = candidate;
691
- return;
692
- }
693
- }
694
- nextSource = `${nextSource.trimEnd()}\n${snippet}\n`;
695
- };
696
- const appendPhpSnippet = (snippet) => {
697
- const closingTagPattern = /\?>\s*$/u;
698
- if (closingTagPattern.test(nextSource)) {
699
- nextSource = nextSource.replace(closingTagPattern, `${snippet}\n?>`);
700
- return;
701
- }
702
- nextSource = `${nextSource.trimEnd()}\n${snippet}\n`;
703
- };
704
661
  if (!hasPhpFunctionDefinition(nextSource, enqueueFunctionName)) {
705
- insertPhpSnippet(enqueueFunction);
662
+ nextSource = insertPhpSnippetBeforeWorkspaceAnchors(nextSource, enqueueFunction);
706
663
  }
707
664
  else {
708
665
  const requiredReferences = [
@@ -726,7 +683,7 @@ function ${enqueueFunctionName}() {
726
683
  }
727
684
  }
728
685
  if (!nextSource.includes(enqueueHook)) {
729
- appendPhpSnippet(enqueueHook);
686
+ nextSource = appendPhpSnippetBeforeClosingTag(nextSource, enqueueHook);
730
687
  }
731
688
  return nextSource;
732
689
  });
@@ -0,0 +1,30 @@
1
+ export interface WorkspaceMutationPlan<TResult> {
2
+ /** Files to capture before the mutation starts. Missing files are restored as absent. */
3
+ filePaths: string[];
4
+ /** Snapshot directories created by the mutation, usually migration fixtures. */
5
+ snapshotDirs?: string[];
6
+ /** Created files or directories to remove if the mutation fails. */
7
+ targetPaths?: string[];
8
+ /** Mutating work to execute after the snapshot is captured. */
9
+ run: () => Promise<TResult>;
10
+ }
11
+ /**
12
+ * Error thrown when the mutation and its rollback both fail.
13
+ */
14
+ export declare class WorkspaceMutationRollbackError extends Error {
15
+ readonly mutationError: unknown;
16
+ readonly rollbackError: unknown;
17
+ constructor(mutationError: unknown, rollbackError: unknown);
18
+ }
19
+ /**
20
+ * Execute a workspace add mutation with rollback on any failure.
21
+ */
22
+ export declare function executeWorkspaceMutationPlan<TResult>({ filePaths, run, snapshotDirs, targetPaths, }: WorkspaceMutationPlan<TResult>): Promise<TResult>;
23
+ /**
24
+ * Insert a PHP snippet before the workspace textdomain hook or closing tag.
25
+ */
26
+ export declare function insertPhpSnippetBeforeWorkspaceAnchors(source: string, snippet: string): string;
27
+ /**
28
+ * Append a PHP snippet before the closing tag when one is present.
29
+ */
30
+ export declare function appendPhpSnippetBeforeClosingTag(source: string, snippet: string): string;