Setting the file. One moment. Wp Route Classifier · Rp Source Wordpress · wix/skills · Skills DocsThis file
- Number
- 18.3
- Position
- 3 of 4
- Type
- JavaScript
- Size
- 18 KB
- Lines
- 444
lib/wp-route-classifier.js
JavaScript·444 lines·18 KB
'excluded_integration'
,
13 'excluded_marketplace_setup',
14 'excluded_unsupported',
15]);
16
17const FRONTEND_RULES = [
18 ['/wp/v2/pages', 'wp.frontend.pages', 'site composition pages are outside backend data scope'],
19 ['/wp/v2/navigation', 'wp.frontend.navigation', 'navigation is site composition, not backend data'],
20 ['/wp/v2/menus', 'wp.frontend.menus', 'menus are site composition, not backend data'],
21 ['/wp/v2/menu-items', 'wp.frontend.menu-items', 'menu items are site composition, not backend data'],
22 ['/wp/v2/menu-locations', 'wp.frontend.menu-locations', 'menu locations are presentation configuration'],
23 ['/wp/v2/search', 'wp.frontend.search', 'search is a frontend lookup helper'],
24 ['/oembed/1.0/*', 'wp.frontend.oembed', 'oEmbed routes are frontend embedding helpers'],
25];
26
27const TEMPLATE_EDITOR_RULES = [
28 ['/wp/v2/templates*', 'wp.editor.templates', 'templates are theme/editor construction state'],
29 ['/wp/v2/template-parts*', 'wp.editor.template-parts', 'template parts are theme/editor construction state'],
30 ['/wp/v2/blocks', 'wp.editor.blocks', 'blocks are editor construction state'],
31 ['/wp/v2/block-types', 'wp.editor.block-types', 'block types are editor metadata'],
32 ['/wp/v2/block-directory/*', 'wp.editor.block-directory', 'block directory routes are editor helpers'],
33 ['/wp/v2/block-patterns/*', 'wp.editor.block-patterns', 'block pattern routes are editor helpers'],
34 ['/wp/v2/pattern-directory/*', 'wp.editor.pattern-directory', 'pattern directory routes are editor helpers'],
35 ['/wp/v2/wp_pattern_category', 'wp.editor.pattern-category', 'pattern categories are editor helpers'],
36 ['/wp-block-editor/*', 'wp.editor.namespace', 'block editor namespace is outside backend data scope'],
37 ['/wp/v2/widgets', 'wp.editor.widgets', 'widgets are presentation configuration'],
38 ['/wp/v2/widget-types', 'wp.editor.widget-types', 'widget types are presentation metadata'],
39 ['/wp/v2/sidebars', 'wp.editor.sidebars', 'sidebars are presentation configuration'],
40 ['/wp/v2/themes', 'wp.editor.themes', 'themes are presentation configuration'],
41 ['/wp/v2/font-*', 'wp.editor.fonts', 'font routes describe presentation assets'],
42 ['/wp/v2/icons', 'wp.editor.icons', 'icons describe presentation assets'],
43];
44
45const RUNTIME_RULES = [
46 ['/wc/store/cart*', 'wc.runtime.cart', 'store cart routes are customer-session state'],
47 ['/wc/store/checkout*', 'wc.runtime.checkout', 'store checkout routes are customer-session state'],
48 ['/wc/store/v1/cart*', 'wc.runtime.v1.cart', 'store cart routes are customer-session state'],
49 ['/wc/store/v1/checkout*', 'wc.runtime.v1.checkout', 'store checkout routes are customer-session state'],
50];
51
52const ADMIN_RULES = [
53 ['/wc-admin/*', 'wc.admin.namespace', 'WooCommerce admin routes are dashboard surfaces'],
54 ['/wc-analytics/*', 'wc.admin.analytics', 'WooCommerce analytics routes are dashboard/reporting surfaces'],
55 ['/wc/v1/reports*', 'wc.admin.reports.v1', 'WooCommerce reports are analytics surfaces'],
56 ['/wc/v2/reports*', 'wc.admin.reports.v2', 'WooCommerce reports are analytics surfaces'],
57 ['/wc/v3/reports*', 'wc.admin.reports.v3', 'WooCommerce reports are analytics surfaces'],
58];
59
60const MARKETPLACE_RULES = [
61 ['/wc/v3/marketplace/*', 'wc.setup.marketplace', 'WooCommerce marketplace routes are setup surfaces'],
62 ['/wc/v3/system_status*', 'wc.setup.system-status', 'WooCommerce system status routes are operational diagnostics'],
63 ['/wc/v3/settings', 'wc.setup.settings', 'WooCommerce settings are setup/configuration surfaces'],
64 ['/wc/v3/wc_paypal/*', 'wc.setup.paypal', 'WooCommerce PayPal routes are provider setup surfaces'],
65 ['/wc/v3/wc_stripe/*', 'wc.setup.stripe', 'WooCommerce Stripe routes are provider setup surfaces'],
66 ['/wccom-site/*', 'wc.setup.wccom-site', 'WooCommerce.com routes are marketplace/setup surfaces'],
67 ['/wc/gla/*', 'wc.setup.google-listings', 'Google Listings and Ads routes are setup/reporting surfaces'],
68];
69
70const DIAGNOSTIC_RULES = [
71 ['/wp-site-health/*', 'wp.diagnostics.site-health', 'site health routes are diagnostics'],
72 ['/wp-abilities/*', 'wp.diagnostics.abilities', 'abilities routes are capability discovery'],
73 ['/serviceapp/*', 'wp.diagnostics.serviceapp', 'service app routes are operational surfaces'],
74 ['/vip/*', 'wp.diagnostics.vip', 'VIP routes are operational surfaces'],
75];
76
77const INTEGRATION_RULES = [
78 ['/jetpack/*', 'wp.integration.jetpack', 'Jetpack routes are external service or site operations surfaces'],
79 ['/post-smtp/*', 'wp.integration.post-smtp', 'SMTP routes are mail operations surfaces'],
80 ['/redirection/*', 'wp.integration.redirection', 'redirection routes are operational helper surfaces'],
81 ['/yoast/*', 'wp.integration.yoast', 'SEO helper routes are outside backend data scope'],
82 ['/klaviyo/*', 'wp.integration.klaviyo', 'marketing integration routes are outside backend data scope'],
83 ['/kb-fluentcrm/*', 'wp.integration.fluentcrm', 'marketing integration routes are outside backend data scope'],
84 ['/kb-getresponse/*', 'wp.integration.getresponse', 'marketing integration routes are outside backend data scope'],
85 ['/kb-mailerlite/*', 'wp.integration.mailerlite', 'marketing integration routes are outside backend data scope'],
86 ['/kb-design-library/*', 'wp.integration.design-library', 'design library routes are presentation helpers'],
87 ['/kbp/*', 'wp.integration.kbp', 'AI or pattern helper routes are outside backend data scope'],
88];
89
90const BACKEND_DATA_RULES = [
91 ['/wp/v2/posts', 'wp.data.posts', 'posts are durable content records'],
92 ['/wp/v2/media', 'wp.data.media', 'media are durable asset records'],
93 ['/wp/v2/categories', 'wp.data.categories', 'categories are durable taxonomy records'],
94 ['/wp/v2/tags', 'wp.data.tags', 'tags are durable taxonomy records'],
95 ['/wp/v2/comments', 'wp.data.comments', 'comments are durable content records'],
96 ['/wc/v3/products/attributes*', 'wc.data.product-attributes', 'WooCommerce product attributes are canonical store data'],
97 ['/wc/v3/products/categories*', 'wc.data.product-categories', 'WooCommerce product categories are canonical store data'],
98 ['/wc/v3/products/tags*', 'wc.data.product-tags', 'WooCommerce product tags are canonical store data'],
99 ['/wc/v3/products/shipping_classes*', 'wc.data.shipping-classes', 'WooCommerce shipping classes are canonical store data'],
100 ['/wc/v3/products*', 'wc.data.products', 'WooCommerce products are canonical store data'],
101 ['/wc/v3/coupons*', 'wc.data.coupons', 'WooCommerce coupons are canonical store data'],
102 ['/wc/v3/orders*', 'wc.data.orders', 'WooCommerce orders are canonical store data'],
103 ['/wc/v3/refunds*', 'wc.data.refunds', 'WooCommerce refunds are canonical store data'],
104 ['/wc/v3/customers*', 'wc.data.customers', 'WooCommerce customers are canonical store data'],
105 ['/wc/v3/taxes*', 'wc.data.taxes', 'WooCommerce taxes are canonical store data'],
106 ['/wc/v3/tax_classes*', 'wc.data.tax-classes', 'WooCommerce tax classes are canonical store data'],
107];
108
109const BACKEND_METADATA_RULES = [
110 {
111 pattern: '/wc/v3/data/currencies*',
112 ruleId: 'wc.metadata.currencies',
113 reason: 'currency metadata may be needed to interpret WooCommerce records',
114 requiredPatterns: ['/wc/v3/products*', '/wc/v3/orders*', '/wc/v3/coupons*'],
115 },
116 {
117 pattern: '/wc/v3/data/countries*',
118 ruleId: 'wc.metadata.countries',
119 reason: 'country metadata may be needed to interpret WooCommerce tax and customer records',
120 requiredPatterns: ['/wc/v3/customers*', '/wc/v3/taxes*', '/wc/v3/orders*'],
121 },
122];
123
124const KNOWN_PLUGIN_DATA_RULES = [
125 ['/ssp/v1/*', 'plugin.data.ssp', 'Seriously Simple Podcasting routes expose durable podcast records'],
126 ['/tribe/events/v1/events', 'plugin.data.tribe-events', 'events routes expose durable event records'],
127 ['/wp/v2/tribe_events', 'plugin.data.wp-tribe-events', 'event custom post type routes expose durable event records'],
128];
129
130const LEGACY_WC_PREFIXES = [
131 '/wc/v1/products',
132 '/wc/v1/coupons',
133 '/wc/v1/orders',
134 '/wc/v1/refunds',
135 '/wc/v1/customers',
136 '/wc/v1/taxes',
137 '/wc/v1/tax_classes',
138 '/wc/v2/products',
139 '/wc/v2/coupons',
140 '/wc/v2/orders',
141 '/wc/v2/refunds',
142 '/wc/v2/customers',
143 '/wc/v2/taxes',
144 '/wc/v2/tax_classes',
145];
146
147function normalizeRoutePath(routePath) {
148 if (typeof routePath !== 'string' || routePath.length === 0) {
149 return '/';
150 }
151 return routePath.startsWith('/') ? routePath : `/${routePath}`;
152}
153
154function routeMatchesPattern(routePath, pattern) {
155 const route = normalizeRoutePath(routePath);
156 if (pattern.endsWith('/*')) {
157 const prefix = pattern.slice(0, -1);
158 return route.startsWith(prefix);
159 }
160 if (pattern.endsWith('*')) {
161 return route.startsWith(pattern.slice(0, -1));
162 }
163 return route === pattern;
164}
165
166function findMatchingRule(routePath, rules) {
167 for (const rule of rules) {
168 const [pattern, ruleId, reason] = rule;
169 if (routeMatchesPattern(routePath, pattern)) {
170 return { ruleId, reason };
171 }
172 }
173 return null;
174}
175
176function actionForCategory(category) {
177 if (category === 'backend_data' || category === 'public_commerce_data') {
178 return 'sample';
179 }
180 if (category === 'backend_metadata') {
181 return 'metadata';
182 }
183 return 'skip';
184}
185
186function makeClassification(candidate, category, reason, ruleId, extra = {}) {
187 return {
188 routePath: candidate.routePath,
189 namespace: candidate.namespace,
190 category,
191 reason,
192 ruleId,
193 sampleByDefault: category === 'backend_data',
194 canIncludeByOverride: category !== 'backend_data',
195 includedByOverride: false,
196 excludedByOverride: false,
197 effectiveAction: actionForCategory(category),
198 ...extra,
199 };
200}
201
202function candidateHasCollectionShape(candidate) {
203 return Boolean(candidate.supportsPagination || candidate.getEndpoint?.args?.per_page || candidate.getEndpoint?.args?.page);
204}
205
206function hasSchema(candidate) {
207 return Boolean(candidate.getEndpoint?.schema || candidate.routeDefinition?.schema);
208}
209
210function classifyStoreCatalogRoute(candidate, routeSet, options = {}) {
211 const routePath = candidate.routePath;
212 let canonicalRoute = null;
213 let ruleId = null;
214 let reason = null;
215
216 if (routeMatchesPattern(routePath, '/wc/store/v1/products/categories*')) {
217 canonicalRoute = '/wc/v3/products/categories';
218 ruleId = 'wc.public-store.product-categories';
219 reason = 'WooCommerce Store API product categories expose durable public commerce taxonomy data';
220 } else if (routeMatchesPattern(routePath, '/wc/store/v1/products*')) {
221 canonicalRoute = '/wc/v3/products';
222 ruleId = 'wc.public-store.products';
223 reason = 'WooCommerce Store API products expose durable public commerce catalog data';
224 } else {
225 return null;
226 }
227
228 const commerceMode = options.commerceMode || 'authenticated';
229 if (commerceMode !== 'public' && routeSet.has(canonicalRoute)) {
230 return makeClassification(
231 candidate,
232 'excluded_unsupported',
233 `duplicated by canonical ${canonicalRoute} for authenticated/private commerce reads`,
234 'wc.public-store.duplicate',
235 { duplicateOf: canonicalRoute },
236 );
237 }
238
239 return makeClassification(candidate, 'public_commerce_data', reason, ruleId, {
240 canonicalRoute: routeSet.has(canonicalRoute) ? canonicalRoute : null,
241 commerceMode,
242 });
243}
244
245function classifyLegacyWooCommerceRoute(candidate, routeSet) {
246 const routePath = candidate.routePath;
247 const legacyPrefix = LEGACY_WC_PREFIXES.find((prefix) => routeMatchesPattern(routePath, `${prefix}*`));
248 if (!legacyPrefix) {
249 return null;
250 }
251
252 const canonicalRoute = routePath.replace(/^\/wc\/v[12]\//, '/wc/v3/');
253 if (routeSet.has(canonicalRoute)) {
254 return makeClassification(
255 candidate,
256 'excluded_unsupported',
257 `duplicated by canonical ${canonicalRoute}`,
258 'wc.duplicate.legacy-version',
259 { duplicateOf: canonicalRoute },
260 );
261 }
262
263 return makeClassification(
264 candidate,
265 'backend_data',
266 'legacy WooCommerce data route accepted because wc/v3 equivalent is unavailable',
267 'wc.data.legacy',
268 );
269}
270
271function classifyBaseRoute(candidate, routeSet, options = {}) {
272 const routePath = candidate.routePath;
273 const ruleGroups = [
274 ['excluded_frontend', FRONTEND_RULES],
275 ['excluded_template_editor', TEMPLATE_EDITOR_RULES],
276 ['excluded_runtime_session', RUNTIME_RULES],
277 ['excluded_admin_dashboard', ADMIN_RULES],
278 ['excluded_marketplace_setup', MARKETPLACE_RULES],
279 ['excluded_diagnostics', DIAGNOSTIC_RULES],
280 ['excluded_integration', INTEGRATION_RULES],
281 ];
282
283 for (const [category, rules] of ruleGroups) {
284 const rule = findMatchingRule(routePath, rules);
285 if (rule) {
286 return makeClassification(candidate, category, rule.reason, rule.ruleId);
287 }
288 }
289
290 const storeCatalogClassification = classifyStoreCatalogRoute(candidate, routeSet, options);
291 if (storeCatalogClassification) {
292 return storeCatalogClassification;
293 }
294
295 const legacyWooCommerce = classifyLegacyWooCommerceRoute(candidate, routeSet);
296 if (legacyWooCommerce) {
297 return legacyWooCommerce;
298 }
299
300 const dataRule = findMatchingRule(routePath, BACKEND_DATA_RULES);
301 if (dataRule) {
302 return makeClassification(candidate, 'backend_data', dataRule.reason, dataRule.ruleId);
303 }
304
305 const metadataRule = BACKEND_METADATA_RULES.find((rule) => routeMatchesPattern(routePath, rule.pattern));
306 if (metadataRule) {
307 return makeClassification(candidate, 'backend_metadata', metadataRule.reason, metadataRule.ruleId, {
308 requiredPatterns: metadataRule.requiredPatterns,
309 });
310 }
311
312 const pluginRule = findMatchingRule(routePath, KNOWN_PLUGIN_DATA_RULES);
313 if (pluginRule) {
314 return makeClassification(candidate, 'backend_data', pluginRule.reason, pluginRule.ruleId);
315 }
316
317 if (candidate.namespace === 'wp/v2' && candidateHasCollectionShape(candidate) && hasSchema(candidate)) {
318 return makeClassification(
319 candidate,
320 'backend_data',
321 'wp/v2 collection route has persisted-record shape and is not otherwise excluded',
322 'wp.data.custom-collection',
323 );
324 }
325
326 if (candidateHasCollectionShape(candidate) && hasSchema(candidate)) {
327 return makeClassification(
328 candidate,
329 'backend_data',
330 'collection route has persisted-record REST shape and is not otherwise excluded',
331 'plugin.data.collection-shape',
332 );
333 }
334
335 return makeClassification(
336 candidate,
337 'excluded_unsupported',
338 'route is not in the backend data allowlist and does not have an accepted collection shape',
339 'unsupported.default',
340 );
341}
342
343function normalizeList(values) {
344 if (!Array.isArray(values)) {
345 return [];
346 }
347 return values.filter(Boolean).map(String);
348}
349
350function normalizeOverrides(overrides = {}) {
351 return {
352 includeRoutes: normalizeList(overrides.includeRoutes).map(normalizeRoutePath),
353 includeNamespaces: normalizeList(overrides.includeNamespaces),
354 includeExcludedCategories: normalizeList(overrides.includeExcludedCategories),
355 excludeRoutes: normalizeList(overrides.excludeRoutes).map(normalizeRoutePath),
356 overrideReason: overrides.overrideReason ? String(overrides.overrideReason) : null,
357 commerceMode: overrides.commerceMode === 'public' ? 'public' : 'authenticated',
358 };
359}
360
361function isIncludedCategory(category, includeExcludedCategories) {
362 return includeExcludedCategories.includes(category) || includeExcludedCategories.includes('all');
363}
364
365function finalizeMetadataActions(classifications) {
366 const sampledRoutes = classifications
367 .filter((classification) => classification.effectiveAction === 'sample')
368 .map((classification) => classification.routePath);
369
370 return classifications.map((classification) => {
371 if (classification.category !== 'backend_metadata' || classification.includedByOverride || classification.excludedByOverride) {
372 return classification;
373 }
374
375 const requiredPatterns = Array.isArray(classification.requiredPatterns) ? classification.requiredPatterns : [];
376 const hasRelatedSampledRoute = requiredPatterns.some((pattern) =>
377 sampledRoutes.some((routePath) => routeMatchesPattern(routePath, pattern)));
378
379 if (hasRelatedSampledRoute) {
380 return { ...classification, effectiveAction: 'metadata' };
381 }
382
383 return {
384 ...classification,
385 effectiveAction: 'skip',
386 sampleByDefault: false,
387 reason: `${classification.reason}; no related sampled backend route is in scope`,
388 };
389 });
390}
391
392function classifyRoutes(candidates, overrides = {}) {
393 const normalizedOverrides = normalizeOverrides(overrides);
394 const routeSet = new Set(candidates.map((candidate) => candidate.routePath));
395 let classifications = candidates.map((candidate) => {
396 const classification = classifyBaseRoute(candidate, routeSet, normalizedOverrides);
397
398 if (normalizedOverrides.excludeRoutes.includes(candidate.routePath)) {
399 return {
400 ...classification,
401 excludedByOverride: true,
402 includedByOverride: false,
403 effectiveAction: 'skip',
404 sampleByDefault: false,
405 overrideReason: normalizedOverrides.overrideReason,
406 };
407 }
408
409 const includeRoute = normalizedOverrides.includeRoutes.includes(candidate.routePath);
410 const includeCategory = isIncludedCategory(classification.category, normalizedOverrides.includeExcludedCategories);
411 if (includeRoute || includeCategory) {
412 return {
413 ...classification,
414 includedByOverride: true,
415 effectiveAction: 'sample',
416 overrideReason: normalizedOverrides.overrideReason,
417 };
418 }
419
420 return classification;
421 });
422
423 classifications = finalizeMetadataActions(classifications);
424 return classifications.sort((a, b) => a.routePath.localeCompare(b.routePath));
425}
426
427function summarizeSkippedByCategory(classifications) {
428 const summary = {};
429 for (const classification of classifications) {
430 if (classification.effectiveAction !== 'skip') {
431 continue;
432 }
433 summary[classification.category] = (summary[classification.category] || 0) + 1;
434 }
435 return Object.fromEntries(Object.entries(summary).sort(([a], [b]) => a.localeCompare(b)));
436}
437
438module.exports = {
439 ROUTE_CATEGORIES,
440 classifyRoutes,
441 normalizeOverrides,
442 routeMatchesPattern,
443 summarizeSkippedByCategory,
444};