@wp-typia/project-tools 0.20.2 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/runtime/cli-add-shared.d.ts +73 -5
- package/dist/runtime/cli-add-shared.js +58 -11
- package/dist/runtime/cli-add-workspace-ability.js +11 -57
- package/dist/runtime/cli-add-workspace-admin-view.d.ts +23 -0
- package/dist/runtime/cli-add-workspace-admin-view.js +872 -0
- package/dist/runtime/cli-add-workspace-ai-anchors.js +2 -5
- package/dist/runtime/cli-add-workspace-ai-source-emitters.d.ts +0 -4
- package/dist/runtime/cli-add-workspace-ai-source-emitters.js +7 -17
- package/dist/runtime/cli-add-workspace-ai.js +4 -6
- package/dist/runtime/cli-add-workspace-assets.d.ts +13 -5
- package/dist/runtime/cli-add-workspace-assets.js +290 -106
- package/dist/runtime/cli-add-workspace-rest-anchors.js +2 -5
- package/dist/runtime/cli-add-workspace-rest-source-emitters.d.ts +0 -1
- package/dist/runtime/cli-add-workspace-rest-source-emitters.js +7 -14
- package/dist/runtime/cli-add-workspace-rest.js +4 -6
- package/dist/runtime/cli-add-workspace.d.ts +58 -1
- package/dist/runtime/cli-add-workspace.js +588 -18
- package/dist/runtime/cli-add.d.ts +1 -1
- package/dist/runtime/cli-add.js +1 -1
- package/dist/runtime/cli-core.d.ts +8 -5
- package/dist/runtime/cli-core.js +7 -4
- package/dist/runtime/cli-diagnostics.d.ts +83 -1
- package/dist/runtime/cli-diagnostics.js +85 -2
- package/dist/runtime/cli-doctor-workspace.js +552 -13
- package/dist/runtime/cli-doctor.d.ts +4 -2
- package/dist/runtime/cli-doctor.js +2 -1
- package/dist/runtime/cli-help.js +19 -9
- package/dist/runtime/cli-init.d.ts +67 -3
- package/dist/runtime/cli-init.js +603 -64
- package/dist/runtime/cli-validation.js +4 -3
- package/dist/runtime/index.d.ts +9 -4
- package/dist/runtime/index.js +7 -3
- package/dist/runtime/package-json-types.d.ts +12 -0
- package/dist/runtime/package-json-types.js +1 -0
- package/dist/runtime/package-versions.d.ts +17 -2
- package/dist/runtime/package-versions.js +46 -1
- package/dist/runtime/php-utils.d.ts +16 -0
- package/dist/runtime/php-utils.js +59 -0
- package/dist/runtime/scaffold-answer-resolution.js +7 -6
- package/dist/runtime/scaffold-apply-utils.d.ts +2 -3
- package/dist/runtime/scaffold-apply-utils.js +3 -43
- package/dist/runtime/template-source-cache.d.ts +112 -0
- package/dist/runtime/template-source-cache.js +434 -0
- package/dist/runtime/template-source-seeds.js +319 -53
- package/dist/runtime/workspace-inventory.d.ts +43 -2
- package/dist/runtime/workspace-inventory.js +138 -5
- package/package.json +2 -2
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parseScaffoldBlockMetadata } from "@wp-typia/block-runtime/blocks";
|
|
4
|
-
import { EDITOR_PLUGIN_SLOT_IDS, REST_RESOURCE_METHOD_IDS, REST_RESOURCE_NAMESPACE_PATTERN, } from "./cli-add-shared.js";
|
|
4
|
+
import { EDITOR_PLUGIN_SLOT_IDS, REST_RESOURCE_METHOD_IDS, REST_RESOURCE_NAMESPACE_PATTERN, resolveEditorPluginSlotAlias, } from "./cli-add-shared.js";
|
|
5
5
|
import { HOOKED_BLOCK_ANCHOR_PATTERN, HOOKED_BLOCK_POSITION_SET, } from "./hooked-blocks.js";
|
|
6
6
|
import { readWorkspaceInventory } from "./workspace-inventory.js";
|
|
7
7
|
import { getInvalidWorkspaceProjectReason, parseWorkspacePackageJson, WORKSPACE_TEMPLATE_PACKAGE, tryResolveWorkspaceProject, } from "./workspace-project.js";
|
|
8
|
+
import { escapeRegex } from "./php-utils.js";
|
|
8
9
|
const WORKSPACE_COLLECTION_IMPORT_LINE = "import '../../collection';";
|
|
9
10
|
const WORKSPACE_COLLECTION_IMPORT_PATTERN = /^\s*import\s+["']\.\.\/\.\.\/collection["']\s*;?\s*$/m;
|
|
10
11
|
const WORKSPACE_BINDING_SERVER_GLOB = "/src/bindings/*/server.php";
|
|
@@ -15,6 +16,10 @@ const WORKSPACE_ABILITY_GLOB = "/inc/abilities/*.php";
|
|
|
15
16
|
const WORKSPACE_ABILITY_EDITOR_SCRIPT = "build/abilities/index.js";
|
|
16
17
|
const WORKSPACE_ABILITY_EDITOR_ASSET = "build/abilities/index.asset.php";
|
|
17
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";
|
|
18
23
|
const WORKSPACE_EDITOR_PLUGIN_EDITOR_SCRIPT = "build/editor-plugins/index.js";
|
|
19
24
|
const WORKSPACE_EDITOR_PLUGIN_EDITOR_ASSET = "build/editor-plugins/index.asset.php";
|
|
20
25
|
const WORKSPACE_EDITOR_PLUGIN_EDITOR_STYLE = "build/editor-plugins/style-index.css";
|
|
@@ -25,14 +30,252 @@ const WORKSPACE_GENERATED_BLOCK_ARTIFACTS = [
|
|
|
25
30
|
"typia-validator.php",
|
|
26
31
|
"typia.openapi.json",
|
|
27
32
|
];
|
|
28
|
-
|
|
29
|
-
|
|
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 };
|
|
30
71
|
}
|
|
31
72
|
function createDoctorScopeCheck(status, detail) {
|
|
32
73
|
return createDoctorCheck("Doctor scope", status, detail);
|
|
33
74
|
}
|
|
34
|
-
function
|
|
35
|
-
return
|
|
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;
|
|
36
279
|
}
|
|
37
280
|
function getWorkspaceBootstrapRelativePath(packageName) {
|
|
38
281
|
const packageBaseName = packageName.split("/").pop() ?? packageName;
|
|
@@ -156,6 +399,51 @@ function checkWorkspaceBlockCollectionImport(projectDir, blockSlug) {
|
|
|
156
399
|
? "Shared block collection import is present"
|
|
157
400
|
: `Missing a shared collection import like ${WORKSPACE_COLLECTION_IMPORT_LINE}`);
|
|
158
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.";
|
|
432
|
+
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
|
+
}
|
|
159
447
|
function checkWorkspacePatternBootstrap(projectDir, packageName) {
|
|
160
448
|
const packageBaseName = packageName.split("/").pop() ?? packageName;
|
|
161
449
|
const bootstrapPath = path.join(projectDir, `${packageBaseName}.php`);
|
|
@@ -196,6 +484,82 @@ function checkWorkspaceBindingSourcesIndex(projectDir, bindingSources) {
|
|
|
196
484
|
? "Binding source editor registrations are aggregated"
|
|
197
485
|
: `Missing editor imports for: ${missingImports.map((entry) => entry.slug).join(", ")}`);
|
|
198
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
|
+
}
|
|
199
563
|
function getWorkspaceRestResourceRequiredFiles(restResource) {
|
|
200
564
|
const schemaNames = new Set();
|
|
201
565
|
if (restResource.methods.includes("list")) {
|
|
@@ -378,20 +742,23 @@ function checkWorkspaceAiFeatureBootstrap(projectDir, packageName, phpPrefix) {
|
|
|
378
742
|
}
|
|
379
743
|
function getWorkspaceEditorPluginRequiredFiles(editorPlugin) {
|
|
380
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");
|
|
381
748
|
return Array.from(new Set([
|
|
382
749
|
editorPlugin.file,
|
|
383
|
-
|
|
750
|
+
surfaceFile,
|
|
384
751
|
path.join(editorPluginDir, "data.ts"),
|
|
385
752
|
path.join(editorPluginDir, "types.ts"),
|
|
386
753
|
path.join(editorPluginDir, "style.scss"),
|
|
387
754
|
]));
|
|
388
755
|
}
|
|
389
756
|
function checkWorkspaceEditorPluginConfig(editorPlugin) {
|
|
390
|
-
const
|
|
391
|
-
const isValidSlot =
|
|
757
|
+
const normalizedSlot = resolveEditorPluginSlotAlias(editorPlugin.slot);
|
|
758
|
+
const isValidSlot = Boolean(normalizedSlot);
|
|
392
759
|
return createDoctorCheck(`Editor plugin config ${editorPlugin.slug}`, isValidSlot ? "pass" : "fail", isValidSlot
|
|
393
|
-
? `Editor plugin slot ${editorPlugin.slot} is supported`
|
|
394
|
-
: `Unsupported editor plugin slot "${editorPlugin.slot}". Expected one of: ${EDITOR_PLUGIN_SLOT_IDS.join(", ")}
|
|
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.`);
|
|
395
762
|
}
|
|
396
763
|
function checkWorkspaceEditorPluginBootstrap(projectDir, packageName, phpPrefix) {
|
|
397
764
|
const packageBaseName = packageName.split("/").pop() ?? packageName;
|
|
@@ -430,18 +797,145 @@ function checkWorkspaceEditorPluginIndex(projectDir, editorPlugins) {
|
|
|
430
797
|
.map((entry) => entry.slug)
|
|
431
798
|
.join(", ")}`);
|
|
432
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 sourceMatch = /^rest-resource:([a-z][a-z0-9-]*)$/u.exec(source);
|
|
818
|
+
const restResourceSlug = sourceMatch?.[1];
|
|
819
|
+
const restResource = restResourceSlug
|
|
820
|
+
? inventory.restResources.find((entry) => entry.slug === restResourceSlug)
|
|
821
|
+
: undefined;
|
|
822
|
+
const isValid = Boolean(restResource?.methods.includes("list"));
|
|
823
|
+
return createDoctorCheck(`Admin view config ${adminView.slug}`, isValid ? "pass" : "fail", isValid
|
|
824
|
+
? `Admin view source ${source} is list-capable`
|
|
825
|
+
: "Admin view source must use rest-resource:<slug> and reference a list-capable REST resource");
|
|
826
|
+
}
|
|
827
|
+
function checkWorkspaceAdminViewBootstrap(projectDir, packageName, phpPrefix) {
|
|
828
|
+
const packageBaseName = packageName.split("/").pop() ?? packageName;
|
|
829
|
+
const bootstrapPath = path.join(projectDir, `${packageBaseName}.php`);
|
|
830
|
+
if (!fs.existsSync(bootstrapPath)) {
|
|
831
|
+
return createDoctorCheck("Admin view bootstrap", "fail", `Missing ${path.basename(bootstrapPath)}`);
|
|
832
|
+
}
|
|
833
|
+
const source = fs.readFileSync(bootstrapPath, "utf8");
|
|
834
|
+
const loadFunctionName = `${phpPrefix}_load_admin_views`;
|
|
835
|
+
const loadHook = `add_action( 'plugins_loaded', '${loadFunctionName}' );`;
|
|
836
|
+
const hasLoaderHook = source.includes(loadHook);
|
|
837
|
+
const hasServerGlob = source.includes(WORKSPACE_ADMIN_VIEW_GLOB);
|
|
838
|
+
return createDoctorCheck("Admin view bootstrap", hasLoaderHook && hasServerGlob ? "pass" : "fail", hasLoaderHook && hasServerGlob
|
|
839
|
+
? "Admin view PHP loader hook is present"
|
|
840
|
+
: "Missing admin view PHP require glob or plugins_loaded hook");
|
|
841
|
+
}
|
|
842
|
+
function checkWorkspaceAdminViewIndex(projectDir, adminViews) {
|
|
843
|
+
const indexRelativePath = [
|
|
844
|
+
path.join("src", "admin-views", "index.ts"),
|
|
845
|
+
path.join("src", "admin-views", "index.js"),
|
|
846
|
+
].find((relativePath) => fs.existsSync(path.join(projectDir, relativePath)));
|
|
847
|
+
if (!indexRelativePath) {
|
|
848
|
+
return createDoctorCheck("Admin views index", "fail", "Missing src/admin-views/index.ts or src/admin-views/index.js");
|
|
849
|
+
}
|
|
850
|
+
const indexPath = path.join(projectDir, indexRelativePath);
|
|
851
|
+
const source = fs.readFileSync(indexPath, "utf8");
|
|
852
|
+
const missingImports = adminViews.filter((adminView) => {
|
|
853
|
+
const importPattern = new RegExp(`['"\`]\\./${escapeRegex(adminView.slug)}(?:/[^'"\`]*)?['"\`]`, "u");
|
|
854
|
+
return !importPattern.test(source);
|
|
855
|
+
});
|
|
856
|
+
return createDoctorCheck("Admin views index", missingImports.length === 0 ? "pass" : "fail", missingImports.length === 0
|
|
857
|
+
? "Admin view registrations are aggregated"
|
|
858
|
+
: `Missing admin view imports for: ${missingImports
|
|
859
|
+
.map((entry) => entry.slug)
|
|
860
|
+
.join(", ")}`);
|
|
861
|
+
}
|
|
862
|
+
function checkWorkspaceAdminViewPhp(projectDir, adminView) {
|
|
863
|
+
const phpPath = path.join(projectDir, adminView.phpFile);
|
|
864
|
+
if (!fs.existsSync(phpPath)) {
|
|
865
|
+
return createDoctorCheck(`Admin view PHP ${adminView.slug}`, "fail", `Missing ${adminView.phpFile}`);
|
|
866
|
+
}
|
|
867
|
+
const source = fs.readFileSync(phpPath, "utf8");
|
|
868
|
+
const hasAdminMenu = source.includes("add_submenu_page");
|
|
869
|
+
const hasAdminEnqueue = source.includes("admin_enqueue_scripts");
|
|
870
|
+
const hasScript = source.includes(WORKSPACE_ADMIN_VIEW_SCRIPT);
|
|
871
|
+
const hasAsset = source.includes(WORKSPACE_ADMIN_VIEW_ASSET);
|
|
872
|
+
const hasStyle = source.includes(WORKSPACE_ADMIN_VIEW_STYLE);
|
|
873
|
+
const hasComponentsStyleDependency = source.includes("'wp-components'");
|
|
874
|
+
return createDoctorCheck(`Admin view PHP ${adminView.slug}`, hasAdminMenu &&
|
|
875
|
+
hasAdminEnqueue &&
|
|
876
|
+
hasScript &&
|
|
877
|
+
hasAsset &&
|
|
878
|
+
hasStyle &&
|
|
879
|
+
hasComponentsStyleDependency
|
|
880
|
+
? "pass"
|
|
881
|
+
: "fail", hasAdminMenu &&
|
|
882
|
+
hasAdminEnqueue &&
|
|
883
|
+
hasScript &&
|
|
884
|
+
hasAsset &&
|
|
885
|
+
hasStyle &&
|
|
886
|
+
hasComponentsStyleDependency
|
|
887
|
+
? "Admin menu, script, style, and wp-components style dependency are wired"
|
|
888
|
+
: "Missing admin menu, enqueue hook, build/admin-views asset reference, or wp-components style dependency");
|
|
889
|
+
}
|
|
433
890
|
function checkVariationEntrypoint(projectDir, blockSlug) {
|
|
434
891
|
const entryPath = path.join(projectDir, "src", "blocks", blockSlug, "index.tsx");
|
|
435
892
|
if (!fs.existsSync(entryPath)) {
|
|
436
893
|
return createDoctorCheck(`Variation entrypoint ${blockSlug}`, "fail", `Missing ${path.relative(projectDir, entryPath)}`);
|
|
437
894
|
}
|
|
438
895
|
const source = fs.readFileSync(entryPath, "utf8");
|
|
439
|
-
const hasImport = source
|
|
440
|
-
const hasCall = source
|
|
896
|
+
const hasImport = hasUncommentedPattern(source, WORKSPACE_VARIATIONS_IMPORT_PATTERN);
|
|
897
|
+
const hasCall = hasExecutablePattern(source, WORKSPACE_VARIATIONS_CALL_PATTERN);
|
|
441
898
|
return createDoctorCheck(`Variation entrypoint ${blockSlug}`, hasImport && hasCall ? "pass" : "fail", hasImport && hasCall
|
|
442
899
|
? "Variations registration hook is present"
|
|
443
900
|
: "Missing ./variations import or registerWorkspaceVariations() call");
|
|
444
901
|
}
|
|
902
|
+
function checkBlockStyleEntrypoint(projectDir, blockSlug) {
|
|
903
|
+
const entryPath = path.join(projectDir, "src", "blocks", blockSlug, "index.tsx");
|
|
904
|
+
if (!fs.existsSync(entryPath)) {
|
|
905
|
+
return createDoctorCheck(`Block style entrypoint ${blockSlug}`, "fail", `Missing ${path.relative(projectDir, entryPath)}`);
|
|
906
|
+
}
|
|
907
|
+
const source = fs.readFileSync(entryPath, "utf8");
|
|
908
|
+
const hasImport = hasUncommentedPattern(source, WORKSPACE_BLOCK_STYLES_IMPORT_PATTERN);
|
|
909
|
+
const hasCall = hasExecutablePattern(source, WORKSPACE_BLOCK_STYLES_CALL_PATTERN);
|
|
910
|
+
return createDoctorCheck(`Block style entrypoint ${blockSlug}`, hasImport && hasCall ? "pass" : "fail", hasImport && hasCall
|
|
911
|
+
? "Block style registration hook is present"
|
|
912
|
+
: "Missing ./styles import or registerWorkspaceBlockStyles() call");
|
|
913
|
+
}
|
|
914
|
+
function checkBlockTransformEntrypoint(projectDir, blockSlug) {
|
|
915
|
+
const entryPath = path.join(projectDir, "src", "blocks", blockSlug, "index.tsx");
|
|
916
|
+
if (!fs.existsSync(entryPath)) {
|
|
917
|
+
return createDoctorCheck(`Block transform entrypoint ${blockSlug}`, "fail", `Missing ${path.relative(projectDir, entryPath)}`);
|
|
918
|
+
}
|
|
919
|
+
const source = fs.readFileSync(entryPath, "utf8");
|
|
920
|
+
const hasImport = hasUncommentedPattern(source, WORKSPACE_BLOCK_TRANSFORMS_IMPORT_PATTERN);
|
|
921
|
+
const hasCall = hasExecutablePattern(source, WORKSPACE_BLOCK_TRANSFORMS_CALL_PATTERN);
|
|
922
|
+
return createDoctorCheck(`Block transform entrypoint ${blockSlug}`, hasImport && hasCall ? "pass" : "fail", hasImport && hasCall
|
|
923
|
+
? "Block transform registration hook is present"
|
|
924
|
+
: "Missing ./transforms import or applyWorkspaceBlockTransforms(registration.settings) call");
|
|
925
|
+
}
|
|
926
|
+
function checkBlockTransformConfig(workspace, transform) {
|
|
927
|
+
const expectedTo = `${workspace.workspace.namespace}/${transform.block}`;
|
|
928
|
+
const issues = [];
|
|
929
|
+
if (!WORKSPACE_FULL_BLOCK_NAME_PATTERN.test(transform.from)) {
|
|
930
|
+
issues.push("from must use full namespace/block format");
|
|
931
|
+
}
|
|
932
|
+
if (transform.to !== expectedTo) {
|
|
933
|
+
issues.push(`to must equal "${expectedTo}" for workspace block "${transform.block}"`);
|
|
934
|
+
}
|
|
935
|
+
return createDoctorCheck(`Block transform config ${transform.block}/${transform.slug}`, issues.length === 0 ? "pass" : "fail", issues.length === 0
|
|
936
|
+
? `${transform.from} transforms into ${transform.to}`
|
|
937
|
+
: issues.join("; "));
|
|
938
|
+
}
|
|
445
939
|
function checkMigrationWorkspaceHint(workspace, packageJson) {
|
|
446
940
|
const hasMigrationScript = typeof packageJson.scripts?.["migration:doctor"] === "string";
|
|
447
941
|
const migrationConfigRelativePath = path.join("src", "migrations", "config.ts");
|
|
@@ -504,12 +998,13 @@ export function getWorkspaceDoctorChecks(cwd) {
|
|
|
504
998
|
checks.push(checkWorkspacePackageMetadata(workspace, workspacePackageJson));
|
|
505
999
|
try {
|
|
506
1000
|
const inventory = readWorkspaceInventory(workspace.projectDir);
|
|
507
|
-
checks.push(createDoctorCheck("Workspace inventory", "pass", `${inventory.blocks.length} block(s), ${inventory.variations.length} variation(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)`));
|
|
1001
|
+
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)`));
|
|
508
1002
|
for (const block of inventory.blocks) {
|
|
509
1003
|
checks.push(checkExistingFiles(workspace.projectDir, `Block ${block.slug}`, getWorkspaceBlockRequiredFiles(block)));
|
|
510
1004
|
checks.push(checkWorkspaceBlockMetadata(workspace.projectDir, workspace, block));
|
|
511
1005
|
checks.push(checkWorkspaceBlockHooks(workspace.projectDir, block.slug));
|
|
512
1006
|
checks.push(checkWorkspaceBlockCollectionImport(workspace.projectDir, block.slug));
|
|
1007
|
+
checks.push(...checkWorkspaceBlockIframeCompatibility(workspace.projectDir, block.slug));
|
|
513
1008
|
}
|
|
514
1009
|
const registeredBlockSlugs = new Set(inventory.blocks.map((block) => block.slug));
|
|
515
1010
|
const variationTargetBlocks = new Set();
|
|
@@ -524,6 +1019,37 @@ export function getWorkspaceDoctorChecks(cwd) {
|
|
|
524
1019
|
for (const blockSlug of variationTargetBlocks) {
|
|
525
1020
|
checks.push(checkVariationEntrypoint(workspace.projectDir, blockSlug));
|
|
526
1021
|
}
|
|
1022
|
+
const blockStyleTargetBlocks = new Set();
|
|
1023
|
+
for (const blockStyle of inventory.blockStyles) {
|
|
1024
|
+
if (!registeredBlockSlugs.has(blockStyle.block)) {
|
|
1025
|
+
checks.push(createDoctorCheck(`Block style ${blockStyle.block}/${blockStyle.slug}`, "fail", `Block style references unknown block "${blockStyle.block}"`));
|
|
1026
|
+
continue;
|
|
1027
|
+
}
|
|
1028
|
+
blockStyleTargetBlocks.add(blockStyle.block);
|
|
1029
|
+
checks.push(checkExistingFiles(workspace.projectDir, `Block style ${blockStyle.block}/${blockStyle.slug}`, [blockStyle.file]));
|
|
1030
|
+
}
|
|
1031
|
+
for (const blockSlug of blockStyleTargetBlocks) {
|
|
1032
|
+
checks.push(checkExistingFiles(workspace.projectDir, `Block style registry ${blockSlug}`, [
|
|
1033
|
+
path.join("src", "blocks", blockSlug, "styles", "index.ts"),
|
|
1034
|
+
]));
|
|
1035
|
+
checks.push(checkBlockStyleEntrypoint(workspace.projectDir, blockSlug));
|
|
1036
|
+
}
|
|
1037
|
+
const blockTransformTargetBlocks = new Set();
|
|
1038
|
+
for (const blockTransform of inventory.blockTransforms) {
|
|
1039
|
+
if (!registeredBlockSlugs.has(blockTransform.block)) {
|
|
1040
|
+
checks.push(createDoctorCheck(`Block transform ${blockTransform.block}/${blockTransform.slug}`, "fail", `Block transform references unknown block "${blockTransform.block}"`));
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
blockTransformTargetBlocks.add(blockTransform.block);
|
|
1044
|
+
checks.push(checkBlockTransformConfig(workspace, blockTransform));
|
|
1045
|
+
checks.push(checkExistingFiles(workspace.projectDir, `Block transform ${blockTransform.block}/${blockTransform.slug}`, [blockTransform.file]));
|
|
1046
|
+
}
|
|
1047
|
+
for (const blockSlug of blockTransformTargetBlocks) {
|
|
1048
|
+
checks.push(checkExistingFiles(workspace.projectDir, `Block transform registry ${blockSlug}`, [
|
|
1049
|
+
path.join("src", "blocks", blockSlug, "transforms", "index.ts"),
|
|
1050
|
+
]));
|
|
1051
|
+
checks.push(checkBlockTransformEntrypoint(workspace.projectDir, blockSlug));
|
|
1052
|
+
}
|
|
527
1053
|
const shouldCheckPatternBootstrap = inventory.patterns.length > 0 ||
|
|
528
1054
|
fs.existsSync(path.join(workspace.projectDir, "src", "patterns"));
|
|
529
1055
|
if (shouldCheckPatternBootstrap) {
|
|
@@ -541,6 +1067,10 @@ export function getWorkspaceDoctorChecks(cwd) {
|
|
|
541
1067
|
bindingSource.serverFile,
|
|
542
1068
|
bindingSource.editorFile,
|
|
543
1069
|
]));
|
|
1070
|
+
const bindingTargetCheck = checkWorkspaceBindingTarget(workspace.projectDir, workspace, registeredBlockSlugs, bindingSource);
|
|
1071
|
+
if (bindingTargetCheck) {
|
|
1072
|
+
checks.push(bindingTargetCheck);
|
|
1073
|
+
}
|
|
544
1074
|
}
|
|
545
1075
|
if (inventory.restResources.length > 0) {
|
|
546
1076
|
checks.push(checkWorkspaceRestResourceBootstrap(workspace.projectDir, workspace.packageName, workspace.workspace.phpPrefix));
|
|
@@ -572,6 +1102,15 @@ export function getWorkspaceDoctorChecks(cwd) {
|
|
|
572
1102
|
checks.push(checkExistingFiles(workspace.projectDir, `Editor plugin ${editorPlugin.slug}`, getWorkspaceEditorPluginRequiredFiles(editorPlugin)));
|
|
573
1103
|
checks.push(checkWorkspaceEditorPluginConfig(editorPlugin));
|
|
574
1104
|
}
|
|
1105
|
+
if (inventory.adminViews.length > 0) {
|
|
1106
|
+
checks.push(checkWorkspaceAdminViewBootstrap(workspace.projectDir, workspace.packageName, workspace.workspace.phpPrefix));
|
|
1107
|
+
checks.push(checkWorkspaceAdminViewIndex(workspace.projectDir, inventory.adminViews));
|
|
1108
|
+
}
|
|
1109
|
+
for (const adminView of inventory.adminViews) {
|
|
1110
|
+
checks.push(checkWorkspaceAdminViewConfig(adminView, inventory));
|
|
1111
|
+
checks.push(checkExistingFiles(workspace.projectDir, `Admin view ${adminView.slug}`, getWorkspaceAdminViewRequiredFiles(adminView)));
|
|
1112
|
+
checks.push(checkWorkspaceAdminViewPhp(workspace.projectDir, adminView));
|
|
1113
|
+
}
|
|
575
1114
|
const migrationWorkspaceCheck = checkMigrationWorkspaceHint(workspace, workspacePackageJson);
|
|
576
1115
|
if (migrationWorkspaceCheck) {
|
|
577
1116
|
checks.push(migrationWorkspaceCheck);
|