wesper 0.0.1
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/LICENSE +276 -0
- package/README.md +70 -0
- package/dist/chunk-44KKIZUQ.js +754 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +96 -0
- package/dist/index.d.ts +2392 -0
- package/dist/index.js +28 -0
- package/package.json +68 -0
- package/schemas/site-context-v1.schema.json +465 -0
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var CONTEXT_VERSION = 1;
|
|
3
|
+
var SCHEMA_URL = "https://humanmade.github.io/wesper/schemas/site-context-v1.schema.json";
|
|
4
|
+
|
|
5
|
+
// src/schema.ts
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
8
|
+
var jsonValueSchema = z.lazy(
|
|
9
|
+
() => z.union([
|
|
10
|
+
z.string(),
|
|
11
|
+
z.number(),
|
|
12
|
+
z.boolean(),
|
|
13
|
+
z.null(),
|
|
14
|
+
z.array(jsonValueSchema),
|
|
15
|
+
z.record(z.string(), jsonValueSchema)
|
|
16
|
+
])
|
|
17
|
+
);
|
|
18
|
+
var warningSeveritySchema = z.enum(["info", "warning", "error"]);
|
|
19
|
+
var contextWarningSchema = z.object({
|
|
20
|
+
code: z.string().min(1),
|
|
21
|
+
severity: warningSeveritySchema,
|
|
22
|
+
message: z.string().min(1),
|
|
23
|
+
surface: z.string().min(1)
|
|
24
|
+
});
|
|
25
|
+
var validationIssueSchema = z.object({
|
|
26
|
+
path: z.string(),
|
|
27
|
+
message: z.string()
|
|
28
|
+
});
|
|
29
|
+
var tokenSchema = z.object({
|
|
30
|
+
slug: z.string().min(1),
|
|
31
|
+
name: z.string().optional(),
|
|
32
|
+
value: z.string().min(1)
|
|
33
|
+
}).passthrough();
|
|
34
|
+
var bindingSourceSchema = z.object({
|
|
35
|
+
name: z.string().min(1),
|
|
36
|
+
label: z.string().nullable().optional(),
|
|
37
|
+
usesContext: z.array(z.string()).default([]),
|
|
38
|
+
argsSchema: jsonValueSchema.nullable().default(null)
|
|
39
|
+
}).passthrough();
|
|
40
|
+
var bindingFieldSchema = z.object({
|
|
41
|
+
key: z.string().min(1),
|
|
42
|
+
source: z.string().min(1),
|
|
43
|
+
type: z.string().optional(),
|
|
44
|
+
single: z.boolean().optional(),
|
|
45
|
+
showInRest: z.boolean().optional(),
|
|
46
|
+
bindable: z.boolean().default(true)
|
|
47
|
+
}).passthrough();
|
|
48
|
+
var siteContextSchema = z.object({
|
|
49
|
+
$schema: z.string().url().default(SCHEMA_URL),
|
|
50
|
+
contextVersion: z.literal(CONTEXT_VERSION),
|
|
51
|
+
site: z.object({
|
|
52
|
+
url: z.string().optional(),
|
|
53
|
+
name: z.string().optional(),
|
|
54
|
+
environment: z.enum(["local", "staging", "production", "unknown"]).default("unknown"),
|
|
55
|
+
isMultisite: z.boolean().default(false)
|
|
56
|
+
}).passthrough(),
|
|
57
|
+
provenance: z.object({
|
|
58
|
+
collectedAt: z.string().datetime(),
|
|
59
|
+
collector: z.enum(["wp-cli", "fixture"]),
|
|
60
|
+
collectorVersion: z.string().min(1),
|
|
61
|
+
sourceHash: z.string().regex(/^sha256:[a-f0-9]{64}$/),
|
|
62
|
+
partial: z.boolean().default(false)
|
|
63
|
+
}).passthrough(),
|
|
64
|
+
wordpress: z.object({
|
|
65
|
+
version: z.string().optional(),
|
|
66
|
+
locale: z.string().optional(),
|
|
67
|
+
permalinkStructure: z.string().optional(),
|
|
68
|
+
features: z.record(z.string(), z.boolean()).default({})
|
|
69
|
+
}).passthrough(),
|
|
70
|
+
theme: z.object({
|
|
71
|
+
stylesheet: z.string().optional(),
|
|
72
|
+
template: z.string().optional(),
|
|
73
|
+
name: z.string().optional(),
|
|
74
|
+
version: z.string().optional(),
|
|
75
|
+
isBlockTheme: z.boolean().optional(),
|
|
76
|
+
themeJsonHash: z.string().optional(),
|
|
77
|
+
settingsOrigin: z.enum(["merged", "theme"]).default("merged"),
|
|
78
|
+
tokens: z.object({
|
|
79
|
+
colors: z.array(tokenSchema).default([]),
|
|
80
|
+
spacing: z.array(tokenSchema).default([]),
|
|
81
|
+
typography: z.array(tokenSchema).default([])
|
|
82
|
+
}).default({ colors: [], spacing: [], typography: [] }),
|
|
83
|
+
settings: jsonValueSchema.optional()
|
|
84
|
+
}).passthrough(),
|
|
85
|
+
plugins: z.array(z.record(z.string(), jsonValueSchema)).default([]),
|
|
86
|
+
blocks: z.object({
|
|
87
|
+
types: z.array(z.record(z.string(), jsonValueSchema)).default([])
|
|
88
|
+
}).passthrough(),
|
|
89
|
+
bindings: z.object({
|
|
90
|
+
available: z.boolean().default(false),
|
|
91
|
+
sources: z.array(bindingSourceSchema).default([]),
|
|
92
|
+
supportedAttributes: z.record(z.string(), z.array(z.string())).default({}),
|
|
93
|
+
warnings: z.array(contextWarningSchema).default([])
|
|
94
|
+
}).passthrough(),
|
|
95
|
+
contentModel: z.object({
|
|
96
|
+
postTypes: z.array(
|
|
97
|
+
z.object({
|
|
98
|
+
name: z.string().min(1),
|
|
99
|
+
label: z.string().optional(),
|
|
100
|
+
public: z.boolean().optional(),
|
|
101
|
+
showInRest: z.boolean().optional(),
|
|
102
|
+
taxonomies: z.array(z.string()).default([]),
|
|
103
|
+
fields: z.array(bindingFieldSchema).default([])
|
|
104
|
+
}).passthrough()
|
|
105
|
+
).default([])
|
|
106
|
+
}).passthrough(),
|
|
107
|
+
patterns: z.object({
|
|
108
|
+
items: z.array(z.record(z.string(), jsonValueSchema)).default([])
|
|
109
|
+
}).passthrough(),
|
|
110
|
+
media: z.object({
|
|
111
|
+
imageSizes: z.array(z.record(z.string(), jsonValueSchema)).default([]),
|
|
112
|
+
maxUploadSize: z.number().optional()
|
|
113
|
+
}).passthrough(),
|
|
114
|
+
warnings: z.array(contextWarningSchema).default([])
|
|
115
|
+
}).passthrough();
|
|
116
|
+
var summarySchema = z.object({
|
|
117
|
+
site: z.object({
|
|
118
|
+
url: z.string().optional(),
|
|
119
|
+
wordpressVersion: z.string().optional(),
|
|
120
|
+
theme: z.string().optional(),
|
|
121
|
+
collector: z.string(),
|
|
122
|
+
collectedAt: z.string(),
|
|
123
|
+
sourceHash: z.string()
|
|
124
|
+
}),
|
|
125
|
+
counts: z.object({
|
|
126
|
+
blockTypes: z.number(),
|
|
127
|
+
bindingSources: z.number(),
|
|
128
|
+
postTypes: z.number(),
|
|
129
|
+
bindableFields: z.number(),
|
|
130
|
+
patterns: z.number(),
|
|
131
|
+
plugins: z.number(),
|
|
132
|
+
imageSizes: z.number(),
|
|
133
|
+
warnings: z.number()
|
|
134
|
+
}),
|
|
135
|
+
bindingReadiness: z.object({
|
|
136
|
+
supportedAttributes: z.record(z.string(), z.array(z.string())),
|
|
137
|
+
fieldsByPostType: z.record(z.string(), z.number())
|
|
138
|
+
}),
|
|
139
|
+
warningsBySurface: z.record(z.string(), z.array(contextWarningSchema))
|
|
140
|
+
});
|
|
141
|
+
var siteContextJsonSchema = zodToJsonSchema(siteContextSchema, {
|
|
142
|
+
name: "WesperSiteContextV1",
|
|
143
|
+
$refStrategy: "root",
|
|
144
|
+
target: "jsonSchema7"
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// src/canonical.ts
|
|
148
|
+
import { createHash } from "node:crypto";
|
|
149
|
+
|
|
150
|
+
// src/redact.ts
|
|
151
|
+
var SECRET_KEY_PATTERN = /(^|[_-])(password|passwd|pwd|secret|authorization|cookie|nonce|credential)($|[_-])|app[_-]?password|api[_-]?key|access[_-]?token|refresh[_-]?token|auth[_-]?token|bearer/i;
|
|
152
|
+
var REDACTED = "[REDACTED]";
|
|
153
|
+
function redactSecrets(value) {
|
|
154
|
+
return redact(value);
|
|
155
|
+
}
|
|
156
|
+
function redact(value) {
|
|
157
|
+
if (Array.isArray(value)) {
|
|
158
|
+
return value.map((item) => redact(item));
|
|
159
|
+
}
|
|
160
|
+
if (!value || typeof value !== "object") {
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
const result = {};
|
|
164
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
165
|
+
result[key] = SECRET_KEY_PATTERN.test(key) ? REDACTED : redact(nested);
|
|
166
|
+
}
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/canonical.ts
|
|
171
|
+
function canonicalize(value) {
|
|
172
|
+
return JSON.stringify(sortJson(value));
|
|
173
|
+
}
|
|
174
|
+
function sourceHash(value) {
|
|
175
|
+
const hash = createHash("sha256");
|
|
176
|
+
hash.update(canonicalize(redactSecrets(withoutVolatileProvenance(value))));
|
|
177
|
+
return `sha256:${hash.digest("hex")}`;
|
|
178
|
+
}
|
|
179
|
+
function withoutVolatileProvenance(value) {
|
|
180
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
181
|
+
return value;
|
|
182
|
+
}
|
|
183
|
+
const clone = structuredClone(value);
|
|
184
|
+
const provenance = clone.provenance;
|
|
185
|
+
if (provenance && typeof provenance === "object" && !Array.isArray(provenance)) {
|
|
186
|
+
delete provenance.collectedAt;
|
|
187
|
+
delete provenance.sourceHash;
|
|
188
|
+
}
|
|
189
|
+
return clone;
|
|
190
|
+
}
|
|
191
|
+
function sortJson(value) {
|
|
192
|
+
if (Array.isArray(value)) {
|
|
193
|
+
return value.map((item) => sortJson(item));
|
|
194
|
+
}
|
|
195
|
+
if (!value || typeof value !== "object") {
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
return Object.fromEntries(
|
|
199
|
+
Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, nested]) => [key, sortJson(nested)])
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/theme.ts
|
|
204
|
+
function parseThemeJsonSettings(settings) {
|
|
205
|
+
const tokens = { colors: [], spacing: [], typography: [] };
|
|
206
|
+
if (!settings || typeof settings !== "object") {
|
|
207
|
+
return tokens;
|
|
208
|
+
}
|
|
209
|
+
const typed = settings;
|
|
210
|
+
for (const entry of presetEntries(typed.color?.palette)) {
|
|
211
|
+
if (entry.slug && entry.color) {
|
|
212
|
+
tokens.colors.push(token(entry.slug, entry.color, entry.name));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
for (const entry of presetEntries(typed.typography?.fontFamilies)) {
|
|
216
|
+
if (entry.slug && entry.fontFamily) {
|
|
217
|
+
tokens.typography.push(token(entry.slug, entry.fontFamily, entry.name));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
for (const entry of presetEntries(typed.typography?.fontSizes)) {
|
|
221
|
+
if (entry.slug && entry.size) {
|
|
222
|
+
tokens.typography.push(token(entry.slug, entry.size, entry.name));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
for (const entry of presetEntries(typed.spacing?.spacingSizes)) {
|
|
226
|
+
if (entry.slug && entry.size) {
|
|
227
|
+
tokens.spacing.push(token(entry.slug, entry.size, entry.name));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
tokens.colors.sort(bySlug);
|
|
231
|
+
tokens.spacing.sort(bySlug);
|
|
232
|
+
tokens.typography.sort(bySlug);
|
|
233
|
+
return tokens;
|
|
234
|
+
}
|
|
235
|
+
function themeWarnings(settings) {
|
|
236
|
+
if (!settings || typeof settings !== "object") {
|
|
237
|
+
return [
|
|
238
|
+
{
|
|
239
|
+
code: "theme.settings_unavailable",
|
|
240
|
+
severity: "warning",
|
|
241
|
+
surface: "theme.settings",
|
|
242
|
+
message: "Theme settings could not be normalized."
|
|
243
|
+
}
|
|
244
|
+
];
|
|
245
|
+
}
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
function token(slug, value, name) {
|
|
249
|
+
return name ? { slug, name, value } : { slug, value };
|
|
250
|
+
}
|
|
251
|
+
function presetEntries(collection) {
|
|
252
|
+
if (Array.isArray(collection)) {
|
|
253
|
+
return collection;
|
|
254
|
+
}
|
|
255
|
+
if (!collection || typeof collection !== "object") {
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
return Object.values(collection).flatMap((value) => Array.isArray(value) ? value : []);
|
|
259
|
+
}
|
|
260
|
+
function bySlug(left, right) {
|
|
261
|
+
return left.slug.localeCompare(right.slug);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/warnings.ts
|
|
265
|
+
function allWarnings(context) {
|
|
266
|
+
return [...context.warnings, ...context.bindings.warnings ?? []];
|
|
267
|
+
}
|
|
268
|
+
function actionableWarnings(warnings) {
|
|
269
|
+
return warnings.filter((warning) => warning.severity !== "info");
|
|
270
|
+
}
|
|
271
|
+
function hasActionableWarnings(warnings) {
|
|
272
|
+
return actionableWarnings(warnings).length > 0;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/summary.ts
|
|
276
|
+
function summarize(context) {
|
|
277
|
+
const postTypes = context.contentModel.postTypes ?? [];
|
|
278
|
+
const fieldsByPostType = Object.fromEntries(
|
|
279
|
+
postTypes.map((postType) => [postType.name, postType.fields?.filter((field) => field.bindable).length ?? 0])
|
|
280
|
+
);
|
|
281
|
+
const warnings = allWarnings(context);
|
|
282
|
+
return {
|
|
283
|
+
site: {
|
|
284
|
+
url: context.site.url,
|
|
285
|
+
wordpressVersion: context.wordpress.version,
|
|
286
|
+
theme: context.theme.name ?? context.theme.stylesheet,
|
|
287
|
+
collector: context.provenance.collector,
|
|
288
|
+
collectedAt: context.provenance.collectedAt,
|
|
289
|
+
sourceHash: context.provenance.sourceHash
|
|
290
|
+
},
|
|
291
|
+
counts: {
|
|
292
|
+
blockTypes: context.blocks.types?.length ?? 0,
|
|
293
|
+
bindingSources: context.bindings.sources?.length ?? 0,
|
|
294
|
+
postTypes: postTypes.length,
|
|
295
|
+
bindableFields: Object.values(fieldsByPostType).reduce((sum, count) => sum + count, 0),
|
|
296
|
+
patterns: context.patterns.items?.length ?? 0,
|
|
297
|
+
plugins: context.plugins.length,
|
|
298
|
+
imageSizes: context.media.imageSizes?.length ?? 0,
|
|
299
|
+
warnings: warnings.length
|
|
300
|
+
},
|
|
301
|
+
bindingReadiness: {
|
|
302
|
+
supportedAttributes: context.bindings.supportedAttributes ?? {},
|
|
303
|
+
fieldsByPostType
|
|
304
|
+
},
|
|
305
|
+
warningsBySurface: groupWarnings(warnings)
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function formatSummaryMarkdown(context) {
|
|
309
|
+
const summary = summarize(context);
|
|
310
|
+
const lines = [
|
|
311
|
+
`# Wesper Context Summary`,
|
|
312
|
+
``,
|
|
313
|
+
`- Site: ${summary.site.url ?? "unknown"}`,
|
|
314
|
+
`- WordPress: ${summary.site.wordpressVersion ?? "unknown"}`,
|
|
315
|
+
`- Theme: ${summary.site.theme ?? "unknown"}`,
|
|
316
|
+
`- Collector: ${summary.site.collector}`,
|
|
317
|
+
`- Source hash: ${summary.site.sourceHash}`,
|
|
318
|
+
``,
|
|
319
|
+
`## Counts`,
|
|
320
|
+
``,
|
|
321
|
+
`- Block types: ${summary.counts.blockTypes}`,
|
|
322
|
+
`- Binding sources: ${summary.counts.bindingSources}`,
|
|
323
|
+
`- Post types: ${summary.counts.postTypes}`,
|
|
324
|
+
`- Bindable fields: ${summary.counts.bindableFields}`,
|
|
325
|
+
`- Patterns: ${summary.counts.patterns}`,
|
|
326
|
+
`- Plugins: ${summary.counts.plugins}`,
|
|
327
|
+
`- Image sizes: ${summary.counts.imageSizes}`,
|
|
328
|
+
`- Warnings: ${summary.counts.warnings}`,
|
|
329
|
+
``,
|
|
330
|
+
`## Binding Readiness`,
|
|
331
|
+
``
|
|
332
|
+
];
|
|
333
|
+
for (const [blockName, attributes] of Object.entries(summary.bindingReadiness.supportedAttributes).sort()) {
|
|
334
|
+
lines.push(`- ${blockName}: ${attributes.join(", ") || "none"}`);
|
|
335
|
+
}
|
|
336
|
+
if (Object.keys(summary.bindingReadiness.supportedAttributes).length === 0) {
|
|
337
|
+
lines.push("- No supported binding attributes reported.");
|
|
338
|
+
}
|
|
339
|
+
lines.push("", "## Fields By Post Type", "");
|
|
340
|
+
for (const [postType, count] of Object.entries(summary.bindingReadiness.fieldsByPostType).sort()) {
|
|
341
|
+
lines.push(`- ${postType}: ${count}`);
|
|
342
|
+
}
|
|
343
|
+
if (Object.keys(summary.bindingReadiness.fieldsByPostType).length === 0) {
|
|
344
|
+
lines.push("- No post types reported.");
|
|
345
|
+
}
|
|
346
|
+
if (summary.counts.warnings > 0) {
|
|
347
|
+
lines.push("", "## Warnings", "");
|
|
348
|
+
for (const [surface, warnings] of Object.entries(summary.warningsBySurface).sort()) {
|
|
349
|
+
for (const warning of warnings) {
|
|
350
|
+
lines.push(`- ${surface}: [${warning.code}] ${warning.message}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return `${lines.join("\n")}
|
|
355
|
+
`;
|
|
356
|
+
}
|
|
357
|
+
function groupWarnings(warnings) {
|
|
358
|
+
const grouped = {};
|
|
359
|
+
for (const warning of warnings) {
|
|
360
|
+
grouped[warning.surface] ??= [];
|
|
361
|
+
grouped[warning.surface]?.push(warning);
|
|
362
|
+
}
|
|
363
|
+
return grouped;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// src/collector/wpcli.ts
|
|
367
|
+
import { execFile } from "node:child_process";
|
|
368
|
+
import { promisify } from "node:util";
|
|
369
|
+
var execFileAsync = promisify(execFile);
|
|
370
|
+
var COLLECTOR_VERSION = "0.1.0";
|
|
371
|
+
async function collectWpCli(options) {
|
|
372
|
+
if (!options.wpPath && !options.ssh) {
|
|
373
|
+
throw new Error("WP-CLI collector requires --wp-path or --ssh.");
|
|
374
|
+
}
|
|
375
|
+
const wpBinary = options.wpBinary ?? "wp";
|
|
376
|
+
const args = wpArgs(options, ["eval", PHP_COLLECTOR]);
|
|
377
|
+
const { stdout } = await execFileAsync(wpBinary, args, {
|
|
378
|
+
encoding: "utf8",
|
|
379
|
+
maxBuffer: 1024 * 1024 * 20
|
|
380
|
+
});
|
|
381
|
+
const raw = parseCollectorJson(stdout);
|
|
382
|
+
return normalizeCollectorOutput(raw);
|
|
383
|
+
}
|
|
384
|
+
function wpArgs(options, command) {
|
|
385
|
+
const args = [];
|
|
386
|
+
if (options.ssh) {
|
|
387
|
+
args.push(`--ssh=${options.ssh}`);
|
|
388
|
+
}
|
|
389
|
+
if (options.wpPath) {
|
|
390
|
+
args.push(`--path=${options.wpPath}`);
|
|
391
|
+
}
|
|
392
|
+
if (options.wpUrl) {
|
|
393
|
+
args.push(`--url=${options.wpUrl}`);
|
|
394
|
+
}
|
|
395
|
+
args.push(...command);
|
|
396
|
+
return args;
|
|
397
|
+
}
|
|
398
|
+
function parseCollectorJson(stdout) {
|
|
399
|
+
const trimmed = stdout.trim();
|
|
400
|
+
const start = trimmed.indexOf("{");
|
|
401
|
+
const end = trimmed.lastIndexOf("}");
|
|
402
|
+
if (start === -1 || end === -1 || end <= start) {
|
|
403
|
+
throw new Error("WP-CLI collector did not return JSON.");
|
|
404
|
+
}
|
|
405
|
+
return JSON.parse(trimmed.slice(start, end + 1));
|
|
406
|
+
}
|
|
407
|
+
function normalizeCollectorOutput(raw) {
|
|
408
|
+
const warnings = warningArray(raw.warnings);
|
|
409
|
+
const bindingsRaw = record(raw.bindings);
|
|
410
|
+
const themeRaw = record(raw.theme);
|
|
411
|
+
const settings = themeRaw.settings;
|
|
412
|
+
warnings.push(...themeWarnings(settings));
|
|
413
|
+
const contextWithoutHash = redactSecrets({
|
|
414
|
+
$schema: SCHEMA_URL,
|
|
415
|
+
contextVersion: CONTEXT_VERSION,
|
|
416
|
+
site: record(raw.site),
|
|
417
|
+
provenance: {
|
|
418
|
+
collectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
419
|
+
collector: "wp-cli",
|
|
420
|
+
collectorVersion: COLLECTOR_VERSION,
|
|
421
|
+
sourceHash: "sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
|
422
|
+
partial: warnings.some((warning) => warning.severity !== "info")
|
|
423
|
+
},
|
|
424
|
+
wordpress: record(raw.wordpress),
|
|
425
|
+
theme: {
|
|
426
|
+
...themeRaw,
|
|
427
|
+
settingsOrigin: "merged",
|
|
428
|
+
themeJsonHash: settings ? sourceHash({ settings }) : void 0,
|
|
429
|
+
tokens: parseThemeJsonSettings(settings),
|
|
430
|
+
settings
|
|
431
|
+
},
|
|
432
|
+
plugins: array(raw.plugins),
|
|
433
|
+
blocks: {
|
|
434
|
+
types: sortByName(array(record(raw.blocks).types))
|
|
435
|
+
},
|
|
436
|
+
bindings: {
|
|
437
|
+
available: Boolean(bindingsRaw.available),
|
|
438
|
+
sources: sortByName(array(bindingsRaw.sources)),
|
|
439
|
+
supportedAttributes: sortSupportedAttributes(record(bindingsRaw.supportedAttributes)),
|
|
440
|
+
warnings: warningArray(bindingsRaw.warnings)
|
|
441
|
+
},
|
|
442
|
+
contentModel: {
|
|
443
|
+
postTypes: sortPostTypes(array(record(raw.contentModel).postTypes))
|
|
444
|
+
},
|
|
445
|
+
patterns: {
|
|
446
|
+
items: sortByName(array(record(raw.patterns).items))
|
|
447
|
+
},
|
|
448
|
+
media: record(raw.media),
|
|
449
|
+
warnings
|
|
450
|
+
});
|
|
451
|
+
const context = {
|
|
452
|
+
...contextWithoutHash,
|
|
453
|
+
provenance: {
|
|
454
|
+
...contextWithoutHash.provenance,
|
|
455
|
+
sourceHash: sourceHash(contextWithoutHash)
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
return siteContextSchema.parse(context);
|
|
459
|
+
}
|
|
460
|
+
function warningArray(value) {
|
|
461
|
+
if (!Array.isArray(value)) {
|
|
462
|
+
return [];
|
|
463
|
+
}
|
|
464
|
+
return value.filter((warning) => {
|
|
465
|
+
return Boolean(
|
|
466
|
+
warning && typeof warning === "object" && typeof warning.code === "string" && typeof warning.severity === "string" && typeof warning.message === "string" && typeof warning.surface === "string"
|
|
467
|
+
);
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
function record(value) {
|
|
471
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
472
|
+
}
|
|
473
|
+
function array(value) {
|
|
474
|
+
return Array.isArray(value) ? value.filter((item) => item && typeof item === "object") : [];
|
|
475
|
+
}
|
|
476
|
+
function sortByName(items) {
|
|
477
|
+
return [...items].sort((left, right) => String(left.name ?? "").localeCompare(String(right.name ?? "")));
|
|
478
|
+
}
|
|
479
|
+
function sortPostTypes(items) {
|
|
480
|
+
return sortByName(items).map((postType) => ({
|
|
481
|
+
...postType,
|
|
482
|
+
fields: sortByName(array(postType.fields)),
|
|
483
|
+
taxonomies: Array.isArray(postType.taxonomies) ? [...postType.taxonomies].sort() : []
|
|
484
|
+
}));
|
|
485
|
+
}
|
|
486
|
+
function sortSupportedAttributes(value) {
|
|
487
|
+
return Object.fromEntries(
|
|
488
|
+
Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([blockName, attributes]) => [
|
|
489
|
+
blockName,
|
|
490
|
+
Array.isArray(attributes) ? attributes.map(String).sort() : []
|
|
491
|
+
])
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
var PHP_COLLECTOR = String.raw`
|
|
495
|
+
$warnings = array();
|
|
496
|
+
|
|
497
|
+
function wesper_warning($code, $surface, $message, $severity = 'warning') {
|
|
498
|
+
return array(
|
|
499
|
+
'code' => $code,
|
|
500
|
+
'severity' => $severity,
|
|
501
|
+
'surface' => $surface,
|
|
502
|
+
'message' => $message,
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function wesper_public_props($object, $props) {
|
|
507
|
+
$out = array();
|
|
508
|
+
foreach ($props as $from => $to) {
|
|
509
|
+
if (is_int($from)) {
|
|
510
|
+
$from = $to;
|
|
511
|
+
}
|
|
512
|
+
if (is_object($object) && isset($object->{$from})) {
|
|
513
|
+
$out[$to] = $object->{$from};
|
|
514
|
+
} elseif (is_array($object) && array_key_exists($from, $object)) {
|
|
515
|
+
$out[$to] = $object[$from];
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return $out;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
global $wp_version;
|
|
522
|
+
|
|
523
|
+
$theme = wp_get_theme();
|
|
524
|
+
$settings = function_exists('wp_get_global_settings') ? wp_get_global_settings() : array();
|
|
525
|
+
|
|
526
|
+
$plugins = array();
|
|
527
|
+
if (!function_exists('get_plugin_data')) {
|
|
528
|
+
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
|
529
|
+
}
|
|
530
|
+
$active_plugins = (array) get_option('active_plugins', array());
|
|
531
|
+
$network_plugins = is_multisite() ? array_keys((array) get_site_option('active_sitewide_plugins', array())) : array();
|
|
532
|
+
foreach (array_values(array_unique(array_merge($active_plugins, $network_plugins))) as $plugin_file) {
|
|
533
|
+
$plugin_path = WP_PLUGIN_DIR . '/' . $plugin_file;
|
|
534
|
+
$data = file_exists($plugin_path) ? get_plugin_data($plugin_path, false, false) : array();
|
|
535
|
+
$plugins[] = array(
|
|
536
|
+
'slug' => $plugin_file,
|
|
537
|
+
'name' => isset($data['Name']) && $data['Name'] ? $data['Name'] : $plugin_file,
|
|
538
|
+
'version' => isset($data['Version']) ? $data['Version'] : '',
|
|
539
|
+
'active' => in_array($plugin_file, $active_plugins, true),
|
|
540
|
+
'networkActive' => in_array($plugin_file, $network_plugins, true),
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
$block_types = array();
|
|
545
|
+
foreach (WP_Block_Type_Registry::get_instance()->get_all_registered() as $name => $block_type) {
|
|
546
|
+
$block_types[] = array(
|
|
547
|
+
'name' => $name,
|
|
548
|
+
'apiVersion' => isset($block_type->api_version) ? $block_type->api_version : null,
|
|
549
|
+
'title' => isset($block_type->title) ? $block_type->title : null,
|
|
550
|
+
'category' => isset($block_type->category) ? $block_type->category : null,
|
|
551
|
+
'attributes' => isset($block_type->attributes) ? $block_type->attributes : array(),
|
|
552
|
+
'supports' => isset($block_type->supports) ? $block_type->supports : array(),
|
|
553
|
+
'source' => strpos($name, 'core/') === 0 ? 'core' : 'plugin',
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
$binding_sources = array();
|
|
558
|
+
$bindings_available = function_exists('get_all_registered_block_bindings_sources');
|
|
559
|
+
if ($bindings_available) {
|
|
560
|
+
foreach (get_all_registered_block_bindings_sources() as $name => $source) {
|
|
561
|
+
$binding_sources[] = array(
|
|
562
|
+
'name' => $name,
|
|
563
|
+
'label' => isset($source->label) ? $source->label : null,
|
|
564
|
+
'usesContext' => isset($source->uses_context) ? array_values((array) $source->uses_context) : array(),
|
|
565
|
+
'argsSchema' => null,
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
} else {
|
|
569
|
+
$warnings[] = wesper_warning('bindings.unavailable', 'bindings', 'Block Bindings are unavailable before WordPress 6.5.');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
$core_supported_attributes = array(
|
|
573
|
+
'core/paragraph' => array('content'),
|
|
574
|
+
'core/heading' => array('content'),
|
|
575
|
+
'core/image' => array('id', 'url', 'title', 'alt', 'caption'),
|
|
576
|
+
'core/button' => array('url', 'text', 'linkTarget', 'rel'),
|
|
577
|
+
'core/post-date' => array('datetime'),
|
|
578
|
+
'core/navigation-link' => array('url'),
|
|
579
|
+
'core/navigation-submenu' => array('url'),
|
|
580
|
+
);
|
|
581
|
+
$supported_attributes = array();
|
|
582
|
+
if (function_exists('get_block_bindings_supported_attributes')) {
|
|
583
|
+
foreach ($block_types as $block_type) {
|
|
584
|
+
$attrs = get_block_bindings_supported_attributes($block_type['name']);
|
|
585
|
+
if (!empty($attrs)) {
|
|
586
|
+
$supported_attributes[$block_type['name']] = array_values($attrs);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
} else {
|
|
590
|
+
$supported_attributes = $core_supported_attributes;
|
|
591
|
+
$warnings[] = wesper_warning('bindings.supported_attributes_partial', 'bindings.supportedAttributes', 'WordPress does not expose get_block_bindings_supported_attributes(); using documented core compatibility table.');
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
$post_types = array();
|
|
595
|
+
foreach (get_post_types(array(), 'objects') as $post_type_name => $post_type_object) {
|
|
596
|
+
$fields = array(
|
|
597
|
+
array('key' => 'title', 'source' => 'core/post-data', 'type' => 'string', 'bindable' => true),
|
|
598
|
+
array('key' => 'excerpt', 'source' => 'core/post-data', 'type' => 'string', 'bindable' => true),
|
|
599
|
+
array('key' => 'featured_media', 'source' => 'core/post-data', 'type' => 'integer', 'bindable' => true),
|
|
600
|
+
);
|
|
601
|
+
$registered_meta = function_exists('get_registered_meta_keys') ? get_registered_meta_keys('post', $post_type_name) : array();
|
|
602
|
+
foreach ($registered_meta as $meta_key => $args) {
|
|
603
|
+
$show_in_rest = !empty($args['show_in_rest']);
|
|
604
|
+
$protected = isset($args['protected']) ? (bool) $args['protected'] : strpos((string) $meta_key, '_') === 0;
|
|
605
|
+
if (!$show_in_rest || $protected) {
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
$fields[] = array(
|
|
609
|
+
'key' => (string) $meta_key,
|
|
610
|
+
'source' => 'core/post-meta',
|
|
611
|
+
'type' => isset($args['type']) ? (string) $args['type'] : 'string',
|
|
612
|
+
'single' => isset($args['single']) ? (bool) $args['single'] : false,
|
|
613
|
+
'showInRest' => true,
|
|
614
|
+
'bindable' => true,
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
if (empty($registered_meta)) {
|
|
618
|
+
$warnings[] = wesper_warning('content_model.no_registered_meta', 'contentModel.postTypes.' . $post_type_name . '.fields', 'No registered REST-visible meta was discovered for this post type.', 'info');
|
|
619
|
+
}
|
|
620
|
+
$post_types[] = array(
|
|
621
|
+
'name' => $post_type_name,
|
|
622
|
+
'label' => $post_type_object->label,
|
|
623
|
+
'public' => (bool) $post_type_object->public,
|
|
624
|
+
'showInRest' => (bool) $post_type_object->show_in_rest,
|
|
625
|
+
'taxonomies' => array_values(get_object_taxonomies($post_type_name)),
|
|
626
|
+
'fields' => $fields,
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
$patterns = array();
|
|
631
|
+
if (class_exists('WP_Block_Patterns_Registry')) {
|
|
632
|
+
foreach (WP_Block_Patterns_Registry::get_instance()->get_all_registered() as $name => $pattern) {
|
|
633
|
+
$patterns[] = array(
|
|
634
|
+
'name' => $name,
|
|
635
|
+
'title' => isset($pattern['title']) ? $pattern['title'] : null,
|
|
636
|
+
'categories' => isset($pattern['categories']) ? array_values((array) $pattern['categories']) : array(),
|
|
637
|
+
'blockTypes' => isset($pattern['blockTypes']) ? array_values((array) $pattern['blockTypes']) : array(),
|
|
638
|
+
'postTypes' => isset($pattern['postTypes']) ? array_values((array) $pattern['postTypes']) : array(),
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
$output = array(
|
|
644
|
+
'site' => array(
|
|
645
|
+
'url' => get_bloginfo('url'),
|
|
646
|
+
'name' => get_bloginfo('name'),
|
|
647
|
+
'environment' => function_exists('wp_get_environment_type') ? wp_get_environment_type() : 'unknown',
|
|
648
|
+
'isMultisite' => is_multisite(),
|
|
649
|
+
),
|
|
650
|
+
'wordpress' => array(
|
|
651
|
+
'version' => $wp_version,
|
|
652
|
+
'locale' => get_locale(),
|
|
653
|
+
'permalinkStructure' => (string) get_option('permalink_structure'),
|
|
654
|
+
'features' => array(
|
|
655
|
+
'blockBindings' => $bindings_available,
|
|
656
|
+
'blockBindingsSupportedAttributesApi' => function_exists('get_block_bindings_supported_attributes'),
|
|
657
|
+
'patterns' => class_exists('WP_Block_Patterns_Registry'),
|
|
658
|
+
),
|
|
659
|
+
),
|
|
660
|
+
'theme' => array(
|
|
661
|
+
'stylesheet' => $theme->get_stylesheet(),
|
|
662
|
+
'template' => $theme->get_template(),
|
|
663
|
+
'name' => (string) $theme->get('Name'),
|
|
664
|
+
'version' => (string) $theme->get('Version'),
|
|
665
|
+
'isBlockTheme' => function_exists('wp_is_block_theme') ? wp_is_block_theme() : false,
|
|
666
|
+
'settings' => $settings,
|
|
667
|
+
),
|
|
668
|
+
'plugins' => $plugins,
|
|
669
|
+
'blocks' => array('types' => $block_types),
|
|
670
|
+
'bindings' => array(
|
|
671
|
+
'available' => $bindings_available,
|
|
672
|
+
'sources' => $binding_sources,
|
|
673
|
+
'supportedAttributes' => $supported_attributes,
|
|
674
|
+
'warnings' => array(),
|
|
675
|
+
),
|
|
676
|
+
'contentModel' => array('postTypes' => $post_types),
|
|
677
|
+
'patterns' => array('items' => $patterns),
|
|
678
|
+
'media' => array(
|
|
679
|
+
'imageSizes' => function_exists('wp_get_registered_image_subsizes') ? array_values(array_map(function($name, $size) {
|
|
680
|
+
return array(
|
|
681
|
+
'name' => $name,
|
|
682
|
+
'width' => isset($size['width']) ? $size['width'] : 0,
|
|
683
|
+
'height' => isset($size['height']) ? $size['height'] : 0,
|
|
684
|
+
'crop' => isset($size['crop']) ? (bool) $size['crop'] : false,
|
|
685
|
+
);
|
|
686
|
+
}, array_keys(wp_get_registered_image_subsizes()), wp_get_registered_image_subsizes())) : array(),
|
|
687
|
+
'maxUploadSize' => function_exists('wp_max_upload_size') ? wp_max_upload_size() : null,
|
|
688
|
+
),
|
|
689
|
+
'warnings' => $warnings,
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
echo wp_json_encode($output);
|
|
693
|
+
`;
|
|
694
|
+
|
|
695
|
+
// src/index.ts
|
|
696
|
+
async function collect(options) {
|
|
697
|
+
const collector = options.collector ?? "wp-cli";
|
|
698
|
+
switch (collector) {
|
|
699
|
+
case "wp-cli":
|
|
700
|
+
return enforceStrict(await collectWpCli({ ...options, collector }), options);
|
|
701
|
+
case "fixture":
|
|
702
|
+
throw new Error("Fixture collection is represented by validate() on a manifest file.");
|
|
703
|
+
default:
|
|
704
|
+
throw new Error(`Unsupported collector: ${String(collector)}`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
function validate(manifest) {
|
|
708
|
+
const redacted = redactSecrets(manifest);
|
|
709
|
+
const result = siteContextSchema.safeParse(redacted);
|
|
710
|
+
if (!result.success) {
|
|
711
|
+
return {
|
|
712
|
+
ok: false,
|
|
713
|
+
errors: issuesFromZod(result.error),
|
|
714
|
+
warnings: []
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
return {
|
|
718
|
+
ok: true,
|
|
719
|
+
context: result.data,
|
|
720
|
+
errors: [],
|
|
721
|
+
warnings: allWarnings(result.data)
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
function enforceStrict(context, options) {
|
|
725
|
+
const warnings = actionableWarnings(allWarnings(context));
|
|
726
|
+
if (options.strict && (context.provenance.partial || warnings.length > 0)) {
|
|
727
|
+
const surfaces = warnings.map((warning) => warning.surface).join(", ") || "provenance.partial";
|
|
728
|
+
throw new Error(`Strict collection failed because the manifest is partial or has actionable warnings: ${surfaces}`);
|
|
729
|
+
}
|
|
730
|
+
return context;
|
|
731
|
+
}
|
|
732
|
+
function issuesFromZod(error) {
|
|
733
|
+
return error.issues.map((issue) => ({
|
|
734
|
+
path: issue.path.join("."),
|
|
735
|
+
message: issue.message
|
|
736
|
+
}));
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
export {
|
|
740
|
+
CONTEXT_VERSION,
|
|
741
|
+
SCHEMA_URL,
|
|
742
|
+
siteContextSchema,
|
|
743
|
+
siteContextJsonSchema,
|
|
744
|
+
redactSecrets,
|
|
745
|
+
canonicalize,
|
|
746
|
+
sourceHash,
|
|
747
|
+
parseThemeJsonSettings,
|
|
748
|
+
allWarnings,
|
|
749
|
+
hasActionableWarnings,
|
|
750
|
+
summarize,
|
|
751
|
+
formatSummaryMarkdown,
|
|
752
|
+
collect,
|
|
753
|
+
validate
|
|
754
|
+
};
|