@wp-typia/project-tools 0.22.2 → 0.22.4

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 (62) hide show
  1. package/dist/runtime/built-in-block-code-templates/interactivity.d.ts +1 -1
  2. package/dist/runtime/built-in-block-code-templates/interactivity.js +4 -2
  3. package/dist/runtime/cli-add-block-json.d.ts +31 -0
  4. package/dist/runtime/cli-add-block-json.js +65 -0
  5. package/dist/runtime/cli-add-collision.d.ts +129 -0
  6. package/dist/runtime/cli-add-collision.js +293 -0
  7. package/dist/runtime/cli-add-filesystem.d.ts +29 -0
  8. package/dist/runtime/cli-add-filesystem.js +77 -0
  9. package/dist/runtime/cli-add-help.d.ts +4 -0
  10. package/dist/runtime/cli-add-help.js +41 -0
  11. package/dist/runtime/cli-add-shared.d.ts +6 -255
  12. package/dist/runtime/cli-add-shared.js +6 -391
  13. package/dist/runtime/cli-add-types.d.ts +247 -0
  14. package/dist/runtime/cli-add-types.js +64 -0
  15. package/dist/runtime/cli-add-validation.d.ts +87 -0
  16. package/dist/runtime/cli-add-validation.js +147 -0
  17. package/dist/runtime/cli-add-workspace-ability-scaffold.d.ts +5 -0
  18. package/dist/runtime/cli-add-workspace-ability-scaffold.js +366 -0
  19. package/dist/runtime/cli-add-workspace-ability-templates.d.ts +34 -0
  20. package/dist/runtime/cli-add-workspace-ability-templates.js +500 -0
  21. package/dist/runtime/cli-add-workspace-ability-types.d.ts +27 -0
  22. package/dist/runtime/cli-add-workspace-ability-types.js +14 -0
  23. package/dist/runtime/cli-add-workspace-ability.js +12 -852
  24. package/dist/runtime/cli-add-workspace-admin-view-scaffold.js +35 -61
  25. package/dist/runtime/cli-add-workspace-ai-scaffold.d.ts +21 -0
  26. package/dist/runtime/cli-add-workspace-ai-scaffold.js +87 -0
  27. package/dist/runtime/cli-add-workspace-ai-templates.d.ts +4 -0
  28. package/dist/runtime/cli-add-workspace-ai-templates.js +607 -0
  29. package/dist/runtime/cli-add-workspace-ai.js +15 -688
  30. package/dist/runtime/cli-add-workspace-assets.js +7 -4
  31. package/dist/runtime/cli-add-workspace-mutation.d.ts +30 -0
  32. package/dist/runtime/cli-add-workspace-mutation.js +60 -0
  33. package/dist/runtime/cli-add-workspace.js +2 -98
  34. package/dist/runtime/cli-add.d.ts +2 -2
  35. package/dist/runtime/cli-add.js +2 -2
  36. package/dist/runtime/cli-doctor-workspace-bindings.d.ts +11 -0
  37. package/dist/runtime/cli-doctor-workspace-bindings.js +134 -0
  38. package/dist/runtime/cli-doctor-workspace-blocks.d.ts +11 -0
  39. package/dist/runtime/cli-doctor-workspace-blocks.js +439 -0
  40. package/dist/runtime/cli-doctor-workspace-features.d.ts +11 -0
  41. package/dist/runtime/cli-doctor-workspace-features.js +383 -0
  42. package/dist/runtime/cli-doctor-workspace-package.d.ts +18 -0
  43. package/dist/runtime/cli-doctor-workspace-package.js +59 -0
  44. package/dist/runtime/cli-doctor-workspace-shared.d.ts +69 -0
  45. package/dist/runtime/cli-doctor-workspace-shared.js +87 -0
  46. package/dist/runtime/cli-doctor-workspace.js +25 -1062
  47. package/dist/runtime/index.d.ts +2 -0
  48. package/dist/runtime/index.js +1 -0
  49. package/dist/runtime/migration-utils.d.ts +2 -1
  50. package/dist/runtime/migration-utils.js +3 -11
  51. package/dist/runtime/package-managers.d.ts +19 -0
  52. package/dist/runtime/package-managers.js +62 -0
  53. package/dist/runtime/template-source-cache.d.ts +59 -0
  54. package/dist/runtime/template-source-cache.js +160 -0
  55. package/dist/runtime/ts-source-masking.d.ts +28 -0
  56. package/dist/runtime/ts-source-masking.js +104 -0
  57. package/dist/runtime/typia-llm.d.ts +9 -1
  58. package/dist/runtime/typia-llm.js +20 -5
  59. package/dist/runtime/workspace-inventory.js +116 -59
  60. package/dist/runtime/workspace-project.d.ts +1 -1
  61. package/dist/runtime/workspace-project.js +2 -10
  62. package/package.json +4 -4
@@ -1,952 +1,24 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { parseScaffoldBlockMetadata } from "@wp-typia/block-runtime/blocks";
4
- import { EDITOR_PLUGIN_SLOT_IDS, REST_RESOURCE_METHOD_IDS, REST_RESOURCE_NAMESPACE_PATTERN, resolveEditorPluginSlotAlias, } from "./cli-add-shared.js";
5
- import { HOOKED_BLOCK_ANCHOR_PATTERN, HOOKED_BLOCK_POSITION_SET, } from "./hooked-blocks.js";
1
+ import { getWorkspaceBindingDoctorChecks, } from "./cli-doctor-workspace-bindings.js";
2
+ import { getWorkspaceBlockDoctorChecks, } from "./cli-doctor-workspace-blocks.js";
3
+ import { getWorkspaceFeatureDoctorChecks, } from "./cli-doctor-workspace-features.js";
4
+ import { getMigrationWorkspaceHintCheck, getWorkspacePackageMetadataCheck, } from "./cli-doctor-workspace-package.js";
5
+ import { createDoctorCheck, createDoctorScopeCheck, } from "./cli-doctor-workspace-shared.js";
6
6
  import { readWorkspaceInventory } from "./workspace-inventory.js";
