@contentful/experience-design-system-cli 2.12.4-dev-build-9c819d8.0 → 2.13.1-dev-build-847dead.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package.json +3 -6
- package/dist/src/analyze/command.js +2 -22
- package/dist/src/analyze/select/command.js +1 -1
- package/dist/src/analyze/select/tui/components/FieldEditor.js +4 -1
- package/dist/src/analyze/select-agent/command.js +1 -1
- package/dist/src/apply/command.d.ts +1 -5
- package/dist/src/apply/command.js +0 -43
- package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +1 -1
- package/dist/src/import/tui/steps/GenerateReviewStep.js +20 -102
- package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +0 -10
- package/dist/src/import/tui/steps/WizardPreviewStep.js +51 -95
- package/dist/src/import/tui/steps/preview-diff.js +0 -48
- package/dist/src/session/db.d.ts +0 -9
- package/dist/src/session/db.js +0 -46
- package/dist/src/types.d.ts +2 -76
- package/dist/src/types.js +1 -4
- package/package.json +3 -6
- package/dist/src/analyze/cycle-detection.d.ts +0 -68
- package/dist/src/analyze/cycle-detection.js +0 -285
- package/dist/src/analyze/extract/astro.d.ts +0 -5
- package/dist/src/analyze/extract/astro.js +0 -289
- package/dist/src/analyze/extract/non-authorable-filter.d.ts +0 -16
- package/dist/src/analyze/extract/non-authorable-filter.js +0 -60
- package/dist/src/analyze/extract/pipeline.d.ts +0 -6
- package/dist/src/analyze/extract/pipeline.js +0 -314
- package/dist/src/analyze/extract/react.d.ts +0 -2
- package/dist/src/analyze/extract/react.js +0 -2041
- package/dist/src/analyze/extract/scoring.d.ts +0 -12
- package/dist/src/analyze/extract/scoring.js +0 -108
- package/dist/src/analyze/extract/slot-allowed-components.d.ts +0 -8
- package/dist/src/analyze/extract/slot-allowed-components.js +0 -40
- package/dist/src/analyze/extract/slot-detection.d.ts +0 -35
- package/dist/src/analyze/extract/slot-detection.js +0 -101
- package/dist/src/analyze/extract/source-inspection.d.ts +0 -13
- package/dist/src/analyze/extract/source-inspection.js +0 -189
- package/dist/src/analyze/extract/stencil.d.ts +0 -2
- package/dist/src/analyze/extract/stencil.js +0 -296
- package/dist/src/analyze/extract/svelte.d.ts +0 -5
- package/dist/src/analyze/extract/svelte.js +0 -1626
- package/dist/src/analyze/extract/tsx-shared.d.ts +0 -8
- package/dist/src/analyze/extract/tsx-shared.js +0 -263
- package/dist/src/analyze/extract/validate.d.ts +0 -16
- package/dist/src/analyze/extract/validate.js +0 -94
- package/dist/src/analyze/extract/vue-tsx.d.ts +0 -2
- package/dist/src/analyze/extract/vue-tsx.js +0 -498
- package/dist/src/analyze/extract/vue.d.ts +0 -5
- package/dist/src/analyze/extract/vue.js +0 -653
- package/dist/src/analyze/extract/web-components.d.ts +0 -2
- package/dist/src/analyze/extract/web-components.js +0 -876
- package/dist/src/analyze/pre-classify.d.ts +0 -17
- package/dist/src/analyze/pre-classify.js +0 -211
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { RawComponentDefinition } from '../../types.js';
|
|
2
|
-
export type ExtractionConfidence = 1 | 2 | 3 | 4 | 5;
|
|
3
|
-
export type ExtractionScore = {
|
|
4
|
-
confidence: ExtractionConfidence;
|
|
5
|
-
reasons: string[];
|
|
6
|
-
};
|
|
7
|
-
export interface ExtractionScoreOptions {
|
|
8
|
-
additionalIssueCount?: number;
|
|
9
|
-
additionalReasons?: string[];
|
|
10
|
-
}
|
|
11
|
-
export declare function computeExtractionScore(component: RawComponentDefinition, options?: ExtractionScoreOptions): ExtractionScore;
|
|
12
|
-
export declare function deriveNeedsReview(confidence: ExtractionConfidence): boolean;
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
// Prop type strings that indicate the extractor couldn't resolve a concrete type
|
|
2
|
-
const OPAQUE_TYPES = new Set(['any', 'unknown', 'object', 'Record<string, unknown>', 'Record<string, any>']);
|
|
3
|
-
// Prop names that are non-obvious and benefit from a description
|
|
4
|
-
const OBVIOUS_PROP_NAMES = new Set([
|
|
5
|
-
'className',
|
|
6
|
-
'style',
|
|
7
|
-
'id',
|
|
8
|
-
'children',
|
|
9
|
-
'disabled',
|
|
10
|
-
'hidden',
|
|
11
|
-
'onClick',
|
|
12
|
-
'onChange',
|
|
13
|
-
'onSubmit',
|
|
14
|
-
'onBlur',
|
|
15
|
-
'onFocus',
|
|
16
|
-
'href',
|
|
17
|
-
'src',
|
|
18
|
-
'alt',
|
|
19
|
-
'type',
|
|
20
|
-
'value',
|
|
21
|
-
'checked',
|
|
22
|
-
'placeholder',
|
|
23
|
-
'label',
|
|
24
|
-
'title',
|
|
25
|
-
'name',
|
|
26
|
-
'required',
|
|
27
|
-
'readOnly',
|
|
28
|
-
'autoFocus',
|
|
29
|
-
'tabIndex',
|
|
30
|
-
'role',
|
|
31
|
-
'aria-label',
|
|
32
|
-
'aria-describedby',
|
|
33
|
-
]);
|
|
34
|
-
// A union is "wide" if it mixes 3+ distinct base primitive types (ignoring nullability modifiers).
|
|
35
|
-
// e.g. "string | number | boolean" → wide. "string | null | undefined" → NOT wide (just nullable string).
|
|
36
|
-
function isWidePrimitiveUnion(type) {
|
|
37
|
-
const parts = type.split('|').map((p) => p.trim());
|
|
38
|
-
if (parts.length < 3)
|
|
39
|
-
return false;
|
|
40
|
-
const basePrimitives = new Set(['string', 'number', 'boolean']);
|
|
41
|
-
const nullability = new Set(['null', 'undefined']);
|
|
42
|
-
const baseCount = parts.filter((p) => basePrimitives.has(p)).length;
|
|
43
|
-
const nullabilityCount = parts.filter((p) => nullability.has(p)).length;
|
|
44
|
-
return baseCount >= 3 || (baseCount >= 2 && nullabilityCount > 0 && baseCount + nullabilityCount >= 3);
|
|
45
|
-
}
|
|
46
|
-
// Count the number of issues found for scoring
|
|
47
|
-
function countIssues(component, options = {}) {
|
|
48
|
-
let count = 0;
|
|
49
|
-
const reasons = [];
|
|
50
|
-
if (component.props.length === 0 && component.slots.length === 0) {
|
|
51
|
-
count++;
|
|
52
|
-
reasons.push('no-props-or-slots');
|
|
53
|
-
}
|
|
54
|
-
if (component.props.length > 50) {
|
|
55
|
-
count++;
|
|
56
|
-
reasons.push(`high-prop-count:${component.props.length}`);
|
|
57
|
-
}
|
|
58
|
-
for (const prop of component.props) {
|
|
59
|
-
if (OPAQUE_TYPES.has(prop.type.trim())) {
|
|
60
|
-
count++;
|
|
61
|
-
reasons.push(`opaque-type:${prop.name}`);
|
|
62
|
-
break;
|
|
63
|
-
}
|
|
64
|
-
if (isWidePrimitiveUnion(prop.type)) {
|
|
65
|
-
count++;
|
|
66
|
-
reasons.push(`wide-union:${prop.name}`);
|
|
67
|
-
break;
|
|
68
|
-
}
|
|
69
|
-
if (!prop.description && !OBVIOUS_PROP_NAMES.has(prop.name)) {
|
|
70
|
-
count++;
|
|
71
|
-
reasons.push('props-missing-description');
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
count += options.additionalIssueCount ?? 0;
|
|
76
|
-
if (options.additionalReasons && options.additionalReasons.length > 0) {
|
|
77
|
-
reasons.push(...options.additionalReasons);
|
|
78
|
-
}
|
|
79
|
-
return { count, reasons: [...new Set(reasons)] };
|
|
80
|
-
}
|
|
81
|
-
// Maps issue count to a 1–5 confidence scale:
|
|
82
|
-
// 0 issues → 5 (clean)
|
|
83
|
-
// 1 issue → 4 (minor concern)
|
|
84
|
-
// 2 issues → 3 (moderate concern)
|
|
85
|
-
// 3 issues → 2 (significant concern)
|
|
86
|
-
// 4+ issues → 1 (likely wrong)
|
|
87
|
-
function issueCountToConfidence(count) {
|
|
88
|
-
if (count === 0)
|
|
89
|
-
return 5;
|
|
90
|
-
if (count === 1)
|
|
91
|
-
return 4;
|
|
92
|
-
if (count === 2)
|
|
93
|
-
return 3;
|
|
94
|
-
if (count === 3)
|
|
95
|
-
return 2;
|
|
96
|
-
return 1;
|
|
97
|
-
}
|
|
98
|
-
export function computeExtractionScore(component, options = {}) {
|
|
99
|
-
const { count, reasons } = countIssues(component, options);
|
|
100
|
-
return {
|
|
101
|
-
confidence: issueCountToConfidence(count),
|
|
102
|
-
reasons,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
// Flag components scoring 2 or below for human review
|
|
106
|
-
export function deriveNeedsReview(confidence) {
|
|
107
|
-
return confidence <= 2;
|
|
108
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export interface AllowedComponentsContext {
|
|
2
|
-
/** e.g. { HeadingProps -> Heading, ButtonProps -> Button } */
|
|
3
|
-
propsToComponent: ReadonlyMap<string, string>;
|
|
4
|
-
/** ComponentType names known to the project (post-filter). */
|
|
5
|
-
componentNames: ReadonlySet<string>;
|
|
6
|
-
}
|
|
7
|
-
export declare function extractAllowedComponentsFromTypeText(typeText: string, ctx: AllowedComponentsContext): string[];
|
|
8
|
-
export declare function extractAllowedComponentsFromJsdoc(jsdocText: string, componentNames: ReadonlySet<string>): string[];
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
// Matches ReactElement<XProps> and ReactElement<XProps, ...> (TS often
|
|
2
|
-
// expands the second generic argument to string | JSXElementConstructor<any>).
|
|
3
|
-
// Only the first generic argument (the props type name) is captured.
|
|
4
|
-
const REACT_ELEMENT_GENERIC = /(?:React\.)?ReactElement\s*<\s*([A-Za-z_$][\w$.]*)(?![\w$.])/g;
|
|
5
|
-
const SVELTE_SNIPPET_TUPLE = /Snippet\s*<\s*\[([^\]]*)\]\s*>/g;
|
|
6
|
-
const IDENTIFIER = /[A-Za-z_$][\w$.]*/g;
|
|
7
|
-
export function extractAllowedComponentsFromTypeText(typeText, ctx) {
|
|
8
|
-
const found = new Set();
|
|
9
|
-
const record = (propsTypeName) => {
|
|
10
|
-
const componentName = ctx.propsToComponent.get(propsTypeName);
|
|
11
|
-
if (componentName && ctx.componentNames.has(componentName))
|
|
12
|
-
found.add(componentName);
|
|
13
|
-
};
|
|
14
|
-
REACT_ELEMENT_GENERIC.lastIndex = 0;
|
|
15
|
-
let m;
|
|
16
|
-
while ((m = REACT_ELEMENT_GENERIC.exec(typeText)) !== null)
|
|
17
|
-
record(m[1]);
|
|
18
|
-
SVELTE_SNIPPET_TUPLE.lastIndex = 0;
|
|
19
|
-
while ((m = SVELTE_SNIPPET_TUPLE.exec(typeText)) !== null) {
|
|
20
|
-
const tupleBody = m[1];
|
|
21
|
-
IDENTIFIER.lastIndex = 0;
|
|
22
|
-
let id;
|
|
23
|
-
while ((id = IDENTIFIER.exec(tupleBody)) !== null)
|
|
24
|
-
record(id[0]);
|
|
25
|
-
}
|
|
26
|
-
return [...found].sort();
|
|
27
|
-
}
|
|
28
|
-
const JSDOC_TAG = /@allowedComponents\s+([^\n*]+)/;
|
|
29
|
-
export function extractAllowedComponentsFromJsdoc(jsdocText, componentNames) {
|
|
30
|
-
const m = JSDOC_TAG.exec(jsdocText);
|
|
31
|
-
if (!m)
|
|
32
|
-
return [];
|
|
33
|
-
const found = new Set();
|
|
34
|
-
for (const raw of m[1].split(',')) {
|
|
35
|
-
const name = raw.trim();
|
|
36
|
-
if (name && componentNames.has(name))
|
|
37
|
-
found.add(name);
|
|
38
|
-
}
|
|
39
|
-
return [...found].sort();
|
|
40
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Slot detection utilities for ReactNode-typed props.
|
|
3
|
-
*
|
|
4
|
-
* Props whose type resolves to ReactNode/ReactElement/JSX.Element are treated
|
|
5
|
-
* as slots rather than plain props — unless their name is in the content-name
|
|
6
|
-
* exception list (label, title, description, etc.).
|
|
7
|
-
*
|
|
8
|
-
* Array ReactNode types (e.g. ReactNode[]) ALWAYS become slots, even if the
|
|
9
|
-
* prop name is in the exception list.
|
|
10
|
-
*/
|
|
11
|
-
/**
|
|
12
|
-
* Prop names that should remain as props even when typed as ReactNode.
|
|
13
|
-
* These are typically text-content names, not composable slots.
|
|
14
|
-
*/
|
|
15
|
-
export declare const CONTENT_NAME_EXCEPTIONS: Set<string>;
|
|
16
|
-
/**
|
|
17
|
-
* Checks if a type string represents a ReactNode/ReactElement/JSX.Element type,
|
|
18
|
-
* including unions with null/undefined.
|
|
19
|
-
*/
|
|
20
|
-
export declare function isReactNodeType(typeText: string): boolean;
|
|
21
|
-
/**
|
|
22
|
-
* Checks if a type string represents an array of ReactNode.
|
|
23
|
-
* Examples: ReactNode[], React.ReactNode[], Array<ReactNode>
|
|
24
|
-
*/
|
|
25
|
-
export declare function isArrayReactNodeType(typeText: string): boolean;
|
|
26
|
-
/**
|
|
27
|
-
* Determines whether a prop should be converted to a slot based on its name and type.
|
|
28
|
-
*
|
|
29
|
-
* Rules:
|
|
30
|
-
* 1. If the type is not a ReactNode type → false
|
|
31
|
-
* 2. If the type is an array ReactNode → true (overrides exception list)
|
|
32
|
-
* 3. If the name is in the content-name exception list → false
|
|
33
|
-
* 4. Otherwise → true
|
|
34
|
-
*/
|
|
35
|
-
export declare function shouldBeSlot(propName: string, typeText: string): boolean;
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Slot detection utilities for ReactNode-typed props.
|
|
3
|
-
*
|
|
4
|
-
* Props whose type resolves to ReactNode/ReactElement/JSX.Element are treated
|
|
5
|
-
* as slots rather than plain props — unless their name is in the content-name
|
|
6
|
-
* exception list (label, title, description, etc.).
|
|
7
|
-
*
|
|
8
|
-
* Array ReactNode types (e.g. ReactNode[]) ALWAYS become slots, even if the
|
|
9
|
-
* prop name is in the exception list.
|
|
10
|
-
*/
|
|
11
|
-
/**
|
|
12
|
-
* Prop names that should remain as props even when typed as ReactNode.
|
|
13
|
-
* These are typically text-content names, not composable slots.
|
|
14
|
-
*/
|
|
15
|
-
export const CONTENT_NAME_EXCEPTIONS = new Set([
|
|
16
|
-
'label',
|
|
17
|
-
'title',
|
|
18
|
-
'description',
|
|
19
|
-
'text',
|
|
20
|
-
'caption',
|
|
21
|
-
'message',
|
|
22
|
-
'placeholder',
|
|
23
|
-
'tooltip',
|
|
24
|
-
'heading',
|
|
25
|
-
'subheading',
|
|
26
|
-
'body',
|
|
27
|
-
'summary',
|
|
28
|
-
'excerpt',
|
|
29
|
-
]);
|
|
30
|
-
const REACT_NODE_EXACT_PATTERNS = ['ReactNode', 'React.ReactNode', 'ReactElement', 'React.ReactElement', 'JSX.Element'];
|
|
31
|
-
/**
|
|
32
|
-
* Checks if a type string represents a ReactNode/ReactElement/JSX.Element type,
|
|
33
|
-
* including unions with null/undefined.
|
|
34
|
-
*/
|
|
35
|
-
export function isReactNodeType(typeText) {
|
|
36
|
-
const normalized = typeText.replace(/\s+/g, ' ').trim();
|
|
37
|
-
// Strip optional markers and union with null/undefined
|
|
38
|
-
const stripped = normalized
|
|
39
|
-
.split('|')
|
|
40
|
-
.map((part) => part.trim())
|
|
41
|
-
.filter((part) => part !== 'null' && part !== 'undefined')
|
|
42
|
-
.join(' | ');
|
|
43
|
-
// Check exact match (after stripping null/undefined union members)
|
|
44
|
-
if (REACT_NODE_EXACT_PATTERNS.includes(stripped)) {
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
47
|
-
// Check array patterns
|
|
48
|
-
if (isArrayReactNodeType(typeText)) {
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Checks if a type string represents an array of ReactNode.
|
|
55
|
-
* Examples: ReactNode[], React.ReactNode[], Array<ReactNode>
|
|
56
|
-
*/
|
|
57
|
-
export function isArrayReactNodeType(typeText) {
|
|
58
|
-
const normalized = typeText.replace(/\s+/g, ' ').trim();
|
|
59
|
-
// Strip optional markers and union with null/undefined
|
|
60
|
-
const stripped = normalized
|
|
61
|
-
.split('|')
|
|
62
|
-
.map((part) => part.trim())
|
|
63
|
-
.filter((part) => part !== 'null' && part !== 'undefined')
|
|
64
|
-
.join(' | ');
|
|
65
|
-
// Pattern: ReactNode[] or React.ReactNode[] etc.
|
|
66
|
-
for (const pattern of REACT_NODE_EXACT_PATTERNS) {
|
|
67
|
-
if (stripped === `${pattern}[]`) {
|
|
68
|
-
return true;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
// Pattern: Array<ReactNode> or Array<React.ReactNode> etc.
|
|
72
|
-
for (const pattern of REACT_NODE_EXACT_PATTERNS) {
|
|
73
|
-
if (stripped === `Array<${pattern}>`) {
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Determines whether a prop should be converted to a slot based on its name and type.
|
|
81
|
-
*
|
|
82
|
-
* Rules:
|
|
83
|
-
* 1. If the type is not a ReactNode type → false
|
|
84
|
-
* 2. If the type is an array ReactNode → true (overrides exception list)
|
|
85
|
-
* 3. If the name is in the content-name exception list → false
|
|
86
|
-
* 4. Otherwise → true
|
|
87
|
-
*/
|
|
88
|
-
export function shouldBeSlot(propName, typeText) {
|
|
89
|
-
if (!isReactNodeType(typeText)) {
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
// Array ReactNode always becomes a slot, even for exception names
|
|
93
|
-
if (isArrayReactNodeType(typeText)) {
|
|
94
|
-
return true;
|
|
95
|
-
}
|
|
96
|
-
// Check exception list
|
|
97
|
-
if (CONTENT_NAME_EXCEPTIONS.has(propName)) {
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
return true;
|
|
101
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { RawComponentDefinition } from '../../types.js';
|
|
2
|
-
export declare const HIGH_CONFIDENCE_DATA_FETCH_WRAPPER_REASON = "data-fetch-wrapper";
|
|
3
|
-
export declare const POSSIBLE_DATA_FETCH_WRAPPER_REASON = "possible-data-fetch-wrapper";
|
|
4
|
-
export declare const ZERO_SURFACE_RENDERED_UI_REASON = "zero-surface:rendered-ui";
|
|
5
|
-
export interface ComponentSourceInspection {
|
|
6
|
-
wrapperConfidence: 0 | 1 | 2 | 3 | 4 | 5;
|
|
7
|
-
reviewReasons: string[];
|
|
8
|
-
keepDespiteZeroSurface: boolean;
|
|
9
|
-
}
|
|
10
|
-
export declare function inspectComponentSource(component: RawComponentDefinition): Promise<ComponentSourceInspection>;
|
|
11
|
-
export declare function isDataWrapperReviewReason(reason: string): boolean;
|
|
12
|
-
export declare function describeReviewReason(reason: string): string;
|
|
13
|
-
export declare function describeReviewReasons(reasons: string[]): string[];
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
export const HIGH_CONFIDENCE_DATA_FETCH_WRAPPER_REASON = 'data-fetch-wrapper';
|
|
3
|
-
export const POSSIBLE_DATA_FETCH_WRAPPER_REASON = 'possible-data-fetch-wrapper';
|
|
4
|
-
export const ZERO_SURFACE_RENDERED_UI_REASON = 'zero-surface:rendered-ui';
|
|
5
|
-
const DATA_WRAPPER_REASON_PREFIX = 'data-wrapper:';
|
|
6
|
-
const INFRA_PROP_NAMES = new Set(['id', 'locale', 'preview', 'slug', 'topic', 'previousComponent', '__typename']);
|
|
7
|
-
const VISIBLE_UI_TAG_PATTERN = /<(?:[A-Z][A-Za-z0-9_.]*|div|span|section|main|article|header|footer|nav|aside|img|video|p|h[1-6]|ul|ol|li|button|input|textarea|select|form|label|table|tbody|thead|tr|td|th)\b/;
|
|
8
|
-
const GENERATED_IMPORT_PATTERN = /from\s+['"][^'"]*__generated[^'"]*['"]/;
|
|
9
|
-
const GENERATED_QUERY_HOOK_PATTERN = /\buse[A-Z][A-Za-z0-9]*(?:Lazy|Suspense)?Query\s*\(/;
|
|
10
|
-
const GQL_FILENAME_PATTERN = /(?:-gql|-ggl)\.[cm]?[jt]sx?$/i;
|
|
11
|
-
const LOADING_NULL_GUARD_PATTERN = /if\s*\([^)]*(?:isLoading|loading|!data|!\w+Collection|!\w+Item|!\w+)\s*[^)]*\)\s*return\s+null\b/;
|
|
12
|
-
const IMPORT_PATTERN = /import\s+(?:type\s+)?(.+?)\s+from\s+['"]([^'"]+)['"]/g;
|
|
13
|
-
function escapeRegExp(value) {
|
|
14
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
15
|
-
}
|
|
16
|
-
function parseImportedNames(importClause) {
|
|
17
|
-
const trimmed = importClause.trim();
|
|
18
|
-
if (!trimmed)
|
|
19
|
-
return [];
|
|
20
|
-
const names = [];
|
|
21
|
-
const defaultAndNamed = trimmed.split(',').map((part) => part.trim());
|
|
22
|
-
for (const part of defaultAndNamed) {
|
|
23
|
-
if (!part)
|
|
24
|
-
continue;
|
|
25
|
-
if (part.startsWith('{') && part.endsWith('}')) {
|
|
26
|
-
for (const named of part
|
|
27
|
-
.slice(1, -1)
|
|
28
|
-
.split(',')
|
|
29
|
-
.map((item) => item.trim())
|
|
30
|
-
.filter(Boolean)) {
|
|
31
|
-
const [local] = named.split(/\s+as\s+/i);
|
|
32
|
-
if (local)
|
|
33
|
-
names.push(local.trim());
|
|
34
|
-
}
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
const [local] = part.split(/\s+as\s+/i);
|
|
38
|
-
if (local)
|
|
39
|
-
names.push(local.trim());
|
|
40
|
-
}
|
|
41
|
-
return names;
|
|
42
|
-
}
|
|
43
|
-
function collectSiblingRendererImports(sourceText) {
|
|
44
|
-
const names = new Set();
|
|
45
|
-
for (const match of sourceText.matchAll(IMPORT_PATTERN)) {
|
|
46
|
-
const importClause = match[1]?.trim() ?? '';
|
|
47
|
-
const importPath = match[2]?.trim() ?? '';
|
|
48
|
-
if (!importPath.startsWith('./'))
|
|
49
|
-
continue;
|
|
50
|
-
if (importPath.includes('__generated'))
|
|
51
|
-
continue;
|
|
52
|
-
if (/-g(?:ql|gl)(?:$|\.)/i.test(importPath))
|
|
53
|
-
continue;
|
|
54
|
-
for (const name of parseImportedNames(importClause)) {
|
|
55
|
-
if (name)
|
|
56
|
-
names.add(name);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return [...names];
|
|
60
|
-
}
|
|
61
|
-
function hasSiblingForwardRender(sourceText, siblingImports) {
|
|
62
|
-
return siblingImports.some((name) => {
|
|
63
|
-
const renderPattern = new RegExp(`<${escapeRegExp(name)}\\b[\\s\\S]*?(?:/>|</${escapeRegExp(name)}>)`);
|
|
64
|
-
if (!renderPattern.test(sourceText))
|
|
65
|
-
return false;
|
|
66
|
-
const siblingSpreadPattern = new RegExp(`<${escapeRegExp(name)}\\b[^>]*\\{\\.\\.\\.(?!props\\b)[^}]+\\}`);
|
|
67
|
-
const propsPlusSiblingSpreadPattern = new RegExp(`<${escapeRegExp(name)}\\b[^>]*\\{\\.\\.\\.props\\}[^>]*\\{\\.\\.\\.(?!props\\b)[^}]+\\}`);
|
|
68
|
-
return siblingSpreadPattern.test(sourceText) || propsPlusSiblingSpreadPattern.test(sourceText);
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
function hasVisibleUiRender(sourceText) {
|
|
72
|
-
return /return\s*(?:\(|<)/.test(sourceText) && VISIBLE_UI_TAG_PATTERN.test(sourceText);
|
|
73
|
-
}
|
|
74
|
-
function infraPropNames(component) {
|
|
75
|
-
const props = component.props.map((prop) => prop.name);
|
|
76
|
-
return props.length > 0 && props.every((prop) => INFRA_PROP_NAMES.has(prop)) ? props : [];
|
|
77
|
-
}
|
|
78
|
-
function scoreToConfidence(score) {
|
|
79
|
-
if (score <= 0)
|
|
80
|
-
return 0;
|
|
81
|
-
if (score <= 2)
|
|
82
|
-
return 2;
|
|
83
|
-
if (score <= 4)
|
|
84
|
-
return 3;
|
|
85
|
-
if (score <= 6)
|
|
86
|
-
return 4;
|
|
87
|
-
return 5;
|
|
88
|
-
}
|
|
89
|
-
function dedupeReasons(reasons) {
|
|
90
|
-
return [...new Set(reasons)];
|
|
91
|
-
}
|
|
92
|
-
export async function inspectComponentSource(component) {
|
|
93
|
-
let sourceText = '';
|
|
94
|
-
try {
|
|
95
|
-
sourceText = await readFile(component.source, 'utf8');
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
return {
|
|
99
|
-
wrapperConfidence: 0,
|
|
100
|
-
reviewReasons: [],
|
|
101
|
-
keepDespiteZeroSurface: false,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
const reviewReasons = [];
|
|
105
|
-
let wrapperScore = 0;
|
|
106
|
-
const hasGeneratedQueryHook = GENERATED_IMPORT_PATTERN.test(sourceText) && GENERATED_QUERY_HOOK_PATTERN.test(sourceText);
|
|
107
|
-
const infraProps = infraPropNames(component);
|
|
108
|
-
const siblingImports = collectSiblingRendererImports(sourceText);
|
|
109
|
-
if (GQL_FILENAME_PATTERN.test(component.source)) {
|
|
110
|
-
reviewReasons.push('data-wrapper:gql-filename');
|
|
111
|
-
wrapperScore += 1;
|
|
112
|
-
}
|
|
113
|
-
if (hasGeneratedQueryHook) {
|
|
114
|
-
reviewReasons.push('data-wrapper:generated-query-hook');
|
|
115
|
-
wrapperScore += 3;
|
|
116
|
-
}
|
|
117
|
-
// Require corroboration from a stronger signal before counting sibling imports —
|
|
118
|
-
// otherwise any composed component that imports two sub-components scores +1 here.
|
|
119
|
-
if (siblingImports.length > 0 && (hasGeneratedQueryHook || infraProps.length > 0)) {
|
|
120
|
-
reviewReasons.push('data-wrapper:sibling-renderer-import');
|
|
121
|
-
wrapperScore += 1;
|
|
122
|
-
}
|
|
123
|
-
if (hasSiblingForwardRender(sourceText, siblingImports)) {
|
|
124
|
-
reviewReasons.push('data-wrapper:fetch-forward-render');
|
|
125
|
-
wrapperScore += 3;
|
|
126
|
-
}
|
|
127
|
-
if (sourceText.includes('useContentfulLiveUpdates(') || sourceText.includes('useContentfulContext(')) {
|
|
128
|
-
reviewReasons.push('data-wrapper:contentful-runtime');
|
|
129
|
-
wrapperScore += 1;
|
|
130
|
-
}
|
|
131
|
-
if (infraProps.length > 0) {
|
|
132
|
-
reviewReasons.push('data-wrapper:infra-props');
|
|
133
|
-
wrapperScore += 2;
|
|
134
|
-
}
|
|
135
|
-
if (LOADING_NULL_GUARD_PATTERN.test(sourceText)) {
|
|
136
|
-
reviewReasons.push('data-wrapper:loading-null-guard');
|
|
137
|
-
wrapperScore += 1;
|
|
138
|
-
}
|
|
139
|
-
const wrapperConfidence = scoreToConfidence(wrapperScore);
|
|
140
|
-
if (wrapperConfidence >= 4) {
|
|
141
|
-
reviewReasons.unshift(HIGH_CONFIDENCE_DATA_FETCH_WRAPPER_REASON);
|
|
142
|
-
}
|
|
143
|
-
else if (wrapperConfidence === 3) {
|
|
144
|
-
reviewReasons.unshift(POSSIBLE_DATA_FETCH_WRAPPER_REASON);
|
|
145
|
-
}
|
|
146
|
-
const keepDespiteZeroSurface = component.props.length === 0 && component.slots.length === 0 && hasVisibleUiRender(sourceText);
|
|
147
|
-
if (keepDespiteZeroSurface) {
|
|
148
|
-
reviewReasons.push(ZERO_SURFACE_RENDERED_UI_REASON);
|
|
149
|
-
}
|
|
150
|
-
return {
|
|
151
|
-
wrapperConfidence,
|
|
152
|
-
reviewReasons: dedupeReasons(reviewReasons),
|
|
153
|
-
keepDespiteZeroSurface,
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
export function isDataWrapperReviewReason(reason) {
|
|
157
|
-
return (reason === HIGH_CONFIDENCE_DATA_FETCH_WRAPPER_REASON ||
|
|
158
|
-
reason === POSSIBLE_DATA_FETCH_WRAPPER_REASON ||
|
|
159
|
-
reason.startsWith(DATA_WRAPPER_REASON_PREFIX));
|
|
160
|
-
}
|
|
161
|
-
export function describeReviewReason(reason) {
|
|
162
|
-
switch (reason) {
|
|
163
|
-
case HIGH_CONFIDENCE_DATA_FETCH_WRAPPER_REASON:
|
|
164
|
-
return 'high-confidence data-fetch wrapper';
|
|
165
|
-
case POSSIBLE_DATA_FETCH_WRAPPER_REASON:
|
|
166
|
-
return 'possible data-fetch wrapper';
|
|
167
|
-
case 'data-wrapper:gql-filename':
|
|
168
|
-
return 'source file follows a gql wrapper naming pattern';
|
|
169
|
-
case 'data-wrapper:generated-query-hook':
|
|
170
|
-
return 'imports and calls a generated query hook';
|
|
171
|
-
case 'data-wrapper:sibling-renderer-import':
|
|
172
|
-
return 'imports a sibling renderer from the same folder';
|
|
173
|
-
case 'data-wrapper:fetch-forward-render':
|
|
174
|
-
return 'forwards fetched data into a sibling renderer';
|
|
175
|
-
case 'data-wrapper:contentful-runtime':
|
|
176
|
-
return 'uses Contentful runtime hooks';
|
|
177
|
-
case 'data-wrapper:infra-props':
|
|
178
|
-
return 'only exposes infra-fetch props';
|
|
179
|
-
case 'data-wrapper:loading-null-guard':
|
|
180
|
-
return 'returns early while loading or when fetched data is missing';
|
|
181
|
-
case ZERO_SURFACE_RENDERED_UI_REASON:
|
|
182
|
-
return 'source renders visible/compositional UI despite zero extracted props and slots';
|
|
183
|
-
default:
|
|
184
|
-
return reason;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
export function describeReviewReasons(reasons) {
|
|
188
|
-
return dedupeReasons(reasons.map(describeReviewReason));
|
|
189
|
-
}
|