@wp-typia/project-tools 0.24.9 → 0.24.11
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.
|
@@ -4,6 +4,8 @@ import { parseScaffoldBlockMetadata } from "@wp-typia/block-runtime/blocks";
|
|
|
4
4
|
import { WORDPRESS_BLOCK_API_COMPATIBILITY } from "@wp-typia/block-types/blocks/compatibility";
|
|
5
5
|
import { createDoctorCheck, resolveWorkspaceBootstrapPath, } from "./cli-doctor-workspace-shared.js";
|
|
6
6
|
import { readJsonFileSync } from "../shared/json-utils.js";
|
|
7
|
+
import { hasPhpFunctionCallWithAssignedStringPrefixArgument, hasPhpFunctionCallWithStringArgumentPrefix, } from "../shared/php-utils.js";
|
|
8
|
+
import { hasExecutablePattern, hasUncommentedPattern, maskTypeScriptComments, maskTypeScriptCommentsAndLiterals, } from "../shared/ts-source-masking.js";
|
|
7
9
|
import { compareVersionFloors, pickHigherVersionFloor, } from "../shared/version-floor.js";
|
|
8
10
|
import { DEFAULT_SCAFFOLD_WORDPRESS_TARGET_VERSION } from "../templates/scaffold-compatibility.js";
|
|
9
11
|
const WORDPRESS_VERSION_CHECK_CODES = {
|
|
@@ -15,9 +17,18 @@ const BLOCK_VARIATION_BLOCK_JSON_KEYS = {
|
|
|
15
17
|
registrationBlockJsonMetadata: "variations",
|
|
16
18
|
registrationMetadataFile: "variations",
|
|
17
19
|
};
|
|
20
|
+
const CORE_VARIATION_REGISTRY_IMPORT_PATTERN = /^\s*import\s*(?!type\b)\{[\s\S]*?\}\s*from\s*["']\.\/[^"']+\/[^"']+\/[^"']+["']\s*;?\s*$/mu;
|
|
21
|
+
const REGISTER_BLOCK_VARIATION_CALL_PATTERN = /\bregisterBlockVariation\s*\(/u;
|
|
22
|
+
const REGISTER_WORKSPACE_CORE_VARIATIONS_CALL_PATTERN = /^\s*registerWorkspaceCoreVariations\s*\(\s*\)\s*;?\s*$/mu;
|
|
23
|
+
const REGISTER_BLOCK_BINDINGS_SOURCE_CALL_PATTERN = /\bregisterBlockBindingsSource\s*\(/gu;
|
|
24
|
+
const GET_FIELDS_LIST_PROPERTY_PATTERN = /(?:async\s+)?\bgetFieldsList\s*\([^)]*\)|(?:async\s+)?["']getFieldsList["']\s*\([^)]*\)|\bgetFieldsList\s*:|["']getFieldsList["']\s*:|\bgetFieldsList\s*(?=,|\})/gu;
|
|
25
|
+
const SUPPORTED_ATTRIBUTES_FILTER_PREFIX = "block_bindings_supported_attributes_";
|
|
18
26
|
function isEnabledMetadataValue(value) {
|
|
19
27
|
return value !== undefined && value !== false && value !== null;
|
|
20
28
|
}
|
|
29
|
+
function assertNeverBlockVariationFeature(feature) {
|
|
30
|
+
throw new Error(`Unhandled block variation metadata feature "${String(feature)}".`);
|
|
31
|
+
}
|
|
21
32
|
function isEnabledBlockVariationMetadataFeature(feature, value) {
|
|
22
33
|
if (feature === "registrationBlockJsonMetadata") {
|
|
23
34
|
return Array.isArray(value) && value.length > 0;
|
|
@@ -25,7 +36,7 @@ function isEnabledBlockVariationMetadataFeature(feature, value) {
|
|
|
25
36
|
if (feature === "registrationMetadataFile") {
|
|
26
37
|
return typeof value === "string" && value.trim().length > 0;
|
|
27
38
|
}
|
|
28
|
-
return
|
|
39
|
+
return assertNeverBlockVariationFeature(feature);
|
|
29
40
|
}
|
|
30
41
|
function getNestedMetadataValue(object, key) {
|
|
31
42
|
if (!object) {
|
|
@@ -80,6 +91,294 @@ function readExistingTextFile(filePath) {
|
|
|
80
91
|
}
|
|
81
92
|
return fs.readFileSync(filePath, "utf8");
|
|
82
93
|
}
|
|
94
|
+
function isTypeScriptIdentifierPart(character) {
|
|
95
|
+
return /^[A-Za-z0-9_$]$/u.test(character ?? "");
|
|
96
|
+
}
|
|
97
|
+
function skipTypeScriptWhitespace(source, index) {
|
|
98
|
+
let cursor = index;
|
|
99
|
+
while (/\s/u.test(source[cursor] ?? "")) {
|
|
100
|
+
cursor += 1;
|
|
101
|
+
}
|
|
102
|
+
return cursor;
|
|
103
|
+
}
|
|
104
|
+
function findClosingDelimiter(source, openIndex, openDelimiter, closeDelimiter) {
|
|
105
|
+
let depth = 0;
|
|
106
|
+
for (let cursor = openIndex; cursor < source.length; cursor += 1) {
|
|
107
|
+
const character = source[cursor];
|
|
108
|
+
if (character === openDelimiter) {
|
|
109
|
+
depth += 1;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (character === closeDelimiter) {
|
|
113
|
+
depth -= 1;
|
|
114
|
+
if (depth === 0) {
|
|
115
|
+
return cursor;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
function findTopLevelSatisfiesIndex(source) {
|
|
122
|
+
let braceDepth = 0;
|
|
123
|
+
let bracketDepth = 0;
|
|
124
|
+
let parenthesisDepth = 0;
|
|
125
|
+
for (let cursor = 0; cursor < source.length; cursor += 1) {
|
|
126
|
+
const character = source[cursor];
|
|
127
|
+
if (character === "{") {
|
|
128
|
+
braceDepth += 1;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (character === "}") {
|
|
132
|
+
braceDepth = Math.max(0, braceDepth - 1);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (character === "[") {
|
|
136
|
+
bracketDepth += 1;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (character === "]") {
|
|
140
|
+
bracketDepth = Math.max(0, bracketDepth - 1);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (character === "(") {
|
|
144
|
+
parenthesisDepth += 1;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (character === ")") {
|
|
148
|
+
parenthesisDepth = Math.max(0, parenthesisDepth - 1);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (braceDepth === 0 &&
|
|
152
|
+
bracketDepth === 0 &&
|
|
153
|
+
parenthesisDepth === 0 &&
|
|
154
|
+
source.startsWith("satisfies", cursor) &&
|
|
155
|
+
!isTypeScriptIdentifierPart(source[cursor - 1]) &&
|
|
156
|
+
!isTypeScriptIdentifierPart(source[cursor + "satisfies".length])) {
|
|
157
|
+
return cursor;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
function isTopLevelObjectPropertyStart(structureMaskedObjectSource, index) {
|
|
163
|
+
let braceDepth = 0;
|
|
164
|
+
let bracketDepth = 0;
|
|
165
|
+
let parenthesisDepth = 0;
|
|
166
|
+
for (let cursor = 0; cursor < index; cursor += 1) {
|
|
167
|
+
const character = structureMaskedObjectSource[cursor];
|
|
168
|
+
if (character === "{") {
|
|
169
|
+
braceDepth += 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (character === "}") {
|
|
173
|
+
braceDepth = Math.max(0, braceDepth - 1);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (character === "[") {
|
|
177
|
+
bracketDepth += 1;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (character === "]") {
|
|
181
|
+
bracketDepth = Math.max(0, bracketDepth - 1);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (character === "(") {
|
|
185
|
+
parenthesisDepth += 1;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (character === ")") {
|
|
189
|
+
parenthesisDepth = Math.max(0, parenthesisDepth - 1);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (braceDepth !== 1 || bracketDepth !== 0 || parenthesisDepth !== 0) {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
let previousCursor = index - 1;
|
|
196
|
+
while (previousCursor >= 0 &&
|
|
197
|
+
/\s/u.test(structureMaskedObjectSource[previousCursor] ?? "")) {
|
|
198
|
+
previousCursor -= 1;
|
|
199
|
+
}
|
|
200
|
+
const previousToken = structureMaskedObjectSource[previousCursor];
|
|
201
|
+
return previousToken === "{" || previousToken === ",";
|
|
202
|
+
}
|
|
203
|
+
function objectLiteralHasGetFieldsListProperty(commentMaskedObjectSource, structureMaskedObjectSource) {
|
|
204
|
+
GET_FIELDS_LIST_PROPERTY_PATTERN.lastIndex = 0;
|
|
205
|
+
let match;
|
|
206
|
+
while ((match = GET_FIELDS_LIST_PROPERTY_PATTERN.exec(commentMaskedObjectSource))) {
|
|
207
|
+
if (isTopLevelObjectPropertyStart(structureMaskedObjectSource, match.index)) {
|
|
208
|
+
GET_FIELDS_LIST_PROPERTY_PATTERN.lastIndex = 0;
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
GET_FIELDS_LIST_PROPERTY_PATTERN.lastIndex = 0;
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
function getRuntimeArgumentEnd(maskedCallArgumentsSource) {
|
|
216
|
+
return (findTopLevelSatisfiesIndex(maskedCallArgumentsSource) ??
|
|
217
|
+
maskedCallArgumentsSource.length);
|
|
218
|
+
}
|
|
219
|
+
function getFirstObjectArgumentSpan(structureMaskedRuntimeArgumentsSource, absoluteStart) {
|
|
220
|
+
const objectStartOffset = skipTypeScriptWhitespace(structureMaskedRuntimeArgumentsSource, 0);
|
|
221
|
+
if (structureMaskedRuntimeArgumentsSource[objectStartOffset] !== "{") {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
const objectEndOffset = findClosingDelimiter(structureMaskedRuntimeArgumentsSource, objectStartOffset, "{", "}");
|
|
225
|
+
if (objectEndOffset === null) {
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
end: absoluteStart + objectEndOffset + 1,
|
|
230
|
+
start: absoluteStart + objectStartOffset,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function getSimpleRuntimeArgumentIdentifier(structureMaskedRuntimeArgumentsSource) {
|
|
234
|
+
const trimmed = structureMaskedRuntimeArgumentsSource.trim();
|
|
235
|
+
const match = /^([A-Za-z_$][\w$]*)(?:\s+as\b[\s\S]*)?$/u.exec(trimmed);
|
|
236
|
+
return match?.[1];
|
|
237
|
+
}
|
|
238
|
+
function findObjectInitializerStart(structureMaskedSource, index) {
|
|
239
|
+
let braceDepth = 0;
|
|
240
|
+
let bracketDepth = 0;
|
|
241
|
+
let parenthesisDepth = 0;
|
|
242
|
+
for (let cursor = index; cursor < structureMaskedSource.length; cursor += 1) {
|
|
243
|
+
const character = structureMaskedSource[cursor];
|
|
244
|
+
if (character === "{") {
|
|
245
|
+
braceDepth += 1;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (character === "}") {
|
|
249
|
+
braceDepth = Math.max(0, braceDepth - 1);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (character === "[") {
|
|
253
|
+
bracketDepth += 1;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (character === "]") {
|
|
257
|
+
bracketDepth = Math.max(0, bracketDepth - 1);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (character === "(") {
|
|
261
|
+
parenthesisDepth += 1;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (character === ")") {
|
|
265
|
+
parenthesisDepth = Math.max(0, parenthesisDepth - 1);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (braceDepth !== 0 || bracketDepth !== 0 || parenthesisDepth !== 0) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (character === ";") {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
if (character !== "=" || structureMaskedSource[cursor + 1] === ">") {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const valueStart = skipTypeScriptWhitespace(structureMaskedSource, cursor + 1);
|
|
278
|
+
return structureMaskedSource[valueStart] === "{" ? valueStart : null;
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
function isTopLevelTypeScriptPosition(structureMaskedSource, index) {
|
|
283
|
+
let braceDepth = 0;
|
|
284
|
+
let bracketDepth = 0;
|
|
285
|
+
let parenthesisDepth = 0;
|
|
286
|
+
for (let cursor = 0; cursor < index; cursor += 1) {
|
|
287
|
+
const character = structureMaskedSource[cursor];
|
|
288
|
+
if (character === "{") {
|
|
289
|
+
braceDepth += 1;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (character === "}") {
|
|
293
|
+
braceDepth = Math.max(0, braceDepth - 1);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (character === "[") {
|
|
297
|
+
bracketDepth += 1;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (character === "]") {
|
|
301
|
+
bracketDepth = Math.max(0, bracketDepth - 1);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (character === "(") {
|
|
305
|
+
parenthesisDepth += 1;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (character === ")") {
|
|
309
|
+
parenthesisDepth = Math.max(0, parenthesisDepth - 1);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return braceDepth === 0 && bracketDepth === 0 && parenthesisDepth === 0;
|
|
313
|
+
}
|
|
314
|
+
function variableObjectHasGetFieldsListProperty(identifier, commentMaskedSource, structureMaskedSource, beforeIndex) {
|
|
315
|
+
const variablePattern = /\b(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b/gu;
|
|
316
|
+
let match;
|
|
317
|
+
let nearestObjectSpan;
|
|
318
|
+
while ((match = variablePattern.exec(structureMaskedSource))) {
|
|
319
|
+
if (match.index >= beforeIndex) {
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
if (match[1] !== identifier) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (!isTopLevelTypeScriptPosition(structureMaskedSource, match.index)) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const objectStart = findObjectInitializerStart(structureMaskedSource, match.index + match[0].length);
|
|
329
|
+
if (objectStart === null || objectStart >= beforeIndex) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const objectEnd = findClosingDelimiter(structureMaskedSource, objectStart, "{", "}");
|
|
333
|
+
if (objectEnd === null) {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
nearestObjectSpan = {
|
|
337
|
+
end: objectEnd + 1,
|
|
338
|
+
start: objectStart,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
return nearestObjectSpan
|
|
342
|
+
? objectLiteralHasGetFieldsListProperty(commentMaskedSource.slice(nearestObjectSpan.start, nearestObjectSpan.end), structureMaskedSource.slice(nearestObjectSpan.start, nearestObjectSpan.end))
|
|
343
|
+
: false;
|
|
344
|
+
}
|
|
345
|
+
function hasRegisterBlockBindingsSourceGetFieldsList(source) {
|
|
346
|
+
const structureMaskedSource = maskTypeScriptCommentsAndLiterals(source);
|
|
347
|
+
const commentMaskedSource = maskTypeScriptComments(source);
|
|
348
|
+
REGISTER_BLOCK_BINDINGS_SOURCE_CALL_PATTERN.lastIndex = 0;
|
|
349
|
+
let match;
|
|
350
|
+
while ((match =
|
|
351
|
+
REGISTER_BLOCK_BINDINGS_SOURCE_CALL_PATTERN.exec(structureMaskedSource))) {
|
|
352
|
+
const openIndex = structureMaskedSource.indexOf("(", match.index);
|
|
353
|
+
if (openIndex === -1) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const closeIndex = findClosingDelimiter(structureMaskedSource, openIndex, "(", ")");
|
|
357
|
+
if (closeIndex === null) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const maskedCallArgumentsSource = structureMaskedSource.slice(openIndex + 1, closeIndex);
|
|
361
|
+
const runtimeArgumentEnd = getRuntimeArgumentEnd(maskedCallArgumentsSource);
|
|
362
|
+
const runtimeArgumentsStart = openIndex + 1;
|
|
363
|
+
const maskedRuntimeArgumentsSource = structureMaskedSource.slice(runtimeArgumentsStart, runtimeArgumentsStart + runtimeArgumentEnd);
|
|
364
|
+
const objectArgumentSpan = getFirstObjectArgumentSpan(maskedRuntimeArgumentsSource, runtimeArgumentsStart);
|
|
365
|
+
if (objectArgumentSpan &&
|
|
366
|
+
objectLiteralHasGetFieldsListProperty(commentMaskedSource.slice(objectArgumentSpan.start, objectArgumentSpan.end), structureMaskedSource.slice(objectArgumentSpan.start, objectArgumentSpan.end))) {
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
const runtimeArgumentIdentifier = getSimpleRuntimeArgumentIdentifier(maskedRuntimeArgumentsSource);
|
|
370
|
+
if (runtimeArgumentIdentifier &&
|
|
371
|
+
variableObjectHasGetFieldsListProperty(runtimeArgumentIdentifier, commentMaskedSource, structureMaskedSource, match.index)) {
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
REGISTER_BLOCK_BINDINGS_SOURCE_CALL_PATTERN.lastIndex = closeIndex + 1;
|
|
375
|
+
}
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
function hasSupportedAttributesFilterRegistration(source) {
|
|
379
|
+
return (hasPhpFunctionCallWithStringArgumentPrefix(source, "add_filter", SUPPORTED_ATTRIBUTES_FILTER_PREFIX) ||
|
|
380
|
+
hasPhpFunctionCallWithAssignedStringPrefixArgument(source, "add_filter", SUPPORTED_ATTRIBUTES_FILTER_PREFIX));
|
|
381
|
+
}
|
|
83
382
|
function collectBlockMetadataRequirements(workspace, inventory) {
|
|
84
383
|
const issues = [];
|
|
85
384
|
const requirements = [];
|
|
@@ -131,23 +430,16 @@ function collectBlockMetadataRequirements(workspace, inventory) {
|
|
|
131
430
|
requirements,
|
|
132
431
|
};
|
|
133
432
|
}
|
|
134
|
-
function
|
|
135
|
-
|
|
433
|
+
function hasGeneratedCoreVariationRegistry(projectDir) {
|
|
434
|
+
// Convention: generated core variation registries are rooted at this entrypoint.
|
|
435
|
+
const registryPath = path.join(projectDir, "src", "editor-plugins", "core-variations", "index.ts");
|
|
436
|
+
const source = readExistingTextFile(registryPath);
|
|
437
|
+
if (!source) {
|
|
136
438
|
return false;
|
|
137
439
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return true;
|
|
142
|
-
}
|
|
143
|
-
if (!isRootDirectory &&
|
|
144
|
-
entry.isFile() &&
|
|
145
|
-
entry.name.endsWith(".ts") &&
|
|
146
|
-
entry.name !== "index.ts") {
|
|
147
|
-
return true;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return false;
|
|
440
|
+
return (hasUncommentedPattern(source, CORE_VARIATION_REGISTRY_IMPORT_PATTERN) &&
|
|
441
|
+
hasExecutablePattern(source, REGISTER_BLOCK_VARIATION_CALL_PATTERN) &&
|
|
442
|
+
hasExecutablePattern(source, REGISTER_WORKSPACE_CORE_VARIATIONS_CALL_PATTERN));
|
|
151
443
|
}
|
|
152
444
|
function collectVariationRequirements(workspace, inventory) {
|
|
153
445
|
const requirements = [];
|
|
@@ -155,8 +447,7 @@ function collectVariationRequirements(workspace, inventory) {
|
|
|
155
447
|
for (const variation of inventory.variations) {
|
|
156
448
|
pushBlockApiRequirement(requirements, `Variation ${variation.block}/${variation.slug}`, variationEntries.editorRegistration);
|
|
157
449
|
}
|
|
158
|
-
|
|
159
|
-
if (hasGeneratedCoreVariationModule(coreVariationsDir)) {
|
|
450
|
+
if (hasGeneratedCoreVariationRegistry(workspace.projectDir)) {
|
|
160
451
|
pushBlockApiRequirement(requirements, "Core variations editor plugin", variationEntries.editorRegistration);
|
|
161
452
|
}
|
|
162
453
|
return {
|
|
@@ -196,11 +487,14 @@ function collectBindingSourceRequirements(workspace, inventory) {
|
|
|
196
487
|
pushBlockApiRequirement(requirements, `Binding source ${bindingSource.slug}`, bindingEntries.serverRegistration);
|
|
197
488
|
pushBlockApiRequirement(requirements, `Binding source ${bindingSource.slug}`, bindingEntries.editorRegistration);
|
|
198
489
|
const editorFilePath = path.join(workspace.projectDir, bindingSource.editorFile);
|
|
199
|
-
|
|
490
|
+
const editorSource = readExistingTextFile(editorFilePath);
|
|
491
|
+
if (editorSource &&
|
|
492
|
+
hasRegisterBlockBindingsSourceGetFieldsList(editorSource)) {
|
|
200
493
|
pushBlockApiRequirement(requirements, `Binding source ${bindingSource.slug}`, bindingEntries.editorFieldsList);
|
|
201
494
|
}
|
|
202
495
|
const serverFilePath = path.join(workspace.projectDir, bindingSource.serverFile);
|
|
203
|
-
|
|
496
|
+
const serverSource = readExistingTextFile(serverFilePath);
|
|
497
|
+
if (serverSource && hasSupportedAttributesFilterRegistration(serverSource)) {
|
|
204
498
|
pushBlockApiRequirement(requirements, `Binding source ${bindingSource.slug}`, bindingEntries.supportedAttributesFilter);
|
|
205
499
|
}
|
|
206
500
|
}
|
|
@@ -247,6 +541,9 @@ function formatRequirementSummary(requirements, floor) {
|
|
|
247
541
|
.map((requirement) => requirement.label);
|
|
248
542
|
return labels.length > 0 ? labels.join(", ") : "generated workspace features";
|
|
249
543
|
}
|
|
544
|
+
function formatFeatureMinimumAction(floor) {
|
|
545
|
+
return `Update the plugin bootstrap \`Requires at least\` header to ${floor} or remove the features that require that floor.`;
|
|
546
|
+
}
|
|
250
547
|
function createFeatureMinimumCheck(workspace, inventory, headers) {
|
|
251
548
|
const { issues, requirements } = collectWordPressVersionRequirements(workspace, inventory);
|
|
252
549
|
const highestFloor = pickHighestRequirementFloor(requirements, issues);
|
|
@@ -257,11 +554,11 @@ function createFeatureMinimumCheck(workspace, inventory, headers) {
|
|
|
257
554
|
return createDoctorCheck("WordPress feature minimum", "pass", "No generated workspace features declare an additional WordPress hard floor.", WORDPRESS_VERSION_CHECK_CODES.featureMinimum);
|
|
258
555
|
}
|
|
259
556
|
if (!headers.requiresAtLeast) {
|
|
260
|
-
return createDoctorCheck("WordPress feature minimum", "fail", `Plugin bootstrap is missing a Requires at least header but generated features require WordPress ${highestFloor}
|
|
557
|
+
return createDoctorCheck("WordPress feature minimum", "fail", `Plugin bootstrap is missing a Requires at least header but generated features require WordPress ${highestFloor}. ${formatFeatureMinimumAction(highestFloor)}`, WORDPRESS_VERSION_CHECK_CODES.featureMinimum);
|
|
261
558
|
}
|
|
262
559
|
try {
|
|
263
560
|
if (compareVersionFloors(headers.requiresAtLeast, highestFloor) < 0) {
|
|
264
|
-
return createDoctorCheck("WordPress feature minimum", "fail", `Requires at least ${headers.requiresAtLeast} is below generated feature floor ${highestFloor} (${formatRequirementSummary(requirements, highestFloor)})
|
|
561
|
+
return createDoctorCheck("WordPress feature minimum", "fail", `Requires at least ${headers.requiresAtLeast} is below generated feature floor ${highestFloor} (${formatRequirementSummary(requirements, highestFloor)}). ${formatFeatureMinimumAction(highestFloor)}`, WORDPRESS_VERSION_CHECK_CODES.featureMinimum);
|
|
265
562
|
}
|
|
266
563
|
}
|
|
267
564
|
catch {
|
|
@@ -275,7 +572,7 @@ function createTestedTargetCheck(headers, targetVersion) {
|
|
|
275
572
|
}
|
|
276
573
|
try {
|
|
277
574
|
if (compareVersionFloors(headers.testedUpTo, targetVersion) < 0) {
|
|
278
|
-
return createDoctorCheck("WordPress tested target", "warn", `Tested up to ${headers.testedUpTo} is below the selected WordPress target ${targetVersion}.`, WORDPRESS_VERSION_CHECK_CODES.testedTarget);
|
|
575
|
+
return createDoctorCheck("WordPress tested target", "warn", `Tested up to ${headers.testedUpTo} is below the selected WordPress target ${targetVersion}. Update the plugin bootstrap \`Tested up to\` header when the workspace is verified against WordPress ${targetVersion}.`, WORDPRESS_VERSION_CHECK_CODES.testedTarget);
|
|
279
576
|
}
|
|
280
577
|
}
|
|
281
578
|
catch {
|
|
@@ -20,6 +20,32 @@ export declare function hasPhpFunctionDefinition(source: string, functionName: s
|
|
|
20
20
|
* @returns Whether `source` contains a code-mode call to `functionName`.
|
|
21
21
|
*/
|
|
22
22
|
export declare function hasPhpFunctionCall(source: string, functionName: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Detect a PHP function call whose first argument is a variable previously
|
|
25
|
+
* assigned from a literal string prefix in executable PHP code.
|
|
26
|
+
*/
|
|
27
|
+
export declare function hasPhpFunctionCallWithAssignedStringPrefixArgument(source: string, functionName: string, literalPrefix: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Detect a PHP function call whose first argument is a literal string value.
|
|
30
|
+
*
|
|
31
|
+
* This uses the same code-mode scanner as {@link hasPhpFunctionCall}, so
|
|
32
|
+
* comments, quoted strings, heredoc, and nowdoc content cannot create matches.
|
|
33
|
+
*/
|
|
34
|
+
export declare function hasPhpFunctionCallWithStringArgument(source: string, functionName: string, literalArgument: string): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Detect a PHP function call whose first argument starts with a literal string prefix.
|
|
37
|
+
*
|
|
38
|
+
* Concatenated expressions are accepted here so dynamic WordPress hooks such as
|
|
39
|
+
* `'block_bindings_supported_attributes_' . $block_type` remain detectable.
|
|
40
|
+
*/
|
|
41
|
+
export declare function hasPhpFunctionCallWithStringArgumentPrefix(source: string, functionName: string, literalPrefix: string): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Detect a code-mode PHP string literal that starts with a literal prefix.
|
|
44
|
+
*
|
|
45
|
+
* This is useful for dynamic hook names stored in variables before a later
|
|
46
|
+
* global function call consumes the variable.
|
|
47
|
+
*/
|
|
48
|
+
export declare function hasPhpCodeStringLiteralPrefix(source: string, literalPrefix: string): boolean;
|
|
23
49
|
/**
|
|
24
50
|
* Locate a PHP function body without counting braces in non-code regions.
|
|
25
51
|
*
|
|
@@ -132,6 +132,15 @@ function matchesPhpFunctionCallAt(source, index, functionName) {
|
|
|
132
132
|
if (isPhpIdentifierPart(source[index - 1])) {
|
|
133
133
|
return false;
|
|
134
134
|
}
|
|
135
|
+
let previousCursor = index - 1;
|
|
136
|
+
while (previousCursor >= 0 && /\s/u.test(source[previousCursor] ?? "")) {
|
|
137
|
+
previousCursor -= 1;
|
|
138
|
+
}
|
|
139
|
+
const previousToken = source[previousCursor];
|
|
140
|
+
if ((previousToken === ">" && source[previousCursor - 1] === "-") ||
|
|
141
|
+
(previousToken === ":" && source[previousCursor - 1] === ":")) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
135
144
|
const cursor = index + functionName.length;
|
|
136
145
|
if (isPhpIdentifierPart(source[cursor])) {
|
|
137
146
|
return false;
|
|
@@ -139,6 +148,60 @@ function matchesPhpFunctionCallAt(source, index, functionName) {
|
|
|
139
148
|
const callStart = skipPhpCallTrivia(source, cursor);
|
|
140
149
|
return callStart !== null && source[callStart] === "(";
|
|
141
150
|
}
|
|
151
|
+
function parsePhpQuotedStringLiteralAt(source, index) {
|
|
152
|
+
const quote = source[index];
|
|
153
|
+
if (quote !== "'" && quote !== '"') {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
let cursor = index + 1;
|
|
157
|
+
let value = "";
|
|
158
|
+
while (cursor < source.length) {
|
|
159
|
+
const character = source[cursor];
|
|
160
|
+
if (character === "\\") {
|
|
161
|
+
const escapedCharacter = source[cursor + 1];
|
|
162
|
+
if (escapedCharacter === undefined) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
value += escapedCharacter;
|
|
166
|
+
cursor += 2;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (character === quote) {
|
|
170
|
+
return {
|
|
171
|
+
end: cursor + 1,
|
|
172
|
+
value,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
value += character;
|
|
176
|
+
cursor += 1;
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
function parsePhpVariableNameAt(source, index) {
|
|
181
|
+
if (source[index] !== "$") {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
const nameStart = index + 1;
|
|
185
|
+
if (!isPhpIdentifierStart(source[nameStart])) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
let cursor = nameStart + 1;
|
|
189
|
+
while (isPhpIdentifierPart(source[cursor])) {
|
|
190
|
+
cursor += 1;
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
end: cursor,
|
|
194
|
+
name: source.slice(nameStart, cursor),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function getPhpFunctionCallFirstArgumentStart(source, index, functionName) {
|
|
198
|
+
const cursor = index + functionName.length;
|
|
199
|
+
const callStart = skipPhpCallTrivia(source, cursor);
|
|
200
|
+
if (callStart === null || source[callStart] !== "(") {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
return skipPhpCallTrivia(source, callStart + 1);
|
|
204
|
+
}
|
|
142
205
|
function createPhpScannerState() {
|
|
143
206
|
return {
|
|
144
207
|
heredocDelimiter: "",
|
|
@@ -309,6 +372,211 @@ export function hasPhpFunctionCall(source, functionName) {
|
|
|
309
372
|
}
|
|
310
373
|
return false;
|
|
311
374
|
}
|
|
375
|
+
function isPhpAssignmentOperatorAt(source, index) {
|
|
376
|
+
if (index === null || source[index] !== "=") {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
const previousToken = source[index - 1];
|
|
380
|
+
const nextToken = source[index + 1];
|
|
381
|
+
return (previousToken !== "=" &&
|
|
382
|
+
previousToken !== "!" &&
|
|
383
|
+
previousToken !== "<" &&
|
|
384
|
+
previousToken !== ">" &&
|
|
385
|
+
previousToken !== "." &&
|
|
386
|
+
nextToken !== "=" &&
|
|
387
|
+
nextToken !== ">");
|
|
388
|
+
}
|
|
389
|
+
function isSupportedPhpAssignedStringSuffix(source, index) {
|
|
390
|
+
const nextTokenIndex = skipPhpCallTrivia(source, index);
|
|
391
|
+
const nextToken = nextTokenIndex === null ? undefined : source[nextTokenIndex];
|
|
392
|
+
return nextToken === "." || nextToken === ";" || nextToken === undefined;
|
|
393
|
+
}
|
|
394
|
+
function getPhpFunctionCallFirstVariableArgumentName(source, index, functionName) {
|
|
395
|
+
const argumentStart = getPhpFunctionCallFirstArgumentStart(source, index, functionName);
|
|
396
|
+
if (argumentStart === null) {
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
const variable = parsePhpVariableNameAt(source, argumentStart);
|
|
400
|
+
if (!variable) {
|
|
401
|
+
return undefined;
|
|
402
|
+
}
|
|
403
|
+
const argumentEnd = skipPhpCallTrivia(source, variable.end);
|
|
404
|
+
const nextToken = argumentEnd === null ? undefined : source[argumentEnd];
|
|
405
|
+
return nextToken === "," || nextToken === ")" || nextToken === undefined
|
|
406
|
+
? variable.name
|
|
407
|
+
: undefined;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Detect a PHP function call whose first argument is a variable previously
|
|
411
|
+
* assigned from a literal string prefix in executable PHP code.
|
|
412
|
+
*/
|
|
413
|
+
export function hasPhpFunctionCallWithAssignedStringPrefixArgument(source, functionName, literalPrefix) {
|
|
414
|
+
const variablesWithPrefix = new Set();
|
|
415
|
+
const scanner = createPhpScannerState();
|
|
416
|
+
let index = 0;
|
|
417
|
+
while (index < source.length) {
|
|
418
|
+
const scan = advancePhpScanner(source, index, scanner);
|
|
419
|
+
if (scan.ambiguous) {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
if (!scan.inCode) {
|
|
423
|
+
index = scan.index;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const variable = parsePhpVariableNameAt(source, index);
|
|
427
|
+
if (variable) {
|
|
428
|
+
const assignmentStart = skipPhpCallTrivia(source, variable.end);
|
|
429
|
+
if (assignmentStart !== null &&
|
|
430
|
+
isPhpAssignmentOperatorAt(source, assignmentStart)) {
|
|
431
|
+
const literalStart = skipPhpCallTrivia(source, assignmentStart + 1);
|
|
432
|
+
const literal = literalStart === null
|
|
433
|
+
? null
|
|
434
|
+
: parsePhpQuotedStringLiteralAt(source, literalStart);
|
|
435
|
+
if (literal &&
|
|
436
|
+
literal.value.startsWith(literalPrefix) &&
|
|
437
|
+
isSupportedPhpAssignedStringSuffix(source, literal.end)) {
|
|
438
|
+
variablesWithPrefix.add(variable.name);
|
|
439
|
+
index = literal.end;
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
variablesWithPrefix.delete(variable.name);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (matchesPhpFunctionCallAt(source, index, functionName)) {
|
|
446
|
+
const variableArgumentName = getPhpFunctionCallFirstVariableArgumentName(source, index, functionName);
|
|
447
|
+
if (variableArgumentName &&
|
|
448
|
+
variablesWithPrefix.has(variableArgumentName)) {
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
index += functionName.length;
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
index += 1;
|
|
455
|
+
}
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Detect a PHP function call whose first argument is a literal string value.
|
|
460
|
+
*
|
|
461
|
+
* This uses the same code-mode scanner as {@link hasPhpFunctionCall}, so
|
|
462
|
+
* comments, quoted strings, heredoc, and nowdoc content cannot create matches.
|
|
463
|
+
*/
|
|
464
|
+
export function hasPhpFunctionCallWithStringArgument(source, functionName, literalArgument) {
|
|
465
|
+
return hasPhpFunctionCallWithStringArgumentMatching(source, functionName, (value) => value === literalArgument, { allowConcatenatedPrefix: false });
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Detect a PHP function call whose first argument starts with a literal string prefix.
|
|
469
|
+
*
|
|
470
|
+
* Concatenated expressions are accepted here so dynamic WordPress hooks such as
|
|
471
|
+
* `'block_bindings_supported_attributes_' . $block_type` remain detectable.
|
|
472
|
+
*/
|
|
473
|
+
export function hasPhpFunctionCallWithStringArgumentPrefix(source, functionName, literalPrefix) {
|
|
474
|
+
return hasPhpFunctionCallWithStringArgumentMatching(source, functionName, (value) => value.startsWith(literalPrefix), { allowConcatenatedPrefix: true });
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Detect a code-mode PHP string literal that starts with a literal prefix.
|
|
478
|
+
*
|
|
479
|
+
* This is useful for dynamic hook names stored in variables before a later
|
|
480
|
+
* global function call consumes the variable.
|
|
481
|
+
*/
|
|
482
|
+
export function hasPhpCodeStringLiteralPrefix(source, literalPrefix) {
|
|
483
|
+
let index = 0;
|
|
484
|
+
while (index < source.length) {
|
|
485
|
+
if (source.startsWith("<?php", index)) {
|
|
486
|
+
index += 5;
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (source.startsWith("<?", index) || source.startsWith("?>", index)) {
|
|
490
|
+
index += 2;
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (source[index] === "/" && source[index + 1] === "*") {
|
|
494
|
+
const commentEnd = source.indexOf("*/", index + 2);
|
|
495
|
+
if (commentEnd === -1) {
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
index = commentEnd + 2;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
const literal = parsePhpQuotedStringLiteralAt(source, index);
|
|
502
|
+
if (literal) {
|
|
503
|
+
if (literal.value.startsWith(literalPrefix)) {
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
index = literal.end;
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
if (source[index] === "/" && source[index + 1] === "/") {
|
|
510
|
+
index = findPhpLineBoundary(source, index + 2).nextStart;
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
if (source[index] === "#" && source[index + 1] !== "[") {
|
|
514
|
+
index = findPhpLineBoundary(source, index + 1).nextStart;
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
const heredoc = parsePhpHeredocStart(source, index);
|
|
518
|
+
if (heredoc) {
|
|
519
|
+
let cursor = heredoc.contentStart;
|
|
520
|
+
let closingEnd = null;
|
|
521
|
+
while (cursor < source.length) {
|
|
522
|
+
closingEnd = findPhpHeredocClosingEnd(source, cursor, heredoc.delimiter);
|
|
523
|
+
if (closingEnd !== null) {
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
cursor = findPhpLineBoundary(source, cursor).nextStart;
|
|
527
|
+
}
|
|
528
|
+
if (closingEnd === null) {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
index = findPhpLineBoundary(source, closingEnd).nextStart;
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
index += 1;
|
|
535
|
+
}
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
function hasPhpFunctionCallWithStringArgumentMatching(source, functionName, matchesArgument, options) {
|
|
539
|
+
const scanner = createPhpScannerState();
|
|
540
|
+
let index = 0;
|
|
541
|
+
while (index < source.length) {
|
|
542
|
+
const scan = advancePhpScanner(source, index, scanner);
|
|
543
|
+
if (scan.ambiguous) {
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
if (!scan.inCode) {
|
|
547
|
+
index = scan.index;
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
if (!matchesPhpFunctionCallAt(source, index, functionName)) {
|
|
551
|
+
index += 1;
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
const argumentStart = getPhpFunctionCallFirstArgumentStart(source, index, functionName);
|
|
555
|
+
if (argumentStart === null) {
|
|
556
|
+
index += functionName.length;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
const argument = parsePhpQuotedStringLiteralAt(source, argumentStart);
|
|
560
|
+
if (!argument) {
|
|
561
|
+
index += functionName.length;
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
const argumentEnd = skipPhpCallTrivia(source, argument.end);
|
|
565
|
+
const nextToken = argumentEnd === null ? undefined : source[argumentEnd];
|
|
566
|
+
const isCompleteLiteralArgument = nextToken === "," || nextToken === ")" || nextToken === undefined;
|
|
567
|
+
const isSupportedPrefixExpression = options.allowConcatenatedPrefix && nextToken === ".";
|
|
568
|
+
if (!isCompleteLiteralArgument &&
|
|
569
|
+
!isSupportedPrefixExpression) {
|
|
570
|
+
index += functionName.length;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
if (matchesArgument(argument.value)) {
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
index += functionName.length;
|
|
577
|
+
}
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
312
580
|
/**
|
|
313
581
|
* Locate a PHP function body without counting braces in non-code regions.
|
|
314
582
|
*
|