7
- import { getInvalidWorkspaceProjectReason, parseWorkspacePackageJson, WORKSPACE_TEMPLATE_PACKAGE, tryResolveWorkspaceProject, } from "./workspace-project.js";
8
- import { escapeRegex } from "./php-utils.js";
9
- const WORKSPACE_COLLECTION_IMPORT_LINE = "import '../../collection';";
10
- const WORKSPACE_COLLECTION_IMPORT_PATTERN = /^\s*import\s+["']\.\.\/\.\.\/collection["']\s*;?\s*$/m;
11
- const WORKSPACE_BINDING_SERVER_GLOB = "/src/bindings/*/server.php";
12
- const WORKSPACE_BINDING_EDITOR_SCRIPT = "build/bindings/index.js";
13
- const WORKSPACE_BINDING_EDITOR_ASSET = "build/bindings/index.asset.php";
14
- const WORKSPACE_REST_RESOURCE_GLOB = "/inc/rest/*.php";
15
- const WORKSPACE_ABILITY_GLOB = "/inc/abilities/*.php";
16
- const WORKSPACE_ABILITY_EDITOR_SCRIPT = "build/abilities/index.js";
17
- const WORKSPACE_ABILITY_EDITOR_ASSET = "build/abilities/index.asset.php";
18
- const WORKSPACE_AI_FEATURE_GLOB = "/inc/ai-features/*.php";
19
- const WORKSPACE_ADMIN_VIEW_GLOB = "/inc/admin-views/*.php";
20
- const WORKSPACE_ADMIN_VIEW_SCRIPT = "build/admin-views/index.js";
21
- const WORKSPACE_ADMIN_VIEW_ASSET = "build/admin-views/index.asset.php";
22
- const WORKSPACE_ADMIN_VIEW_STYLE = "build/admin-views/style-index.css";
23
- const WORKSPACE_EDITOR_PLUGIN_EDITOR_SCRIPT = "build/editor-plugins/index.js";
24
- const WORKSPACE_EDITOR_PLUGIN_EDITOR_ASSET = "build/editor-plugins/index.asset.php";
25
- const WORKSPACE_EDITOR_PLUGIN_EDITOR_STYLE = "build/editor-plugins/style-index.css";
26
- const WORKSPACE_GENERATED_BLOCK_ARTIFACTS = [
27
- "block.json",
28
- "typia.manifest.json",
29
- "typia.schema.json",
30
- "typia-validator.php",
31
- "typia.openapi.json",
32
- ];
33
- const WORKSPACE_FULL_BLOCK_NAME_PATTERN = /^[a-z0-9-]+\/[a-z0-9-]+$/u;
34
- const WORKSPACE_VARIATIONS_IMPORT_PATTERN = /^\s*import\s*\{\s*registerWorkspaceVariations\s*\}\s*from\s*["']\.\/variations["']\s*;?\s*$/mu;
35
- const WORKSPACE_VARIATIONS_CALL_PATTERN = /registerWorkspaceVariations\s*\(\s*\)\s*;?/u;
36
- const WORKSPACE_BLOCK_STYLES_IMPORT_PATTERN = /^\s*import\s*\{\s*registerWorkspaceBlockStyles\s*\}\s*from\s*["']\.\/styles["']\s*;?\s*$/mu;
37
- const WORKSPACE_BLOCK_STYLES_CALL_PATTERN = /registerWorkspaceBlockStyles\s*\(\s*\)\s*;?/u;
38
- const WORKSPACE_BLOCK_TRANSFORMS_IMPORT_PATTERN = /^\s*import\s*\{\s*applyWorkspaceBlockTransforms\s*\}\s*from\s*["']\.\/transforms["']\s*;?\s*$/mu;
39
- const WORKSPACE_BLOCK_TRANSFORMS_CALL_PATTERN = /applyWorkspaceBlockTransforms\s*\(\s*registration\s*\.\s*settings\s*\)\s*;?/u;
40
- const WORKSPACE_BLOCK_IFRAME_COMPATIBILITY_DOC_URL = "https://developer.wordpress.org/block-editor/reference-guides/block-api/block-api-versions/block-migration-for-iframe-editor-compatibility/";
41
- const WORKSPACE_BLOCK_IFRAME_DIAGNOSTIC_CODES = {
42
- API_VERSION: "wp-typia.workspace.block.iframe.api-version",
43
- BLOCK_PROPS: "wp-typia.workspace.block.iframe.block-props",
44
- EDITOR_GLOBALS: "wp-typia.workspace.block.iframe.editor-globals",
45
- EDITOR_STYLES: "wp-typia.workspace.block.iframe.editor-styles",
46
- };
47
- const WORKSPACE_BLOCK_EDITOR_SOURCE_FILE_PATTERN = /\.[cm]?[jt]sx?$/u;
48
- const WORKSPACE_BLOCK_EDITOR_SOURCE_BASENAMES = new Set([
49
- "edit",
50
- "editor",
51
- "index",
52
- "save",
53
- ]);
54
- const WORKSPACE_BLOCK_EDITOR_SOURCE_DIRECTORIES = new Set([
55
- "components",
56
- "controls",
57
- "editor",
58
- "inspector",
59
- ]);
60
- const WORKSPACE_BLOCK_LOCAL_STYLE_FILES = [
61
- "editor.css",
62
- "editor.scss",
63
- "index.css",
64
- "style.css",
65
- "style.scss",
66
- ];
67
- const WORKSPACE_BLOCK_IFRAME_GLOBAL_DOM_PATTERN = /\b(?:document|window)\b|\b(?:parent|top)\b(?!\s*:)/gu;
68
- const WORKSPACE_BLOCK_PROPS_PATTERN = /\buse(?:Block|InnerBlocks)Props(?:\.save)?\s*\(/u;
69
- function createDoctorCheck(label, status, detail, code) {
70
- return code ? { code, detail, label, status } : { detail, label, status };
71
- }
72
- function createDoctorScopeCheck(status, detail) {
73
- return createDoctorCheck("Doctor scope", status, detail);
74
- }
75
- function maskSourceSegment(segment) {
76
- return segment.replace(/[^\n\r]/gu, " ");
77
- }
78
- function maskTypeScriptComments(source) {
79
- return source
80
- .replace(/\/\*[\s\S]*?\*\//gu, maskSourceSegment)
81
- .replace(/\/\/[^\n\r]*/gu, maskSourceSegment);
82
- }
83
- // Preserve offsets while hiding non-executable text from hook checks.
84
- function maskTypeScriptCommentsAndLiterals(source) {
85
- let maskedSource = "";
86
- let index = 0;
87
- while (index < source.length) {
88
- const current = source[index];
89
- const next = source[index + 1];
90
- if (current === "/" && next === "/") {
91
- const start = index;
92
- index += 2;
93
- while (index < source.length &&
94
- source[index] !== "\n" &&
95
- source[index] !== "\r") {
96
- index += 1;
97
- }
98
- maskedSource += maskSourceSegment(source.slice(start, index));
99
- continue;
100
- }
101
- if (current === "/" && next === "*") {
102
- const start = index;
103
- index += 2;
104
- while (index < source.length &&
105
- !(source[index] === "*" && source[index + 1] === "/")) {
106
- index += 1;
107
- }
108
- index = Math.min(index + 2, source.length);
109
- maskedSource += maskSourceSegment(source.slice(start, index));
110
- continue;
111
- }
112
- if (current === "'" || current === '"' || current === "`") {
113
- const start = index;
114
- const quote = current;
115
- index += 1;
116
- while (index < source.length) {
117
- const char = source[index];
118
- if (char === "\\") {
119
- index += 2;
120
- continue;
121
- }
122
- index += 1;
123
- if (char === quote) {
124
- break;
125
- }
126
- }
127
- maskedSource += maskSourceSegment(source.slice(start, index));
128
- continue;
129
- }
130
- maskedSource += current;
131
- index += 1;
132
- }
133
- return maskedSource;
134
- }
135
- function hasUncommentedPattern(source, pattern) {
136
- return pattern.test(maskTypeScriptComments(source));
137
- }
138
- function hasExecutablePattern(source, pattern) {
139
- return pattern.test(maskTypeScriptCommentsAndLiterals(source));
140
- }
141
- function normalizePathSeparators(relativePath) {
142
- return relativePath.split(path.sep).join("/");
143
- }
144
- function hasRegisteredBlockAsset(value) {
145
- if (typeof value === "string") {
146
- return value.trim().length > 0;
147
- }
148
- if (Array.isArray(value)) {
149
- return value.some((entry) => hasRegisteredBlockAsset(entry));
150
- }
151
- return false;
152
- }
153
- function readWorkspaceBlockIframeMetadata(projectDir, blockSlug) {
154
- const blockJsonRelativePath = path.join("src", "blocks", blockSlug, "block.json");
155
- const blockJsonPath = path.join(projectDir, blockJsonRelativePath);
156
- if (!fs.existsSync(blockJsonPath)) {
157
- return {
158
- blockJsonRelativePath,
159
- error: `Missing ${blockJsonRelativePath}`,
160
- };
161
- }
162
- try {
163
- const parsed = JSON.parse(fs.readFileSync(blockJsonPath, "utf8"));
164
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
165
- return {
166
- blockJsonRelativePath,
167
- error: `${blockJsonRelativePath} must contain a JSON object`,
168
- };
169
- }
170
- return {
171
- blockJsonRelativePath,
172
- document: parsed,
173
- };
174
- }
175
- catch (error) {
176
- return {
177
- blockJsonRelativePath,
178
- error: error instanceof Error ? error.message : String(error),
179
- };
180
- }
181
- }
182
- function isWorkspaceBlockEditorSource(relativePath) {
183
- const normalizedPath = normalizePathSeparators(relativePath);
184
- const normalizedLowerPath = normalizedPath.toLowerCase();
185
- if (!WORKSPACE_BLOCK_EDITOR_SOURCE_FILE_PATTERN.test(normalizedLowerPath) ||
186
- /\.d\.[cm]?[jt]s$/u.test(normalizedLowerPath)) {
187
- return false;
188
- }
189
- const segments = normalizedLowerPath.split("/");
190
- const fileName = segments[segments.length - 1] ?? "";
191
- const baseName = fileName.replace(/\.[^.]+$/u, "");
192
- if (WORKSPACE_BLOCK_EDITOR_SOURCE_BASENAMES.has(baseName)) {
193
- return true;
194
- }
195
- return segments
196
- .slice(0, -1)
197
- .some((segment) => WORKSPACE_BLOCK_EDITOR_SOURCE_DIRECTORIES.has(segment));
198
- }
199
- function isWorkspaceBlockSaveSource(relativePath) {
200
- const fileName = normalizePathSeparators(relativePath).split("/").pop() ?? "";
201
- return fileName.replace(/\.[^.]+$/u, "").toLowerCase() === "save";
202
- }
203
- function collectWorkspaceBlockEditorSources(projectDir, blockSlug) {
204
- const blockDir = path.join(projectDir, "src", "blocks", blockSlug);
205
- if (!fs.existsSync(blockDir)) {
206
- return [];
207
- }
208
- const sources = [];
209
- const visitDirectory = (directory) => {
210
- for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
211
- const entryPath = path.join(directory, entry.name);
212
- if (entry.isDirectory()) {
213
- visitDirectory(entryPath);
214
- continue;
215
- }
216
- if (!entry.isFile()) {
217
- continue;
218
- }
219
- const relativePath = normalizePathSeparators(path.relative(projectDir, entryPath));
220
- const blockRelativePath = path.relative(blockDir, entryPath);
221
- if (!isWorkspaceBlockEditorSource(blockRelativePath)) {
222
- continue;
223
- }
224
- sources.push({
225
- relativePath,
226
- source: fs.readFileSync(entryPath, "utf8"),
227
- });
228
- }
229
- };
230
- visitDirectory(blockDir);
231
- return sources;
232
- }
233
- function getSourceLineNumber(source, index) {
234
- let lineNumber = 1;
235
- for (let cursor = 0; cursor < index; cursor += 1) {
236
- if (source[cursor] === "\n") {
237
- lineNumber += 1;
238
- }
239
- }
240
- return lineNumber;
241
- }
242
- function isGlobalDomAccessCandidate(maskedSource, index, token) {
243
- if (token === "document" || token === "window") {
244
- return true;
245
- }
246
- const before = maskedSource.slice(0, index);
247
- const after = maskedSource.slice(index + token.length);
248
- const previousNonWhitespace = before.match(/\S(?=\s*$)/u)?.[0] ?? "";
249
- const nextNonWhitespace = after.match(/^\s*(\S)/u)?.[1] ?? "";
250
- if (previousNonWhitespace === "." || previousNonWhitespace === "{" || previousNonWhitespace === ",") {
251
- return false;
252
- }
253
- if (nextNonWhitespace === ":") {
254
- return false;
255
- }
256
- if (/\b(?:const|function|let|var)\s+$/u.test(before)) {
257
- return false;
258
- }
259
- return true;
260
- }
261
- function findWorkspaceBlockGlobalDomAccesses(sources) {
262
- const findings = [];
263
- for (const { relativePath, source } of sources) {
264
- const maskedSource = maskTypeScriptCommentsAndLiterals(source);
265
- const matches = maskedSource.matchAll(WORKSPACE_BLOCK_IFRAME_GLOBAL_DOM_PATTERN);
266
- for (const match of matches) {
267
- const index = match.index ?? 0;
268
- const matchedToken = match[0].replace(/\W+/gu, "");
269
- if (!isGlobalDomAccessCandidate(maskedSource, index, matchedToken)) {
270
- continue;
271
- }
272
- findings.push(`${relativePath}:${getSourceLineNumber(source, index)} (${matchedToken})`);
273
- if (findings.length >= 5) {
274
- return findings;
275
- }
276
- }
277
- }
278
- return findings;
279
- }
280
- function getWorkspaceBootstrapRelativePath(packageName) {
281
- const packageBaseName = packageName.split("/").pop() ?? packageName;
282
- return `${packageBaseName}.php`;
283
- }
284
- function checkExistingFiles(projectDir, label, filePaths) {
285
- const missing = filePaths
286
- .filter((filePath) => typeof filePath === "string")
287
- .filter((filePath) => !fs.existsSync(path.join(projectDir, filePath)));
288
- return createDoctorCheck(label, missing.length === 0 ? "pass" : "fail", missing.length === 0 ? "All referenced files exist" : `Missing: ${missing.join(", ")}`);
289
- }
290
- function checkWorkspacePackageMetadata(workspace, packageJson) {
291
- const issues = [];
292
- const packageName = packageJson.name;
293
- const bootstrapRelativePath = getWorkspaceBootstrapRelativePath(typeof packageName === "string" && packageName.length > 0 ? packageName : workspace.packageName);
294
- const wpTypia = packageJson.wpTypia;
295
- if (typeof packageName !== "string" || packageName.length === 0) {
296
- issues.push("package.json must define a string name for workspace bootstrap resolution");
297
- }
298
- if (wpTypia?.projectType !== "workspace") {
299
- issues.push('wpTypia.projectType must be "workspace"');
300
- }
301
- if (wpTypia?.templatePackage !== WORKSPACE_TEMPLATE_PACKAGE) {
302
- issues.push(`wpTypia.templatePackage must be "${WORKSPACE_TEMPLATE_PACKAGE}"`);
303
- }
304
- if (wpTypia?.namespace !== workspace.workspace.namespace) {
305
- issues.push(`wpTypia.namespace must equal "${workspace.workspace.namespace}"`);
306
- }
307
- if (wpTypia?.textDomain !== workspace.workspace.textDomain) {
308
- issues.push(`wpTypia.textDomain must equal "${workspace.workspace.textDomain}"`);
309
- }
310
- if (wpTypia?.phpPrefix !== workspace.workspace.phpPrefix) {
311
- issues.push(`wpTypia.phpPrefix must equal "${workspace.workspace.phpPrefix}"`);
312
- }
313
- if (!fs.existsSync(path.join(workspace.projectDir, bootstrapRelativePath))) {
314
- issues.push(`Missing bootstrap file ${bootstrapRelativePath}`);
315
- }
316
- return createDoctorCheck("Workspace package metadata", issues.length === 0 ? "pass" : "fail", issues.length === 0
317
- ? `package.json metadata aligns with ${workspace.packageName} and ${bootstrapRelativePath}`
318
- : issues.join("; "));
319
- }
320
- function getWorkspaceBlockRequiredFiles(block) {
321
- const blockDir = path.join("src", "blocks", block.slug);
322
- return Array.from(new Set([
323
- block.typesFile,
324
- block.apiTypesFile,
325
- block.openApiFile,
326
- path.join(blockDir, "index.tsx"),
327
- ...WORKSPACE_GENERATED_BLOCK_ARTIFACTS.map((fileName) => path.join(blockDir, fileName)),
328
- ].filter((filePath) => typeof filePath === "string")));
329
- }
330
- function checkWorkspaceBlockMetadata(projectDir, workspace, block) {
331
- const blockJsonRelativePath = path.join("src", "blocks", block.slug, "block.json");
332
- const blockJsonPath = path.join(projectDir, blockJsonRelativePath);
333
- if (!fs.existsSync(blockJsonPath)) {
334
- return createDoctorCheck(`Block metadata ${block.slug}`, "fail", `Missing ${blockJsonRelativePath}`);
335
- }
336
- let blockJson;
337
- try {
338
- blockJson = parseScaffoldBlockMetadata(JSON.parse(fs.readFileSync(blockJsonPath, "utf8")));
339
- }
340
- catch (error) {
341
- return createDoctorCheck(`Block metadata ${block.slug}`, "fail", error instanceof Error ? error.message : String(error));
342
- }
343
- const expectedName = `${workspace.workspace.namespace}/${block.slug}`;
344
- const issues = [];
345
- if (blockJson.name !== expectedName) {
346
- issues.push(`block.json name must equal "${expectedName}"`);
347
- }
348
- if (blockJson.textdomain !== workspace.workspace.textDomain) {
349
- issues.push(`block.json textdomain must equal "${workspace.workspace.textDomain}"`);
350
- }
351
- return createDoctorCheck(`Block metadata ${block.slug}`, issues.length === 0 ? "pass" : "fail", issues.length === 0
352
- ? `block.json matches ${expectedName} and ${workspace.workspace.textDomain}`
353
- : issues.join("; "));
354
- }
355
- function checkWorkspaceBlockHooks(projectDir, blockSlug) {
356
- const blockJsonRelativePath = path.join("src", "blocks", blockSlug, "block.json");
357
- const blockJsonPath = path.join(projectDir, blockJsonRelativePath);
358
- if (!fs.existsSync(blockJsonPath)) {
359
- return createDoctorCheck(`Block hooks ${blockSlug}`, "fail", `Missing ${blockJsonRelativePath}`);
360
- }
361
- let blockJson;
362
- try {
363
- blockJson = parseScaffoldBlockMetadata(JSON.parse(fs.readFileSync(blockJsonPath, "utf8")));
364
- }
365
- catch (error) {
366
- return createDoctorCheck(`Block hooks ${blockSlug}`, "fail", error instanceof Error ? error.message : String(error));
367
- }
368
- const blockHooks = blockJson.blockHooks;
369
- if (blockHooks === undefined) {
370
- return createDoctorCheck(`Block hooks ${blockSlug}`, "pass", "No blockHooks metadata configured");
371
- }
372
- if (!blockHooks || typeof blockHooks !== "object" || Array.isArray(blockHooks)) {
373
- return createDoctorCheck(`Block hooks ${blockSlug}`, "fail", `${blockJsonRelativePath} must define blockHooks as an object when present.`);
374
- }
375
- const blockName = typeof blockJson.name === "string" && blockJson.name.trim().length > 0
376
- ? blockJson.name.trim()
377
- : null;
378
- const invalidEntries = Object.entries(blockHooks).filter(([anchor, position]) => (blockName !== null && anchor.trim() === blockName) ||
379
- anchor.trim().length === 0 ||
380
- anchor !== anchor.trim() ||
381
- !HOOKED_BLOCK_ANCHOR_PATTERN.test(anchor) ||
382
- typeof position !== "string" ||
383
- !HOOKED_BLOCK_POSITION_SET.has(position));
384
- return createDoctorCheck(`Block hooks ${blockSlug}`, invalidEntries.length === 0 ? "pass" : "fail", invalidEntries.length === 0
385
- ? `blockHooks metadata is valid${Object.keys(blockHooks).length > 0 ? ` (${Object.keys(blockHooks).join(", ")})` : ""}`
386
- : `Invalid blockHooks entries: ${invalidEntries
387
- .map(([anchor, position]) => `${anchor || "<empty>"} => ${String(position)}`)
388
- .join(", ")}`);
389
- }
390
- function checkWorkspaceBlockCollectionImport(projectDir, blockSlug) {
391
- const entryRelativePath = path.join("src", "blocks", blockSlug, "index.tsx");
392
- const entryPath = path.join(projectDir, entryRelativePath);
393
- if (!fs.existsSync(entryPath)) {
394
- return createDoctorCheck(`Block collection ${blockSlug}`, "fail", `Missing ${entryRelativePath}`);
395
- }
396
- const source = fs.readFileSync(entryPath, "utf8");
397
- const hasCollectionImport = WORKSPACE_COLLECTION_IMPORT_PATTERN.test(source);
398
- return createDoctorCheck(`Block collection ${blockSlug}`, hasCollectionImport ? "pass" : "fail", hasCollectionImport
399
- ? "Shared block collection import is present"
400
- : `Missing a shared collection import like ${WORKSPACE_COLLECTION_IMPORT_LINE}`);
401
- }
402
- function checkWorkspaceBlockIframeCompatibility(projectDir, blockSlug) {
403
- const metadataResult = readWorkspaceBlockIframeMetadata(projectDir, blockSlug);
404
- if (!metadataResult.document) {
405
- return [
406
- createDoctorCheck(`Block iframe/API v3 ${blockSlug}`, "warn", metadataResult.error ?? `Unable to inspect ${metadataResult.blockJsonRelativePath}`, WORKSPACE_BLOCK_IFRAME_DIAGNOSTIC_CODES.API_VERSION),
407
- ];
408
- }
409
- const blockJson = metadataResult.document;
410
- const apiVersion = typeof blockJson.apiVersion === "number" && Number.isFinite(blockJson.apiVersion)
411
- ? blockJson.apiVersion
412
- : null;
413
- const blockDir = path.join(projectDir, "src", "blocks", blockSlug);
414
- const localStyleFiles = WORKSPACE_BLOCK_LOCAL_STYLE_FILES.filter((fileName) => fs.existsSync(path.join(blockDir, fileName))).map((fileName) => normalizePathSeparators(path.join("src", "blocks", blockSlug, fileName)));
415
- const hasRegisteredEditorStyles = hasRegisteredBlockAsset(blockJson.style) ||
416
- hasRegisteredBlockAsset(blockJson.editorStyle);
417
- const editorSources = collectWorkspaceBlockEditorSources(projectDir, blockSlug);
418
- const editorWrapperSources = editorSources.filter((source) => !isWorkspaceBlockSaveSource(source.relativePath));
419
- const globalDomAccesses = findWorkspaceBlockGlobalDomAccesses(editorSources);
420
- const hasBlockPropsUsage = editorSources.some(({ source }) => hasExecutablePattern(source, WORKSPACE_BLOCK_PROPS_PATTERN));
421
- const hasEditorBlockPropsUsage = editorWrapperSources.some(({ source }) => hasExecutablePattern(source, WORKSPACE_BLOCK_PROPS_PATTERN));
422
- const blockWrapperStatus = editorWrapperSources.length === 0 || hasEditorBlockPropsUsage ? "pass" : "warn";
423
- const blockWrapperDetail = editorSources.length === 0
424
- ? "No editor-facing block source files found; general file checks will report missing entrypoints"
425
- : editorWrapperSources.length === 0
426
- ? "No editor wrapper source files found; general file checks will report missing entrypoints"
427
- : hasEditorBlockPropsUsage
428
- ? "Editor-facing sources use block wrapper props"
429
- : hasBlockPropsUsage
430
- ? "Only save-facing useBlockProps.save() usage was detected. Confirm the editor wrapper also receives useBlockProps() or useInnerBlocksProps() before relying on iframe editor rendering."
431
- : "No useBlockProps(), useBlockProps.save(), or useInnerBlocksProps() usage was detected in editor-facing sources. Confirm the block wrapper receives WordPress block editor props before relying on iframe editor rendering.";
7
+ import { getInvalidWorkspaceProjectReason, parseWorkspacePackageJson, tryResolveWorkspaceProject, } from "./workspace-project.js";
8
+ function formatWorkspaceInventorySummary(inventory) {
432
9
  return [
433
- createDoctorCheck(`Block iframe API version ${blockSlug}`, apiVersion !== null && apiVersion >= 3 ? "pass" : "warn", apiVersion !== null && apiVersion >= 3
434
- ? "block.json declares apiVersion 3 for iframe editor readiness"
435
- : `Set ${metadataResult.blockJsonRelativePath} apiVersion to 3 after testing the block in iframe-enabled Post Editor and Site Editor contexts. WordPress recommends API v3 for iframe editor compatibility. See ${WORKSPACE_BLOCK_IFRAME_COMPATIBILITY_DOC_URL}`, WORKSPACE_BLOCK_IFRAME_DIAGNOSTIC_CODES.API_VERSION),
436
- createDoctorCheck(`Block iframe styles ${blockSlug}`, localStyleFiles.length === 0 || hasRegisteredEditorStyles ? "pass" : "warn", localStyleFiles.length === 0
437
- ? "No local block stylesheet source files found to register"
438
- : hasRegisteredEditorStyles
439
- ? "block.json registers block styles for iframe editor loading"
440
- : `Found stylesheet source files (${localStyleFiles.join(", ")}) but block.json does not declare style or editorStyle. Register block content styles so iframe editors do not depend on parent admin styles.`, WORKSPACE_BLOCK_IFRAME_DIAGNOSTIC_CODES.EDITOR_STYLES),
441
- createDoctorCheck(`Block iframe globals ${blockSlug}`, globalDomAccesses.length === 0 ? "pass" : "warn", globalDomAccesses.length === 0
442
- ? "No direct window/document/parent DOM access detected in editor-facing block sources"
443
- : `Direct global DOM access detected at ${globalDomAccesses.join(", ")}. Prefer element.ownerDocument/defaultView via refs or useRefEffect for iframe editor content.`, WORKSPACE_BLOCK_IFRAME_DIAGNOSTIC_CODES.EDITOR_GLOBALS),
444
- createDoctorCheck(`Block iframe wrapper ${blockSlug}`, blockWrapperStatus, blockWrapperDetail, WORKSPACE_BLOCK_IFRAME_DIAGNOSTIC_CODES.BLOCK_PROPS),
445
- ];
446
- }
447
- function checkWorkspacePatternBootstrap(projectDir, packageName) {
448
- const packageBaseName = packageName.split("/").pop() ?? packageName;
449
- const bootstrapPath = path.join(projectDir, `${packageBaseName}.php`);
450
- if (!fs.existsSync(bootstrapPath)) {
451
- return createDoctorCheck("Pattern bootstrap", "fail", `Missing ${path.basename(bootstrapPath)}`);
452
- }
453
- const source = fs.readFileSync(bootstrapPath, "utf8");
454
- const hasCategoryAnchor = source.includes("register_block_pattern_category");
455
- const hasPatternGlob = source.includes("/src/patterns/*.php");
456
- return createDoctorCheck("Pattern bootstrap", hasCategoryAnchor && hasPatternGlob ? "pass" : "fail", hasCategoryAnchor && hasPatternGlob
457
- ? "Pattern category and loader hooks are present"
458
- : "Missing pattern category registration or src/patterns loader hook");
459
- }
460
- function checkWorkspaceBindingBootstrap(projectDir, packageName) {
461
- const packageBaseName = packageName.split("/").pop() ?? packageName;
462
- const bootstrapPath = path.join(projectDir, `${packageBaseName}.php`);
463
- if (!fs.existsSync(bootstrapPath)) {
464
- return createDoctorCheck("Binding bootstrap", "fail", `Missing ${path.basename(bootstrapPath)}`);
465
- }
466
- const source = fs.readFileSync(bootstrapPath, "utf8");
467
- const hasServerGlob = source.includes(WORKSPACE_BINDING_SERVER_GLOB);
468
- const hasEditorEnqueueHook = source.includes("enqueue_block_editor_assets");
469
- const hasEditorScript = source.includes(WORKSPACE_BINDING_EDITOR_SCRIPT);
470
- const hasEditorAsset = source.includes(WORKSPACE_BINDING_EDITOR_ASSET);
471
- return createDoctorCheck("Binding bootstrap", hasServerGlob && hasEditorEnqueueHook && hasEditorScript && hasEditorAsset ? "pass" : "fail", hasServerGlob && hasEditorEnqueueHook && hasEditorScript && hasEditorAsset
472
- ? "Binding source PHP and editor bootstrap hooks are present"
473
- : "Missing binding source PHP require glob or editor enqueue hook");
474
- }
475
- function checkWorkspaceBindingSourcesIndex(projectDir, bindingSources) {
476
- const indexRelativePath = [path.join("src", "bindings", "index.ts"), path.join("src", "bindings", "index.js")].find((relativePath) => fs.existsSync(path.join(projectDir, relativePath)));
477
- if (!indexRelativePath) {
478
- return createDoctorCheck("Binding sources index", "fail", "Missing src/bindings/index.ts or src/bindings/index.js");
479
- }
480
- const indexPath = path.join(projectDir, indexRelativePath);
481
- const source = fs.readFileSync(indexPath, "utf8");
482
- const missingImports = bindingSources.filter((bindingSource) => !source.includes(`./${bindingSource.slug}/editor`));
483
- return createDoctorCheck("Binding sources index", missingImports.length === 0 ? "pass" : "fail", missingImports.length === 0
484
- ? "Binding source editor registrations are aggregated"
485
- : `Missing editor imports for: ${missingImports.map((entry) => entry.slug).join(", ")}`);
486
- }
487
- function checkWorkspaceBindingTarget(projectDir, workspace, registeredBlockSlugs, bindingSource) {
488
- const hasBlock = bindingSource.block !== undefined;
489
- const hasAttribute = bindingSource.attribute !== undefined;
490
- if (!hasBlock && !hasAttribute) {
491
- return undefined;
492
- }
493
- if (!bindingSource.block || !bindingSource.attribute) {
494
- return createDoctorCheck(`Binding target ${bindingSource.slug}`, "fail", "Binding target entries must include both block and attribute.");
495
- }
496
- if (!registeredBlockSlugs.has(bindingSource.block)) {
497
- return createDoctorCheck(`Binding target ${bindingSource.slug}`, "fail", `Binding target references unknown block "${bindingSource.block}".`);
498
- }
499
- const blockJsonRelativePath = path.join("src", "blocks", bindingSource.block, "block.json");
500
- const blockJsonPath = path.join(projectDir, blockJsonRelativePath);
501
- const issues = [];
502
- try {
503
- const blockJson = parseScaffoldBlockMetadata(JSON.parse(fs.readFileSync(blockJsonPath, "utf8")));
504
- const attributes = blockJson.attributes;
505
- if (!attributes || typeof attributes !== "object" || Array.isArray(attributes)) {
506
- issues.push(`${blockJsonRelativePath} must define an attributes object`);
507
- }
508
- else {
509
- const attributeConfig = attributes[bindingSource.attribute];
510
- if (!attributeConfig ||
511
- typeof attributeConfig !== "object" ||
512
- Array.isArray(attributeConfig)) {
513
- issues.push(`${blockJsonRelativePath} must declare attribute "${bindingSource.attribute}"`);
514
- }
515
- }
516
- }
517
- catch (error) {
518
- issues.push(error instanceof Error
519
- ? `Unable to read ${blockJsonRelativePath}: ${error.message}`
520
- : `Unable to read ${blockJsonRelativePath}.`);
521
- }
522
- const serverPath = path.join(projectDir, bindingSource.serverFile);
523
- if (fs.existsSync(serverPath)) {
524
- const serverSource = fs.readFileSync(serverPath, "utf8");
525
- const supportedAttributesFilter = `block_bindings_supported_attributes_${workspace.workspace.namespace}/${bindingSource.block}`;
526
- if (!serverSource.includes(supportedAttributesFilter)) {
527
- issues.push(`${bindingSource.serverFile} must register ${supportedAttributesFilter}`);
528
- }
529
- if (!new RegExp(`'${escapeRegex(bindingSource.attribute)}'`, "u").test(serverSource)) {
530
- issues.push(`${bindingSource.serverFile} must expose attribute "${bindingSource.attribute}"`);
531
- }
532
- }
533
- else {
534
- issues.push(`Missing ${bindingSource.serverFile}`);
535
- }
536
- const editorPath = path.join(projectDir, bindingSource.editorFile);
537
- if (fs.existsSync(editorPath)) {
538
- const editorSource = fs.readFileSync(editorPath, "utf8");
539
- const blockName = `${workspace.workspace.namespace}/${bindingSource.block}`;
540
- const bindingSourceTargetMatch = editorSource.match(/export\s+const\s+BINDING_SOURCE_TARGET\s*=\s*\{([\s\S]*?)\}\s+as\s+const\s*;/u);
541
- if (!bindingSourceTargetMatch) {
542
- issues.push(`${bindingSource.editorFile} must export BINDING_SOURCE_TARGET`);
543
- }
544
- else {
545
- const targetSource = bindingSourceTargetMatch[1] ?? "";
546
- const attributePattern = new RegExp(`\\battribute\\s*:\\s*["']${escapeRegex(bindingSource.attribute)}["']`, "u");
547
- const blockPattern = new RegExp(`\\bblock\\s*:\\s*["']${escapeRegex(blockName)}["']`, "u");
548
- if (!attributePattern.test(targetSource)) {
549
- issues.push(`${bindingSource.editorFile} must document target attribute "${bindingSource.attribute}"`);
550
- }
551
- if (!blockPattern.test(targetSource)) {
552
- issues.push(`${bindingSource.editorFile} must document target block "${blockName}"`);
553
- }
554
- }
555
- }
556
- else {
557
- issues.push(`Missing ${bindingSource.editorFile}`);
558
- }
559
- return createDoctorCheck(`Binding target ${bindingSource.slug}`, issues.length === 0 ? "pass" : "fail", issues.length === 0
560
- ? `${bindingSource.block}.${bindingSource.attribute} is declared and supported`
561
- : issues.join("; "));
562
- }
563
- function getWorkspaceRestResourceRequiredFiles(restResource) {
564
- const schemaNames = new Set();
565
- if (restResource.methods.includes("list")) {
566
- schemaNames.add("list-query");
567
- schemaNames.add("list-response");
568
- }
569
- if (restResource.methods.includes("read")) {
570
- schemaNames.add("read-query");
571
- schemaNames.add("read-response");
572
- }
573
- if (restResource.methods.includes("create")) {
574
- schemaNames.add("create-request");
575
- schemaNames.add("create-response");
576
- }
577
- if (restResource.methods.includes("update")) {
578
- schemaNames.add("update-query");
579
- schemaNames.add("update-request");
580
- schemaNames.add("update-response");
581
- }
582
- if (restResource.methods.includes("delete")) {
583
- schemaNames.add("delete-query");
584
- schemaNames.add("delete-response");
585
- }
586
- return Array.from(new Set([
587
- restResource.apiFile,
588
- ...Array.from(schemaNames, (schemaName) => path.join(path.dirname(restResource.typesFile), "api-schemas", `${schemaName}.schema.json`)),
589
- restResource.clientFile,
590
- restResource.dataFile,
591
- restResource.openApiFile,
592
- restResource.phpFile,
593
- restResource.typesFile,
594
- restResource.validatorsFile,
595
- ]));
596
- }
597
- function checkWorkspaceRestResourceConfig(restResource) {
598
- const hasNamespace = REST_RESOURCE_NAMESPACE_PATTERN.test(restResource.namespace);
599
- const hasMethods = restResource.methods.length > 0 &&
600
- restResource.methods.every((method) => REST_RESOURCE_METHOD_IDS.includes(method));
601
- return createDoctorCheck(`REST resource config ${restResource.slug}`, hasNamespace && hasMethods ? "pass" : "fail", hasNamespace && hasMethods
602
- ? `REST resource namespace ${restResource.namespace} with methods ${restResource.methods.join(", ")}`
603
- : "REST resource namespace or methods are invalid");
604
- }
605
- function checkWorkspaceRestResourceBootstrap(projectDir, packageName, phpPrefix) {
606
- const packageBaseName = packageName.split("/").pop() ?? packageName;
607
- const bootstrapPath = path.join(projectDir, `${packageBaseName}.php`);
608
- if (!fs.existsSync(bootstrapPath)) {
609
- return createDoctorCheck("REST resource bootstrap", "fail", `Missing ${path.basename(bootstrapPath)}`);
610
- }
611
- const source = fs.readFileSync(bootstrapPath, "utf8");
612
- const registerFunctionName = `${phpPrefix}_register_rest_resources`;
613
- const registerHook = `add_action( 'init', '${registerFunctionName}', 20 );`;
614
- const hasServerGlob = source.includes(WORKSPACE_REST_RESOURCE_GLOB);
615
- const hasRegisterHook = source.includes(registerHook);
616
- return createDoctorCheck("REST resource bootstrap", hasServerGlob && hasRegisterHook ? "pass" : "fail", hasServerGlob && hasRegisterHook
617
- ? "REST resource PHP loader hook is present"
618
- : "Missing REST resource PHP require glob or init hook");
619
- }
620
- function getWorkspaceAbilityRequiredFiles(ability) {
621
- return Array.from(new Set([
622
- ability.clientFile,
623
- ability.configFile,
624
- ability.dataFile,
625
- ability.inputSchemaFile,
626
- ability.outputSchemaFile,
627
- ability.phpFile,
628
- ability.typesFile,
629
- ]));
630
- }
631
- function checkWorkspaceAbilityConfig(projectDir, ability) {
632
- const configPath = path.join(projectDir, ability.configFile);
633
- if (!fs.existsSync(configPath)) {
634
- return createDoctorCheck(`Ability config ${ability.slug}`, "fail", `Missing ${ability.configFile}`);
635
- }
636
- try {
637
- const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
638
- const abilityId = typeof config.abilityId === "string" ? config.abilityId.trim() : "";
639
- const categorySlug = typeof config.category?.slug === "string"
640
- ? config.category.slug.trim()
641
- : "";
642
- const hasValidAbilityId = /^[a-z0-9-]+\/[a-z0-9-]+$/u.test(abilityId);
643
- const hasValidCategorySlug = /^[a-z0-9-]+$/u.test(categorySlug);
644
- return createDoctorCheck(`Ability config ${ability.slug}`, hasValidAbilityId && hasValidCategorySlug ? "pass" : "fail", hasValidAbilityId && hasValidCategorySlug
645
- ? `Ability id ${abilityId} in category ${categorySlug} is valid`
646
- : "Ability config must define a valid abilityId (`namespace/ability-name`) and category.slug.");
647
- }
648
- catch (error) {
649
- return createDoctorCheck(`Ability config ${ability.slug}`, "fail", error instanceof Error ? error.message : String(error));
650
- }
651
- }
652
- function checkWorkspaceAbilityBootstrap(projectDir, packageName, phpPrefix) {
653
- const packageBaseName = packageName.split("/").pop() ?? packageName;
654
- const bootstrapPath = path.join(projectDir, `${packageBaseName}.php`);
655
- if (!fs.existsSync(bootstrapPath)) {
656
- return createDoctorCheck("Ability bootstrap", "fail", `Missing ${path.basename(bootstrapPath)}`);
657
- }
658
- const source = fs.readFileSync(bootstrapPath, "utf8");
659
- const loadFunctionName = `${phpPrefix}_load_workflow_abilities`;
660
- const enqueueFunctionName = `${phpPrefix}_enqueue_workflow_abilities`;
661
- const loadHook = `add_action( 'plugins_loaded', '${loadFunctionName}' );`;
662
- const adminEnqueueHook = `add_action( 'admin_enqueue_scripts', '${enqueueFunctionName}' );`;
663
- const editorEnqueueHook = `add_action( 'enqueue_block_editor_assets', '${enqueueFunctionName}' );`;
664
- const hasLoaderHook = source.includes(loadHook);
665
- const hasAdminEnqueueHook = source.includes(adminEnqueueHook);
666
- const hasEditorEnqueueHook = source.includes(editorEnqueueHook);
667
- const hasServerGlob = source.includes(WORKSPACE_ABILITY_GLOB);
668
- const hasEditorScript = source.includes(WORKSPACE_ABILITY_EDITOR_SCRIPT);
669
- const hasEditorAsset = source.includes(WORKSPACE_ABILITY_EDITOR_ASSET);
670
- const hasScriptModuleEnqueue = source.includes("wp_enqueue_script_module");
671
- return createDoctorCheck("Ability bootstrap", hasLoaderHook &&
672
- hasAdminEnqueueHook &&
673
- hasEditorEnqueueHook &&
674
- hasServerGlob &&
675
- hasEditorScript &&
676
- hasEditorAsset &&
677
- hasScriptModuleEnqueue
678
- ? "pass"
679
- : "fail", hasLoaderHook &&
680
- hasAdminEnqueueHook &&
681
- hasEditorEnqueueHook &&
682
- hasServerGlob &&
683
- hasEditorScript &&
684
- hasEditorAsset &&
685
- hasScriptModuleEnqueue
686
- ? "Ability loader and admin/editor script-module bootstrap hooks are present"
687
- : "Missing ability loader hook, script-module enqueue, or build/abilities asset references");
688
- }
689
- function checkWorkspaceAbilityIndex(projectDir, abilities) {
690
- const indexRelativePath = [
691
- path.join("src", "abilities", "index.ts"),
692
- path.join("src", "abilities", "index.js"),
693
- ].find((relativePath) => fs.existsSync(path.join(projectDir, relativePath)));
694
- if (!indexRelativePath) {
695
- return createDoctorCheck("Abilities index", "fail", "Missing src/abilities/index.ts or src/abilities/index.js");
696
- }
697
- const indexPath = path.join(projectDir, indexRelativePath);
698
- const source = fs.readFileSync(indexPath, "utf8");
699
- const missingExports = abilities.filter((ability) => {
700
- const exportPattern = new RegExp(`^\\s*export\\s+(?:\\*\\s+from|\\{[^}]+\\}\\s+from)\\s+['"\`]\\./${escapeRegex(ability.slug)}\\/client['"\`]`, "mu");
701
- return !exportPattern.test(source);
702
- });
703
- return createDoctorCheck("Abilities index", missingExports.length === 0 ? "pass" : "fail", missingExports.length === 0
704
- ? "Ability client helpers are aggregated"
705
- : `Missing ability exports for: ${missingExports.map((entry) => entry.slug).join(", ")}`);
706
- }
707
- function getWorkspaceAiFeatureRequiredFiles(aiFeature) {
708
- return Array.from(new Set([
709
- aiFeature.aiSchemaFile,
710
- aiFeature.apiFile,
711
- path.join(path.dirname(aiFeature.typesFile), "api-schemas", "feature-request.schema.json"),
712
- path.join(path.dirname(aiFeature.typesFile), "api-schemas", "feature-response.schema.json"),
713
- path.join(path.dirname(aiFeature.typesFile), "api-schemas", "feature-result.schema.json"),
714
- aiFeature.clientFile,
715
- aiFeature.dataFile,
716
- aiFeature.openApiFile,
717
- aiFeature.phpFile,
718
- aiFeature.typesFile,
719
- aiFeature.validatorsFile,
720
- ]));
721
- }
722
- function checkWorkspaceAiFeatureConfig(aiFeature) {
723
- const hasNamespace = REST_RESOURCE_NAMESPACE_PATTERN.test(aiFeature.namespace);
724
- return createDoctorCheck(`AI feature config ${aiFeature.slug}`, hasNamespace ? "pass" : "fail", hasNamespace
725
- ? `AI feature namespace ${aiFeature.namespace} is valid`
726
- : "AI feature namespace is invalid");
727
- }
728
- function checkWorkspaceAiFeatureBootstrap(projectDir, packageName, phpPrefix) {
729
- const packageBaseName = packageName.split("/").pop() ?? packageName;
730
- const bootstrapPath = path.join(projectDir, `${packageBaseName}.php`);
731
- if (!fs.existsSync(bootstrapPath)) {
732
- return createDoctorCheck("AI feature bootstrap", "fail", `Missing ${path.basename(bootstrapPath)}`);
733
- }
734
- const source = fs.readFileSync(bootstrapPath, "utf8");
735
- const registerFunctionName = `${phpPrefix}_register_ai_features`;
736
- const registerHook = `add_action( 'init', '${registerFunctionName}', 20 );`;
737
- const hasServerGlob = source.includes(WORKSPACE_AI_FEATURE_GLOB);
738
- const hasRegisterHook = source.includes(registerHook);
739
- return createDoctorCheck("AI feature bootstrap", hasServerGlob && hasRegisterHook ? "pass" : "fail", hasServerGlob && hasRegisterHook
740
- ? "AI feature PHP loader hook is present"
741
- : "Missing AI feature PHP require glob or init hook");
742
- }
743
- function getWorkspaceEditorPluginRequiredFiles(editorPlugin) {
744
- const editorPluginDir = path.join("src", "editor-plugins", editorPlugin.slug);
745
- const surfaceFile = editorPlugin.slot === "PluginSidebar"
746
- ? path.join(editorPluginDir, "Sidebar.tsx")
747
- : path.join(editorPluginDir, "Surface.tsx");
748
- return Array.from(new Set([
749
- editorPlugin.file,
750
- surfaceFile,
751
- path.join(editorPluginDir, "data.ts"),
752
- path.join(editorPluginDir, "types.ts"),
753
- path.join(editorPluginDir, "style.scss"),
754
- ]));
755
- }
756
- function checkWorkspaceEditorPluginConfig(editorPlugin) {
757
- const normalizedSlot = resolveEditorPluginSlotAlias(editorPlugin.slot);
758
- const isValidSlot = Boolean(normalizedSlot);
759
- return createDoctorCheck(`Editor plugin config ${editorPlugin.slug}`, isValidSlot ? "pass" : "fail", isValidSlot
760
- ? `Editor plugin slot ${editorPlugin.slot} is supported as ${normalizedSlot}`
761
- : `Unsupported editor plugin slot "${editorPlugin.slot}". Expected one of: ${EDITOR_PLUGIN_SLOT_IDS.join(", ")} or legacy aliases PluginSidebar, PluginDocumentSettingPanel.`);
762
- }
763
- function checkWorkspaceEditorPluginBootstrap(projectDir, packageName, phpPrefix) {
764
- const packageBaseName = packageName.split("/").pop() ?? packageName;
765
- const bootstrapPath = path.join(projectDir, `${packageBaseName}.php`);
766
- if (!fs.existsSync(bootstrapPath)) {
767
- return createDoctorCheck("Editor plugin bootstrap", "fail", `Missing ${path.basename(bootstrapPath)}`);
768
- }
769
- const source = fs.readFileSync(bootstrapPath, "utf8");
770
- const enqueueFunctionName = `${phpPrefix}_enqueue_editor_plugins_editor`;
771
- const enqueueHook = `add_action( 'enqueue_block_editor_assets', '${enqueueFunctionName}' );`;
772
- const hasEditorEnqueueHook = source.includes(enqueueHook);
773
- const hasEditorScript = source.includes(WORKSPACE_EDITOR_PLUGIN_EDITOR_SCRIPT);
774
- const hasEditorAsset = source.includes(WORKSPACE_EDITOR_PLUGIN_EDITOR_ASSET);
775
- const hasEditorStyle = source.includes(WORKSPACE_EDITOR_PLUGIN_EDITOR_STYLE);
776
- return createDoctorCheck("Editor plugin bootstrap", hasEditorEnqueueHook && hasEditorScript && hasEditorAsset && hasEditorStyle ? "pass" : "fail", hasEditorEnqueueHook && hasEditorScript && hasEditorAsset && hasEditorStyle
777
- ? "Editor plugin enqueue hook is present"
778
- : "Missing editor plugin enqueue hook or build/editor-plugins script/style asset references");
779
- }
780
- function checkWorkspaceEditorPluginIndex(projectDir, editorPlugins) {
781
- const indexRelativePath = [
782
- path.join("src", "editor-plugins", "index.ts"),
783
- path.join("src", "editor-plugins", "index.js"),
784
- ].find((relativePath) => fs.existsSync(path.join(projectDir, relativePath)));
785
- if (!indexRelativePath) {
786
- return createDoctorCheck("Editor plugins index", "fail", "Missing src/editor-plugins/index.ts or src/editor-plugins/index.js");
787
- }
788
- const indexPath = path.join(projectDir, indexRelativePath);
789
- const source = fs.readFileSync(indexPath, "utf8");
790
- const missingImports = editorPlugins.filter((editorPlugin) => {
791
- const importPattern = new RegExp(`['"\`]\\./${escapeRegex(editorPlugin.slug)}(?:/[^'"\`]*)?['"\`]`, "u");
792
- return !importPattern.test(source);
793
- });
794
- return createDoctorCheck("Editor plugins index", missingImports.length === 0 ? "pass" : "fail", missingImports.length === 0
795
- ? "Editor plugin registrations are aggregated"
796
- : `Missing editor plugin imports for: ${missingImports
797
- .map((entry) => entry.slug)
798
- .join(", ")}`);
799
- }
800
- function getWorkspaceAdminViewRequiredFiles(adminView) {
801
- const adminViewDir = path.join("src", "admin-views", adminView.slug);
802
- return Array.from(new Set([
803
- adminView.file,
804
- adminView.phpFile,
805
- path.join(adminViewDir, "Screen.tsx"),
806
- path.join(adminViewDir, "config.ts"),
807
- path.join(adminViewDir, "data.ts"),
808
- path.join(adminViewDir, "style.scss"),
809
- path.join(adminViewDir, "types.ts"),
810
- ]));
811
- }
812
- function checkWorkspaceAdminViewConfig(adminView, inventory) {
813
- if (adminView.source === undefined) {
814
- return createDoctorCheck(`Admin view config ${adminView.slug}`, "pass", "Admin view uses a replaceable local fetcher");
815
- }
816
- const source = adminView.source.trim();
817
- const restSourceMatch = /^rest-resource:([a-z][a-z0-9-]*)$/u.exec(source);
818
- const coreDataSourceMatch = /^core-data:(postType|taxonomy)\/([a-z0-9][a-z0-9_-]*)$/u.exec(source);
819
- const restResourceSlug = restSourceMatch?.[1];
820
- const restResource = restResourceSlug
821
- ? inventory.restResources.find((entry) => entry.slug === restResourceSlug)
822
- : undefined;
823
- const isValid = Boolean(restResource?.methods.includes("list")) || Boolean(coreDataSourceMatch);
824
- return createDoctorCheck(`Admin view config ${adminView.slug}`, isValid ? "pass" : "fail", isValid
825
- ? `Admin view source ${source} is list-capable`
826
- : "Admin view source must use rest-resource:<slug> with a list-capable REST resource or core-data:<postType|taxonomy>/<name>");
827
- }
828
- function checkWorkspaceAdminViewBootstrap(projectDir, packageName, phpPrefix) {
829
- const packageBaseName = packageName.split("/").pop() ?? packageName;
830
- const bootstrapPath = path.join(projectDir, `${packageBaseName}.php`);
831
- if (!fs.existsSync(bootstrapPath)) {
832
- return createDoctorCheck("Admin view bootstrap", "fail", `Missing ${path.basename(bootstrapPath)}`);
833
- }
834
- const source = fs.readFileSync(bootstrapPath, "utf8");
835
- const loadFunctionName = `${phpPrefix}_load_admin_views`;
836
- const loadHook = `add_action( 'plugins_loaded', '${loadFunctionName}' );`;
837
- const hasLoaderHook = source.includes(loadHook);
838
- const hasServerGlob = source.includes(WORKSPACE_ADMIN_VIEW_GLOB);
839
- return createDoctorCheck("Admin view bootstrap", hasLoaderHook && hasServerGlob ? "pass" : "fail", hasLoaderHook && hasServerGlob
840
- ? "Admin view PHP loader hook is present"
841
- : "Missing admin view PHP require glob or plugins_loaded hook");
842
- }
843
- function checkWorkspaceAdminViewIndex(projectDir, adminViews) {
844
- const indexRelativePath = [
845
- path.join("src", "admin-views", "index.ts"),
846
- path.join("src", "admin-views", "index.js"),
847
- ].find((relativePath) => fs.existsSync(path.join(projectDir, relativePath)));
848
- if (!indexRelativePath) {
849
- return createDoctorCheck("Admin views index", "fail", "Missing src/admin-views/index.ts or src/admin-views/index.js");
850
- }
851
- const indexPath = path.join(projectDir, indexRelativePath);
852
- const source = fs.readFileSync(indexPath, "utf8");
853
- const missingImports = adminViews.filter((adminView) => {
854
- const importPattern = new RegExp(`['"\`]\\./${escapeRegex(adminView.slug)}(?:/[^'"\`]*)?['"\`]`, "u");
855
- return !importPattern.test(source);
856
- });
857
- return createDoctorCheck("Admin views index", missingImports.length === 0 ? "pass" : "fail", missingImports.length === 0
858
- ? "Admin view registrations are aggregated"
859
- : `Missing admin view imports for: ${missingImports
860
- .map((entry) => entry.slug)
861
- .join(", ")}`);
862
- }
863
- function checkWorkspaceAdminViewPhp(projectDir, adminView) {
864
- const phpPath = path.join(projectDir, adminView.phpFile);
865
- if (!fs.existsSync(phpPath)) {
866
- return createDoctorCheck(`Admin view PHP ${adminView.slug}`, "fail", `Missing ${adminView.phpFile}`);
867
- }
868
- const source = fs.readFileSync(phpPath, "utf8");
869
- const hasAdminMenu = source.includes("add_submenu_page");
870
- const hasAdminEnqueue = source.includes("admin_enqueue_scripts");
871
- const hasScript = source.includes(WORKSPACE_ADMIN_VIEW_SCRIPT);
872
- const hasAsset = source.includes(WORKSPACE_ADMIN_VIEW_ASSET);
873
- const hasStyle = source.includes(WORKSPACE_ADMIN_VIEW_STYLE);
874
- const hasComponentsStyleDependency = source.includes("'wp-components'");
875
- return createDoctorCheck(`Admin view PHP ${adminView.slug}`, hasAdminMenu &&
876
- hasAdminEnqueue &&
877
- hasScript &&
878
- hasAsset &&
879
- hasStyle &&
880
- hasComponentsStyleDependency
881
- ? "pass"
882
- : "fail", hasAdminMenu &&
883
- hasAdminEnqueue &&
884
- hasScript &&
885
- hasAsset &&
886
- hasStyle &&
887
- hasComponentsStyleDependency
888
- ? "Admin menu, script, style, and wp-components style dependency are wired"
889
- : "Missing admin menu, enqueue hook, build/admin-views asset reference, or wp-components style dependency");
890
- }
891
- function checkVariationEntrypoint(projectDir, blockSlug) {
892
- const entryPath = path.join(projectDir, "src", "blocks", blockSlug, "index.tsx");
893
- if (!fs.existsSync(entryPath)) {
894
- return createDoctorCheck(`Variation entrypoint ${blockSlug}`, "fail", `Missing ${path.relative(projectDir, entryPath)}`);
895
- }
896
- const source = fs.readFileSync(entryPath, "utf8");
897
- const hasImport = hasUncommentedPattern(source, WORKSPACE_VARIATIONS_IMPORT_PATTERN);
898
- const hasCall = hasExecutablePattern(source, WORKSPACE_VARIATIONS_CALL_PATTERN);
899
- return createDoctorCheck(`Variation entrypoint ${blockSlug}`, hasImport && hasCall ? "pass" : "fail", hasImport && hasCall
900
- ? "Variations registration hook is present"
901
- : "Missing ./variations import or registerWorkspaceVariations() call");
902
- }
903
- function checkBlockStyleEntrypoint(projectDir, blockSlug) {
904
- const entryPath = path.join(projectDir, "src", "blocks", blockSlug, "index.tsx");
905
- if (!fs.existsSync(entryPath)) {
906
- return createDoctorCheck(`Block style entrypoint ${blockSlug}`, "fail", `Missing ${path.relative(projectDir, entryPath)}`);
907
- }
908
- const source = fs.readFileSync(entryPath, "utf8");
909
- const hasImport = hasUncommentedPattern(source, WORKSPACE_BLOCK_STYLES_IMPORT_PATTERN);
910
- const hasCall = hasExecutablePattern(source, WORKSPACE_BLOCK_STYLES_CALL_PATTERN);
911
- return createDoctorCheck(`Block style entrypoint ${blockSlug}`, hasImport && hasCall ? "pass" : "fail", hasImport && hasCall
912
- ? "Block style registration hook is present"
913
- : "Missing ./styles import or registerWorkspaceBlockStyles() call");
914
- }
915
- function checkBlockTransformEntrypoint(projectDir, blockSlug) {
916
- const entryPath = path.join(projectDir, "src", "blocks", blockSlug, "index.tsx");
917
- if (!fs.existsSync(entryPath)) {
918
- return createDoctorCheck(`Block transform entrypoint ${blockSlug}`, "fail", `Missing ${path.relative(projectDir, entryPath)}`);
919
- }
920
- const source = fs.readFileSync(entryPath, "utf8");
921
- const hasImport = hasUncommentedPattern(source, WORKSPACE_BLOCK_TRANSFORMS_IMPORT_PATTERN);
922
- const hasCall = hasExecutablePattern(source, WORKSPACE_BLOCK_TRANSFORMS_CALL_PATTERN);
923
- return createDoctorCheck(`Block transform entrypoint ${blockSlug}`, hasImport && hasCall ? "pass" : "fail", hasImport && hasCall
924
- ? "Block transform registration hook is present"
925
- : "Missing ./transforms import or applyWorkspaceBlockTransforms(registration.settings) call");
926
- }
927
- function checkBlockTransformConfig(workspace, transform) {
928
- const expectedTo = `${workspace.workspace.namespace}/${transform.block}`;
929
- const issues = [];
930
- if (!WORKSPACE_FULL_BLOCK_NAME_PATTERN.test(transform.from)) {
931
- issues.push("from must use full namespace/block format");
932
- }
933
- if (transform.to !== expectedTo) {
934
- issues.push(`to must equal "${expectedTo}" for workspace block "${transform.block}"`);
935
- }
936
- return createDoctorCheck(`Block transform config ${transform.block}/${transform.slug}`, issues.length === 0 ? "pass" : "fail", issues.length === 0
937
- ? `${transform.from} transforms into ${transform.to}`
938
- : issues.join("; "));
939
- }
940
- function checkMigrationWorkspaceHint(workspace, packageJson) {
941
- const hasMigrationScript = typeof packageJson.scripts?.["migration:doctor"] === "string";
942
- const migrationConfigRelativePath = path.join("src", "migrations", "config.ts");
943
- const hasMigrationConfig = fs.existsSync(path.join(workspace.projectDir, migrationConfigRelativePath));
944
- if (!hasMigrationScript && !hasMigrationConfig) {
945
- return null;
946
- }
947
- return createDoctorCheck("Migration workspace", hasMigrationConfig ? "pass" : "fail", hasMigrationConfig
948
- ? "Run `wp-typia migrate doctor --all` for migration target, snapshot, fixture, and generated artifact checks"
949
- : `Missing ${migrationConfigRelativePath} for the configured migration workspace`);
10
+ `${inventory.blocks.length} block(s)`,
11
+ `${inventory.variations.length} variation(s)`,
12
+ `${inventory.blockStyles.length} block style(s)`,
13
+ `${inventory.blockTransforms.length} block transform(s)`,
14
+ `${inventory.patterns.length} pattern(s)`,
15
+ `${inventory.bindingSources.length} binding source(s)`,
16
+ `${inventory.restResources.length} REST resource(s)`,
17
+ `${inventory.abilities.length} ability scaffold(s)`,
18
+ `${inventory.aiFeatures.length} AI feature(s)`,
19
+ `${inventory.editorPlugins.length} editor plugin(s)`,
20
+ `${inventory.adminViews.length} admin view(s)`,
21
+ ].join(", ");
950
22
  }
951
23
  /**
952
24
  * Collect workspace-scoped doctor checks for the given working directory.
@@ -996,123 +68,14 @@ export function getWorkspaceDoctorChecks(cwd) {
996
68
  checks.push(createDoctorCheck("Workspace package metadata", "fail", error instanceof Error ? error.message : String(error)));
997
69
  return checks;
998
70
  }
999
- checks.push(checkWorkspacePackageMetadata(workspace, workspacePackageJson));
71
+ checks.push(getWorkspacePackageMetadataCheck(workspace, workspacePackageJson));
1000
72
  try {
1001
73
  const inventory = readWorkspaceInventory(workspace.projectDir);
1002
- checks.push(createDoctorCheck("Workspace inventory", "pass", `${inventory.blocks.length} block(s), ${inventory.variations.length} variation(s), ${inventory.blockStyles.length} block style(s), ${inventory.blockTransforms.length} block transform(s), ${inventory.patterns.length} pattern(s), ${inventory.bindingSources.length} binding source(s), ${inventory.restResources.length} REST resource(s), ${inventory.abilities.length} ability scaffold(s), ${inventory.aiFeatures.length} AI feature(s), ${inventory.editorPlugins.length} editor plugin(s), ${inventory.adminViews.length} admin view(s)`));
1003
- for (const block of inventory.blocks) {
1004
- checks.push(checkExistingFiles(workspace.projectDir, `Block ${block.slug}`, getWorkspaceBlockRequiredFiles(block)));
1005
- checks.push(checkWorkspaceBlockMetadata(workspace.projectDir, workspace, block));
1006
- checks.push(checkWorkspaceBlockHooks(workspace.projectDir, block.slug));
1007
- checks.push(checkWorkspaceBlockCollectionImport(workspace.projectDir, block.slug));
1008
- checks.push(...checkWorkspaceBlockIframeCompatibility(workspace.projectDir, block.slug));
1009
- }
1010
- const registeredBlockSlugs = new Set(inventory.blocks.map((block) => block.slug));
1011
- const variationTargetBlocks = new Set();
1012
- for (const variation of inventory.variations) {
1013
- if (!registeredBlockSlugs.has(variation.block)) {
1014
- checks.push(createDoctorCheck(`Variation ${variation.block}/${variation.slug}`, "fail", `Variation references unknown block "${variation.block}"`));
1015
- continue;
1016
- }
1017
- variationTargetBlocks.add(variation.block);
1018
- checks.push(checkExistingFiles(workspace.projectDir, `Variation ${variation.block}/${variation.slug}`, [variation.file]));
1019
- }
1020
- for (const blockSlug of variationTargetBlocks) {
1021
- checks.push(checkVariationEntrypoint(workspace.projectDir, blockSlug));
1022
- }
1023
- const blockStyleTargetBlocks = new Set();
1024
- for (const blockStyle of inventory.blockStyles) {
1025
- if (!registeredBlockSlugs.has(blockStyle.block)) {
1026
- checks.push(createDoctorCheck(`Block style ${blockStyle.block}/${blockStyle.slug}`, "fail", `Block style references unknown block "${blockStyle.block}"`));
1027
- continue;
1028
- }
1029
- blockStyleTargetBlocks.add(blockStyle.block);
1030
- checks.push(checkExistingFiles(workspace.projectDir, `Block style ${blockStyle.block}/${blockStyle.slug}`, [blockStyle.file]));
1031
- }
1032
- for (const blockSlug of blockStyleTargetBlocks) {
1033
- checks.push(checkExistingFiles(workspace.projectDir, `Block style registry ${blockSlug}`, [
1034
- path.join("src", "blocks", blockSlug, "styles", "index.ts"),
1035
- ]));
1036
- checks.push(checkBlockStyleEntrypoint(workspace.projectDir, blockSlug));
1037
- }
1038
- const blockTransformTargetBlocks = new Set();
1039
- for (const blockTransform of inventory.blockTransforms) {
1040
- if (!registeredBlockSlugs.has(blockTransform.block)) {
1041
- checks.push(createDoctorCheck(`Block transform ${blockTransform.block}/${blockTransform.slug}`, "fail", `Block transform references unknown block "${blockTransform.block}"`));
1042
- continue;
1043
- }
1044
- blockTransformTargetBlocks.add(blockTransform.block);
1045
- checks.push(checkBlockTransformConfig(workspace, blockTransform));
1046
- checks.push(checkExistingFiles(workspace.projectDir, `Block transform ${blockTransform.block}/${blockTransform.slug}`, [blockTransform.file]));
1047
- }
1048
- for (const blockSlug of blockTransformTargetBlocks) {
1049
- checks.push(checkExistingFiles(workspace.projectDir, `Block transform registry ${blockSlug}`, [
1050
- path.join("src", "blocks", blockSlug, "transforms", "index.ts"),
1051
- ]));
1052
- checks.push(checkBlockTransformEntrypoint(workspace.projectDir, blockSlug));
1053
- }
1054
- const shouldCheckPatternBootstrap = inventory.patterns.length > 0 ||
1055
- fs.existsSync(path.join(workspace.projectDir, "src", "patterns"));
1056
- if (shouldCheckPatternBootstrap) {
1057
- checks.push(checkWorkspacePatternBootstrap(workspace.projectDir, workspace.packageName));
1058
- }
1059
- for (const pattern of inventory.patterns) {
1060
- checks.push(checkExistingFiles(workspace.projectDir, `Pattern ${pattern.slug}`, [pattern.file]));
1061
- }
1062
- if (inventory.bindingSources.length > 0) {
1063
- checks.push(checkWorkspaceBindingBootstrap(workspace.projectDir, workspace.packageName));
1064
- checks.push(checkWorkspaceBindingSourcesIndex(workspace.projectDir, inventory.bindingSources));
1065
- }
1066
- for (const bindingSource of inventory.bindingSources) {
1067
- checks.push(checkExistingFiles(workspace.projectDir, `Binding source ${bindingSource.slug}`, [
1068
- bindingSource.serverFile,
1069
- bindingSource.editorFile,
1070
- ]));
1071
- const bindingTargetCheck = checkWorkspaceBindingTarget(workspace.projectDir, workspace, registeredBlockSlugs, bindingSource);
1072
- if (bindingTargetCheck) {
1073
- checks.push(bindingTargetCheck);
1074
- }
1075
- }
1076
- if (inventory.restResources.length > 0) {
1077
- checks.push(checkWorkspaceRestResourceBootstrap(workspace.projectDir, workspace.packageName, workspace.workspace.phpPrefix));
1078
- }
1079
- for (const restResource of inventory.restResources) {
1080
- checks.push(checkWorkspaceRestResourceConfig(restResource));
1081
- checks.push(checkExistingFiles(workspace.projectDir, `REST resource ${restResource.slug}`, getWorkspaceRestResourceRequiredFiles(restResource)));
1082
- }
1083
- if (inventory.abilities.length > 0) {
1084
- checks.push(checkWorkspaceAbilityBootstrap(workspace.projectDir, workspace.packageName, workspace.workspace.phpPrefix));
1085
- checks.push(checkWorkspaceAbilityIndex(workspace.projectDir, inventory.abilities));
1086
- }
1087
- for (const ability of inventory.abilities) {
1088
- checks.push(checkWorkspaceAbilityConfig(workspace.projectDir, ability));
1089
- checks.push(checkExistingFiles(workspace.projectDir, `Ability ${ability.slug}`, getWorkspaceAbilityRequiredFiles(ability)));
1090
- }
1091
- if (inventory.aiFeatures.length > 0) {
1092
- checks.push(checkWorkspaceAiFeatureBootstrap(workspace.projectDir, workspace.packageName, workspace.workspace.phpPrefix));
1093
- }
1094
- for (const aiFeature of inventory.aiFeatures) {
1095
- checks.push(checkWorkspaceAiFeatureConfig(aiFeature));
1096
- checks.push(checkExistingFiles(workspace.projectDir, `AI feature ${aiFeature.slug}`, getWorkspaceAiFeatureRequiredFiles(aiFeature)));
1097
- }
1098
- if (inventory.editorPlugins.length > 0) {
1099
- checks.push(checkWorkspaceEditorPluginBootstrap(workspace.projectDir, workspace.packageName, workspace.workspace.phpPrefix));
1100
- checks.push(checkWorkspaceEditorPluginIndex(workspace.projectDir, inventory.editorPlugins));
1101
- }
1102
- for (const editorPlugin of inventory.editorPlugins) {
1103
- checks.push(checkExistingFiles(workspace.projectDir, `Editor plugin ${editorPlugin.slug}`, getWorkspaceEditorPluginRequiredFiles(editorPlugin)));
1104
- checks.push(checkWorkspaceEditorPluginConfig(editorPlugin));
1105
- }
1106
- if (inventory.adminViews.length > 0) {
1107
- checks.push(checkWorkspaceAdminViewBootstrap(workspace.projectDir, workspace.packageName, workspace.workspace.phpPrefix));
1108
- checks.push(checkWorkspaceAdminViewIndex(workspace.projectDir, inventory.adminViews));
1109
- }
1110
- for (const adminView of inventory.adminViews) {
1111
- checks.push(checkWorkspaceAdminViewConfig(adminView, inventory));
1112
- checks.push(checkExistingFiles(workspace.projectDir, `Admin view ${adminView.slug}`, getWorkspaceAdminViewRequiredFiles(adminView)));
1113
- checks.push(checkWorkspaceAdminViewPhp(workspace.projectDir, adminView));
1114
- }
1115
- const migrationWorkspaceCheck = checkMigrationWorkspaceHint(workspace, workspacePackageJson);
74
+ checks.push(createDoctorCheck("Workspace inventory", "pass", formatWorkspaceInventorySummary(inventory)));
75
+ checks.push(...getWorkspaceBlockDoctorChecks(workspace, inventory));
76
+ checks.push(...getWorkspaceBindingDoctorChecks(workspace, inventory));
77
+ checks.push(...getWorkspaceFeatureDoctorChecks(workspace, inventory));
78
+ const migrationWorkspaceCheck = getMigrationWorkspaceHintCheck(workspace, workspacePackageJson);
1116
79
  if (migrationWorkspaceCheck) {
1117
80
  checks.push(migrationWorkspaceCheck);
1118
81
  }