@wp-typia/project-tools 0.24.8 → 0.24.10
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,15 +4,40 @@ 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 = {
|
|
10
12
|
featureMinimum: "wp-typia.workspace.wordpress.feature-minimum",
|
|
11
13
|
testedTarget: "wp-typia.workspace.wordpress.tested-target",
|
|
12
14
|
};
|
|
15
|
+
// Both entries read the same `variations` field and are distinguished by value type.
|
|
16
|
+
const BLOCK_VARIATION_BLOCK_JSON_KEYS = {
|
|
17
|
+
registrationBlockJsonMetadata: "variations",
|
|
18
|
+
registrationMetadataFile: "variations",
|
|
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_";
|
|
13
26
|
function isEnabledMetadataValue(value) {
|
|
14
27
|
return value !== undefined && value !== false && value !== null;
|
|
15
28
|
}
|
|
29
|
+
function assertNeverBlockVariationFeature(feature) {
|
|
30
|
+
throw new Error(`Unhandled block variation metadata feature "${String(feature)}".`);
|
|
31
|
+
}
|
|
32
|
+
function isEnabledBlockVariationMetadataFeature(feature, value) {
|
|
33
|
+
if (feature === "registrationBlockJsonMetadata") {
|
|
34
|
+
return Array.isArray(value) && value.length > 0;
|
|
35
|
+
}
|
|
36
|
+
if (feature === "registrationMetadataFile") {
|
|
37
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
38
|
+
}
|
|
39
|
+
return assertNeverBlockVariationFeature(feature);
|
|
40
|
+
}
|
|
16
41
|
function getNestedMetadataValue(object, key) {
|
|
17
42
|
if (!object) {
|
|
18
43
|
return undefined;
|
|
@@ -66,6 +91,294 @@ function readExistingTextFile(filePath) {
|
|
|
66
91
|
}
|
|
67
92
|
return fs.readFileSync(filePath, "utf8");
|
|
68
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
|
+
}
|
|
69
382
|
function collectBlockMetadataRequirements(workspace, inventory) {
|
|
70
383
|
const issues = [];
|
|
71
384
|
const requirements = [];
|
|
@@ -101,12 +414,47 @@ function collectBlockMetadataRequirements(workspace, inventory) {
|
|
|
101
414
|
pushBlockApiRequirement(requirements, `Block ${block.slug}`, entry);
|
|
102
415
|
}
|
|
103
416
|
}
|
|
417
|
+
for (const [feature, entry] of Object.entries(WORDPRESS_BLOCK_API_COMPATIBILITY.blockVariations)) {
|
|
418
|
+
if (!entry.runtime.includes("block-json")) {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
const blockJsonFeature = feature;
|
|
422
|
+
const blockJsonKey = BLOCK_VARIATION_BLOCK_JSON_KEYS[blockJsonFeature];
|
|
423
|
+
if (isEnabledBlockVariationMetadataFeature(blockJsonFeature, getNestedMetadataValue(blockJson, blockJsonKey))) {
|
|
424
|
+
pushBlockApiRequirement(requirements, `Block ${block.slug}`, entry);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
104
427
|
}
|
|
105
428
|
return {
|
|
106
429
|
issues,
|
|
107
430
|
requirements,
|
|
108
431
|
};
|
|
109
432
|
}
|
|
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) {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
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));
|
|
443
|
+
}
|
|
444
|
+
function collectVariationRequirements(workspace, inventory) {
|
|
445
|
+
const requirements = [];
|
|
446
|
+
const variationEntries = WORDPRESS_BLOCK_API_COMPATIBILITY.blockVariations;
|
|
447
|
+
for (const variation of inventory.variations) {
|
|
448
|
+
pushBlockApiRequirement(requirements, `Variation ${variation.block}/${variation.slug}`, variationEntries.editorRegistration);
|
|
449
|
+
}
|
|
450
|
+
if (hasGeneratedCoreVariationRegistry(workspace.projectDir)) {
|
|
451
|
+
pushBlockApiRequirement(requirements, "Core variations editor plugin", variationEntries.editorRegistration);
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
issues: [],
|
|
455
|
+
requirements,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
110
458
|
function collectInventoryCompatibilityRequirements(inventory) {
|
|
111
459
|
const requirements = [];
|
|
112
460
|
for (const ability of inventory.abilities) {
|
|
@@ -139,11 +487,14 @@ function collectBindingSourceRequirements(workspace, inventory) {
|
|
|
139
487
|
pushBlockApiRequirement(requirements, `Binding source ${bindingSource.slug}`, bindingEntries.serverRegistration);
|
|
140
488
|
pushBlockApiRequirement(requirements, `Binding source ${bindingSource.slug}`, bindingEntries.editorRegistration);
|
|
141
489
|
const editorFilePath = path.join(workspace.projectDir, bindingSource.editorFile);
|
|
142
|
-
|
|
490
|
+
const editorSource = readExistingTextFile(editorFilePath);
|
|
491
|
+
if (editorSource &&
|
|
492
|
+
hasRegisterBlockBindingsSourceGetFieldsList(editorSource)) {
|
|
143
493
|
pushBlockApiRequirement(requirements, `Binding source ${bindingSource.slug}`, bindingEntries.editorFieldsList);
|
|
144
494
|
}
|
|
145
495
|
const serverFilePath = path.join(workspace.projectDir, bindingSource.serverFile);
|
|
146
|
-
|
|
496
|
+
const serverSource = readExistingTextFile(serverFilePath);
|
|
497
|
+
if (serverSource && hasSupportedAttributesFilterRegistration(serverSource)) {
|
|
147
498
|
pushBlockApiRequirement(requirements, `Binding source ${bindingSource.slug}`, bindingEntries.supportedAttributesFilter);
|
|
148
499
|
}
|
|
149
500
|
}
|
|
@@ -154,16 +505,19 @@ function collectBindingSourceRequirements(workspace, inventory) {
|
|
|
154
505
|
}
|
|
155
506
|
function collectWordPressVersionRequirements(workspace, inventory) {
|
|
156
507
|
const blockRequirements = collectBlockMetadataRequirements(workspace, inventory);
|
|
508
|
+
const variationRequirements = collectVariationRequirements(workspace, inventory);
|
|
157
509
|
const bindingRequirements = collectBindingSourceRequirements(workspace, inventory);
|
|
158
510
|
const inventoryRequirements = collectInventoryCompatibilityRequirements(inventory);
|
|
159
511
|
return {
|
|
160
512
|
issues: [
|
|
161
513
|
...blockRequirements.issues,
|
|
514
|
+
...variationRequirements.issues,
|
|
162
515
|
...bindingRequirements.issues,
|
|
163
516
|
...inventoryRequirements.issues,
|
|
164
517
|
],
|
|
165
518
|
requirements: [
|
|
166
519
|
...blockRequirements.requirements,
|
|
520
|
+
...variationRequirements.requirements,
|
|
167
521
|
...bindingRequirements.requirements,
|
|
168
522
|
...inventoryRequirements.requirements,
|
|
169
523
|
],
|
|
@@ -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
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wp-typia/project-tools",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.10",
|
|
4
4
|
"description": "Project orchestration and programmatic tooling for wp-typia",
|
|
5
5
|
"packageManager": "bun@1.3.11",
|
|
6
6
|
"type": "module",
|
|
@@ -168,7 +168,7 @@
|
|
|
168
168
|
"@wp-typia/api-client": "^0.4.6",
|
|
169
169
|
"@wp-typia/block-runtime": "^0.7.1",
|
|
170
170
|
"@wp-typia/rest": "^0.3.14",
|
|
171
|
-
"@wp-typia/block-types": "^0.3.
|
|
171
|
+
"@wp-typia/block-types": "^0.3.5",
|
|
172
172
|
"mustache": "^4.2.0",
|
|
173
173
|
"npm-package-arg": "^13.0.0",
|
|
174
174
|
"semver": "^7.7.3",
|