@deneb-ui/cli 2.0.88 → 2.0.90
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/bin/index.js +31 -5
- package/package.json +2 -2
- package/src/arc/__tests__/acceptance-gates.test.cjs +62 -0
- package/src/arc/__tests__/arc.test.cjs +922 -0
- package/src/arc/__tests__/canonical.test.cjs +57 -0
- package/src/arc/__tests__/classification.test.cjs +54 -0
- package/src/arc/__tests__/corpus.test.cjs +144 -0
- package/src/arc/__tests__/explain-blocked.test.cjs +192 -0
- package/src/arc/__tests__/fuzz.test.cjs +106 -0
- package/src/arc/__tests__/interaction.test.cjs +85 -0
- package/src/arc/__tests__/visual-regression.test.cjs +99 -0
- package/src/arc/acceptance-gates.cjs +119 -0
- package/src/arc/canonical-paths.cjs +142 -23
- package/src/arc/corpus-verifier.cjs +295 -0
- package/src/arc/data-classification.cjs +182 -0
- package/src/arc/data-flow.cjs +62 -2
- package/src/arc/explain-blocked.cjs +388 -0
- package/src/arc/fuzz-engine.cjs +604 -0
- package/src/arc/index.cjs +148 -2
- package/src/arc/interaction-verifier.cjs +185 -0
- package/src/arc/ir-builder.cjs +170 -12
- package/src/arc/planner.cjs +16 -1
- package/src/arc/residual.cjs +22 -0
- package/src/arc/rsc-boundary.cjs +105 -4
- package/src/arc/runtime-validator.cjs +65 -0
- package/src/arc/semantic.cjs +12 -1
- package/src/arc/transformer.cjs +14 -2745
- package/src/arc/transforms/assets/tailwind-bg.cjs +38 -0
- package/src/arc/transforms/collections/list.cjs +474 -0
- package/src/arc/transforms/components/child-card.cjs +268 -0
- package/src/arc/transforms/components/compound-content.cjs +49 -0
- package/src/arc/transforms/components/reusable-component.cjs +173 -0
- package/src/arc/transforms/element-transform.cjs +192 -0
- package/src/arc/transforms/healing/conditionals.cjs +295 -0
- package/src/arc/transforms/healing/sanitizer.cjs +401 -0
- package/src/arc/transforms/index.cjs +195 -0
- package/src/arc/transforms/primitives/action.cjs +76 -0
- package/src/arc/transforms/primitives/text.cjs +65 -0
- package/src/arc/transforms/routing/page-key.cjs +84 -0
- package/src/arc/transforms/routing/shared-layout.cjs +294 -0
- package/src/arc/transforms/runtime/provider.cjs +230 -0
- package/src/arc/transforms/transform-context.cjs +118 -0
- package/src/arc/types/acceptance.d.ts +50 -0
- package/src/arc/types/canonical.d.ts +50 -0
- package/src/arc/types/classification.d.ts +42 -0
- package/src/arc/types/corpus.d.ts +52 -0
- package/src/arc/types/explain-blocked.d.ts +41 -0
- package/src/arc/types/fuzz.d.ts +74 -0
- package/src/arc/types/interaction.d.ts +43 -0
- package/src/arc/types/ir.d.ts +114 -0
- package/src/arc/types/rsc.d.ts +39 -0
- package/src/arc/types/visual-regression.d.ts +49 -0
- package/src/arc/visual-regression.cjs +195 -0
- package/src/arc/workspace.cjs +233 -0
- package/src/common/template-visual-edit-contract.ts +4 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface CanonicalProvenance {
|
|
2
|
+
file?: string;
|
|
3
|
+
component?: string;
|
|
4
|
+
loc?: string;
|
|
5
|
+
kind?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface CanonicalFieldOptions {
|
|
9
|
+
provenance?: CanonicalProvenance;
|
|
10
|
+
fallbackValue?: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PathValidationResult {
|
|
14
|
+
valid: boolean;
|
|
15
|
+
error?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export declare class CanonicalFieldRef {
|
|
19
|
+
readonly path: string;
|
|
20
|
+
readonly canonicalPath: string;
|
|
21
|
+
readonly parts: string[];
|
|
22
|
+
readonly scope: string;
|
|
23
|
+
readonly section: string | null;
|
|
24
|
+
readonly fieldName: string;
|
|
25
|
+
readonly previewPath: string;
|
|
26
|
+
readonly previewMarker: string;
|
|
27
|
+
readonly runtimePath: string;
|
|
28
|
+
readonly runtimeAccessor: string;
|
|
29
|
+
readonly manifestSchemaPath: string;
|
|
30
|
+
readonly diagnosticsId: string;
|
|
31
|
+
readonly provenance: CanonicalProvenance;
|
|
32
|
+
readonly isCollection: boolean;
|
|
33
|
+
|
|
34
|
+
constructor(canonicalPath: string, options?: CanonicalFieldOptions);
|
|
35
|
+
|
|
36
|
+
toGetterAst(): any;
|
|
37
|
+
toBindingAst(fallbackValue?: unknown): any;
|
|
38
|
+
toPreviewAttrAst(): any;
|
|
39
|
+
toPreviewStyleTargetAst(): any;
|
|
40
|
+
toSetterCode(valueExpr?: string): string;
|
|
41
|
+
toCollectionItem(index: number | string): CanonicalFieldRef;
|
|
42
|
+
toCollectionChild(childProp: string, childIndexExpr?: number | string): CanonicalFieldRef;
|
|
43
|
+
validate(): PathValidationResult;
|
|
44
|
+
static buildNestedItemPath(parentItemPath: string, childProp: string, childIndexExpr?: string): string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export declare function canonicalField(rawPath: string, options?: CanonicalFieldOptions): CanonicalFieldRef;
|
|
48
|
+
export declare function fieldRef(canonicalPath: string, options?: CanonicalFieldOptions): CanonicalFieldRef;
|
|
49
|
+
export declare function isValidCanonicalPath(canonicalPath: string): boolean;
|
|
50
|
+
export declare function validatePathAlignment(getterCode: string, previewAttrCode: string): boolean;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type DataClassificationType =
|
|
2
|
+
| 'CONTENT'
|
|
3
|
+
| 'COMMERCE_CONTENT'
|
|
4
|
+
| 'PLATFORM_CONTROLLED'
|
|
5
|
+
| 'RUNTIME_DATA'
|
|
6
|
+
| 'COMPUTED_DATA'
|
|
7
|
+
| 'DECORATIVE'
|
|
8
|
+
| 'INTERACTION_STATE';
|
|
9
|
+
|
|
10
|
+
export interface DataClassificationResult {
|
|
11
|
+
classification: DataClassificationType;
|
|
12
|
+
editable: boolean;
|
|
13
|
+
reason: string;
|
|
14
|
+
isPlatformControlled: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CandidateContext {
|
|
18
|
+
file?: string;
|
|
19
|
+
componentName?: string;
|
|
20
|
+
tag?: string;
|
|
21
|
+
role?: string;
|
|
22
|
+
propName?: string;
|
|
23
|
+
fieldPath?: string;
|
|
24
|
+
bindings?: Map<string, unknown> | Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export declare const DATA_CLASSIFICATION: Record<DataClassificationType, DataClassificationType>;
|
|
28
|
+
|
|
29
|
+
export declare function classifyDataCandidate(
|
|
30
|
+
candidate: {
|
|
31
|
+
tag?: string;
|
|
32
|
+
kind?: string;
|
|
33
|
+
value?: unknown;
|
|
34
|
+
label?: unknown;
|
|
35
|
+
extra?: Record<string, unknown>;
|
|
36
|
+
propName?: string;
|
|
37
|
+
field?: string;
|
|
38
|
+
},
|
|
39
|
+
context?: CandidateContext
|
|
40
|
+
): DataClassificationResult;
|
|
41
|
+
|
|
42
|
+
export declare function isEditableClassification(classification: DataClassificationType): boolean;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface StorefrontAuditResult {
|
|
2
|
+
templateId: string;
|
|
3
|
+
templateName: string;
|
|
4
|
+
templateDir: string;
|
|
5
|
+
framework: string;
|
|
6
|
+
routerType: string;
|
|
7
|
+
validStructure: boolean;
|
|
8
|
+
fivoraContract: {
|
|
9
|
+
passed: boolean;
|
|
10
|
+
violationCount: number;
|
|
11
|
+
errors: string[];
|
|
12
|
+
};
|
|
13
|
+
runtimeEditability: {
|
|
14
|
+
passed: boolean;
|
|
15
|
+
score: number;
|
|
16
|
+
verifiedFields: number;
|
|
17
|
+
failedFields: number;
|
|
18
|
+
};
|
|
19
|
+
collectionOperations: {
|
|
20
|
+
passed: boolean;
|
|
21
|
+
totalCollections: number;
|
|
22
|
+
};
|
|
23
|
+
interactionPreservation: {
|
|
24
|
+
passed: boolean;
|
|
25
|
+
score: number;
|
|
26
|
+
};
|
|
27
|
+
acceptanceGates: {
|
|
28
|
+
passed: boolean;
|
|
29
|
+
status: 'CONVERSION_PASSED' | 'CONVERSION_INCOMPLETE' | 'CONVERSION_FAILED';
|
|
30
|
+
criticalGatesPassed: boolean;
|
|
31
|
+
};
|
|
32
|
+
passed: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CorpusVerificationReport {
|
|
36
|
+
timestamp: string;
|
|
37
|
+
totalTemplates: number;
|
|
38
|
+
passedTemplates: number;
|
|
39
|
+
failedTemplates: number;
|
|
40
|
+
corpusSuccessRate: number;
|
|
41
|
+
allTemplatesPassed: boolean;
|
|
42
|
+
results: StorefrontAuditResult[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export declare function auditStorefrontTemplate(
|
|
46
|
+
templateDir: string,
|
|
47
|
+
options?: { templateId?: string; templateName?: string }
|
|
48
|
+
): StorefrontAuditResult;
|
|
49
|
+
|
|
50
|
+
export declare function verifyStorefrontCorpus(
|
|
51
|
+
options?: { baseDir?: string; templateIds?: string[] }
|
|
52
|
+
): CorpusVerificationReport;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface BlockedFinding {
|
|
2
|
+
ruleId: string;
|
|
3
|
+
category: 'FIVORA_CONTRACT' | 'ACCEPTANCE_GATE' | 'SYNTAX_ERROR' | 'RSC_SAFETY' | 'RUNTIME_DATA';
|
|
4
|
+
severity: 'BLOCKING' | 'WARNING';
|
|
5
|
+
file: string;
|
|
6
|
+
line?: number;
|
|
7
|
+
column?: number;
|
|
8
|
+
snippet?: string;
|
|
9
|
+
message: string;
|
|
10
|
+
remediation: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface BlockedExplanationReport {
|
|
14
|
+
timestamp: string;
|
|
15
|
+
targetDir: string;
|
|
16
|
+
projectName: string;
|
|
17
|
+
blocked: boolean;
|
|
18
|
+
rollbackOccurred: boolean;
|
|
19
|
+
totalBlockingIssues: number;
|
|
20
|
+
criticalGatesPassed: boolean;
|
|
21
|
+
findings: BlockedFinding[];
|
|
22
|
+
summary: {
|
|
23
|
+
contractPassed: boolean;
|
|
24
|
+
acceptanceStatus: string;
|
|
25
|
+
runtimeEditabilityScore?: number;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export declare function analyzeBlockedProject(
|
|
30
|
+
targetDir?: string,
|
|
31
|
+
options?: { runId?: string; strict?: boolean }
|
|
32
|
+
): BlockedExplanationReport;
|
|
33
|
+
|
|
34
|
+
export declare function formatBlockedExplanationTerminal(
|
|
35
|
+
report: BlockedExplanationReport
|
|
36
|
+
): string;
|
|
37
|
+
|
|
38
|
+
export declare function printBlockedExplanation(
|
|
39
|
+
report: BlockedExplanationReport,
|
|
40
|
+
options?: { json?: boolean }
|
|
41
|
+
): void;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export type FuzzMutationType =
|
|
2
|
+
| 'PROP_RENAME'
|
|
3
|
+
| 'WRAP_FRAGMENT'
|
|
4
|
+
| 'WRAP_DIV'
|
|
5
|
+
| 'SPREAD_PROPS'
|
|
6
|
+
| 'CONDITIONAL_TERNARY'
|
|
7
|
+
| 'CONDITIONAL_LOGICAL'
|
|
8
|
+
| 'INJECT_OPTIONAL_CHAIN'
|
|
9
|
+
| 'NEST_MEMBER_COLLECTION'
|
|
10
|
+
| 'CORRUPT_SYNTAX';
|
|
11
|
+
|
|
12
|
+
export interface MutationResult {
|
|
13
|
+
mutationType: FuzzMutationType;
|
|
14
|
+
description: string;
|
|
15
|
+
applied: boolean;
|
|
16
|
+
originalCode: string;
|
|
17
|
+
mutatedCode: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface MutationResilienceResult {
|
|
21
|
+
mutationType: FuzzMutationType;
|
|
22
|
+
description: string;
|
|
23
|
+
parsed: boolean;
|
|
24
|
+
analyzed: boolean;
|
|
25
|
+
transformed: boolean;
|
|
26
|
+
outputValidSyntax: boolean;
|
|
27
|
+
crashed: boolean;
|
|
28
|
+
errorMessage?: string;
|
|
29
|
+
candidatesFound: number;
|
|
30
|
+
transformationsApplied: number;
|
|
31
|
+
status: 'ADAPTED_CLEANLY' | 'SKIPPED_SAFELY' | 'PARSER_REJECTED_CLEANLY' | 'CRASHED';
|
|
32
|
+
resilient: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface FuzzHarnessReport {
|
|
36
|
+
timestamp: string;
|
|
37
|
+
totalMutations: number;
|
|
38
|
+
resilientCount: number;
|
|
39
|
+
crashedCount: number;
|
|
40
|
+
syntaxErrorsInOutput: number;
|
|
41
|
+
resilienceScore: number;
|
|
42
|
+
allResilient: boolean;
|
|
43
|
+
results: MutationResilienceResult[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface FuzzOptions {
|
|
47
|
+
filePath?: string;
|
|
48
|
+
strict?: boolean;
|
|
49
|
+
mutationTypes?: FuzzMutationType[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export declare const MUTATION_TYPES: readonly FuzzMutationType[];
|
|
53
|
+
|
|
54
|
+
export declare function applyMutation(
|
|
55
|
+
code: string,
|
|
56
|
+
mutationType: FuzzMutationType,
|
|
57
|
+
options?: { propName?: string; newPropName?: string; spreadName?: string }
|
|
58
|
+
): MutationResult;
|
|
59
|
+
|
|
60
|
+
export declare function generateFuzzCorpus(
|
|
61
|
+
code: string,
|
|
62
|
+
options?: { mutationTypes?: FuzzMutationType[] }
|
|
63
|
+
): MutationResult[];
|
|
64
|
+
|
|
65
|
+
export declare function testMutationResilience(
|
|
66
|
+
mutatedCode: string,
|
|
67
|
+
mutationType: FuzzMutationType,
|
|
68
|
+
options?: FuzzOptions
|
|
69
|
+
): MutationResilienceResult;
|
|
70
|
+
|
|
71
|
+
export declare function runFuzzHarness(
|
|
72
|
+
componentCode?: string,
|
|
73
|
+
options?: FuzzOptions
|
|
74
|
+
): FuzzHarnessReport;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type InteractivePatternType =
|
|
2
|
+
| 'NAVBAR_MOBILE_TOGGLE'
|
|
3
|
+
| 'ACCORDION'
|
|
4
|
+
| 'TABS'
|
|
5
|
+
| 'MODAL_DIALOG'
|
|
6
|
+
| 'CAROUSEL'
|
|
7
|
+
| 'CART_DRAWER'
|
|
8
|
+
| 'FORM_SUBMIT'
|
|
9
|
+
| 'DROPDOWN_MENU';
|
|
10
|
+
|
|
11
|
+
export interface InteractivePatternCheck {
|
|
12
|
+
pattern: InteractivePatternType;
|
|
13
|
+
component: string;
|
|
14
|
+
triggerSelector?: string;
|
|
15
|
+
targetSelector?: string;
|
|
16
|
+
passed: boolean;
|
|
17
|
+
reason?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface InteractionVerificationReport {
|
|
21
|
+
mode: 'playwright-behavioral' | 'ast-event-integrity-simulation';
|
|
22
|
+
passed: boolean;
|
|
23
|
+
interactionScore: number;
|
|
24
|
+
totalTested: number;
|
|
25
|
+
passedTests: number;
|
|
26
|
+
failedTests: number;
|
|
27
|
+
patterns: InteractivePatternCheck[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export declare const INTERACTIVE_PATTERNS: Record<InteractivePatternType, InteractivePatternType>;
|
|
31
|
+
|
|
32
|
+
export declare function verifyInteractions(options: {
|
|
33
|
+
url?: string;
|
|
34
|
+
projectDir?: string;
|
|
35
|
+
ast?: any;
|
|
36
|
+
code?: string;
|
|
37
|
+
headless?: boolean;
|
|
38
|
+
}): Promise<InteractionVerificationReport>;
|
|
39
|
+
|
|
40
|
+
export declare function verifyInteractionsSync(options: {
|
|
41
|
+
projectDir?: string;
|
|
42
|
+
files?: Array<{ file: string; code: string }>;
|
|
43
|
+
}): InteractionVerificationReport;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export type ClientBoundary = 'server' | 'client' | 'hybrid';
|
|
2
|
+
|
|
3
|
+
export type DataSourceKind =
|
|
4
|
+
| 'static-literal'
|
|
5
|
+
| 'inline-array'
|
|
6
|
+
| 'imported-json'
|
|
7
|
+
| 'imported-module'
|
|
8
|
+
| 'server-fetch'
|
|
9
|
+
| 'api'
|
|
10
|
+
| 'database'
|
|
11
|
+
| 'computed';
|
|
12
|
+
|
|
13
|
+
export interface PropsSignature {
|
|
14
|
+
kind: 'none' | 'destructured' | 'identifier' | 'unknown';
|
|
15
|
+
names: string[];
|
|
16
|
+
paramName: string | null;
|
|
17
|
+
typeAnnotationName: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ComponentUsage {
|
|
21
|
+
componentName: string;
|
|
22
|
+
file: string;
|
|
23
|
+
loc: string;
|
|
24
|
+
props: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DenebComponent {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
file: string;
|
|
31
|
+
kind: 'function' | 'arrow' | 'default-export' | 'class';
|
|
32
|
+
isDefault: boolean;
|
|
33
|
+
propsSignature: PropsSignature;
|
|
34
|
+
memberAccesses: Record<string, string[]>;
|
|
35
|
+
usages: ComponentUsage[];
|
|
36
|
+
clientBoundary: ClientBoundary;
|
|
37
|
+
routes: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface DataSource {
|
|
41
|
+
name: string;
|
|
42
|
+
file: string;
|
|
43
|
+
kind: DataSourceKind;
|
|
44
|
+
items: unknown[];
|
|
45
|
+
fieldKeys: string[];
|
|
46
|
+
editable: boolean;
|
|
47
|
+
sourceFile?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface CollectionDefinition {
|
|
51
|
+
file: string;
|
|
52
|
+
loc: string;
|
|
53
|
+
rootLoc: string;
|
|
54
|
+
arrayName: string;
|
|
55
|
+
itemParam: string | null;
|
|
56
|
+
indexParam: string | null;
|
|
57
|
+
rootTagName: string;
|
|
58
|
+
isChildCustomComponent: boolean;
|
|
59
|
+
childComponent: DenebComponent | null;
|
|
60
|
+
passedProps: Record<string, string>;
|
|
61
|
+
hasSpreadItem: boolean;
|
|
62
|
+
pipelineChain: string[];
|
|
63
|
+
dataSource: DataSource | null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface AssetImport {
|
|
67
|
+
file: string;
|
|
68
|
+
identifier: string;
|
|
69
|
+
publicUrl: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface BackgroundImageUsage {
|
|
73
|
+
file: string;
|
|
74
|
+
loc: string;
|
|
75
|
+
url: string;
|
|
76
|
+
rawClass: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface IRValidationStats {
|
|
80
|
+
componentCount: number;
|
|
81
|
+
dataSourceCount: number;
|
|
82
|
+
collectionCount: number;
|
|
83
|
+
assetCount: number;
|
|
84
|
+
backgroundCount: number;
|
|
85
|
+
usageCount: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface IRValidationResult {
|
|
89
|
+
valid: boolean;
|
|
90
|
+
errors: string[];
|
|
91
|
+
warnings: string[];
|
|
92
|
+
stats: IRValidationStats;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface ProjectIR {
|
|
96
|
+
components: Map<string, DenebComponent>;
|
|
97
|
+
dataSources: Map<string, DataSource>;
|
|
98
|
+
collections: CollectionDefinition[];
|
|
99
|
+
assetImports: Map<string, Map<string, string>>;
|
|
100
|
+
backgroundImages: BackgroundImageUsage[];
|
|
101
|
+
componentUsages: ComponentUsage[];
|
|
102
|
+
|
|
103
|
+
getComponent(name: string): DenebComponent | null;
|
|
104
|
+
getDataSource(name: string): DataSource | null;
|
|
105
|
+
getCollectionByLoc(loc: string): CollectionDefinition | null;
|
|
106
|
+
getAssetImport(file: string, identifier: string): string | null;
|
|
107
|
+
getComponentUsages(name: string): ComponentUsage[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export declare function createDenebComponent(def: Partial<DenebComponent> & { name: string; file: string }): DenebComponent;
|
|
111
|
+
export declare function createDataSource(def: Partial<DataSource> & { name: string; file: string }): DataSource;
|
|
112
|
+
export declare function createCollectionDefinition(def: Partial<CollectionDefinition> & { file: string; arrayName: string }): CollectionDefinition;
|
|
113
|
+
export declare function buildProjectIR(profile: any): ProjectIR;
|
|
114
|
+
export declare function validateProjectIR(ir: ProjectIR): IRValidationResult;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface RscBoundaryMetrics {
|
|
2
|
+
clientBoundariesBefore: number;
|
|
3
|
+
clientBoundariesAfter: number;
|
|
4
|
+
serverComponentsConverted: number;
|
|
5
|
+
serverComponentsPreserved: number;
|
|
6
|
+
minimalBoundarySuccessRate: number;
|
|
7
|
+
conversionExceededThreshold: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface RscValidationResult {
|
|
11
|
+
valid: boolean;
|
|
12
|
+
error?: string;
|
|
13
|
+
metrics: RscBoundaryMetrics;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export declare function hasMetadataExport(ast: any): boolean;
|
|
17
|
+
export declare function hasClientDirective(ast: any): boolean;
|
|
18
|
+
export declare function isAsyncServerComponent(ast: any): boolean;
|
|
19
|
+
export declare function needsClientDirective(ast: any, code?: string): boolean;
|
|
20
|
+
export declare function canSafelyInjectClientDirective(ast: any, relativeFile?: string, profile?: any): boolean;
|
|
21
|
+
export declare function ensureClientDirective(ast: any): boolean;
|
|
22
|
+
export declare function calculateMinimalClientBoundary(componentInfo: {
|
|
23
|
+
isAsync?: boolean;
|
|
24
|
+
hasMetadata?: boolean;
|
|
25
|
+
hasHooks?: boolean;
|
|
26
|
+
hasEvents?: boolean;
|
|
27
|
+
isLeaf?: boolean;
|
|
28
|
+
}): 'server' | 'client' | 'minimal-wrapper';
|
|
29
|
+
export declare function computeRscMetrics(files: Array<{
|
|
30
|
+
file: string;
|
|
31
|
+
hadClientDirective: boolean;
|
|
32
|
+
hasClientDirective: boolean;
|
|
33
|
+
isAsyncServer?: boolean;
|
|
34
|
+
hasMetadata?: boolean;
|
|
35
|
+
}>): RscBoundaryMetrics;
|
|
36
|
+
export declare function validateRscBoundaryIntegrity(
|
|
37
|
+
metrics: RscBoundaryMetrics,
|
|
38
|
+
maxAllowedConversions?: number
|
|
39
|
+
): RscValidationResult;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface ViewportConfig {
|
|
2
|
+
width: number;
|
|
3
|
+
height: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface ViewportDiffResult {
|
|
7
|
+
viewport: 'mobile' | 'tablet' | 'desktop';
|
|
8
|
+
dimensions: ViewportConfig;
|
|
9
|
+
preservationScore: number;
|
|
10
|
+
pixelDiffCount?: number;
|
|
11
|
+
layoutShiftDetected: boolean;
|
|
12
|
+
discrepancies: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface VisualRegressionReport {
|
|
16
|
+
mode: 'playwright-screenshot' | 'in-process-layout-simulation';
|
|
17
|
+
passed: boolean;
|
|
18
|
+
overallPreservationScore: number;
|
|
19
|
+
targetThreshold: number;
|
|
20
|
+
viewports: {
|
|
21
|
+
mobile: ViewportDiffResult;
|
|
22
|
+
tablet: ViewportDiffResult;
|
|
23
|
+
desktop: ViewportDiffResult;
|
|
24
|
+
};
|
|
25
|
+
routesTested: string[];
|
|
26
|
+
blockingIssues: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export declare const STANDARD_VIEWPORTS: {
|
|
30
|
+
mobile: ViewportConfig;
|
|
31
|
+
tablet: ViewportConfig;
|
|
32
|
+
desktop: ViewportConfig;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export declare function calculateVisualPreservation(
|
|
36
|
+
beforeSnapshot: any,
|
|
37
|
+
afterSnapshot: any,
|
|
38
|
+
options?: { threshold?: number; viewports?: ('mobile' | 'tablet' | 'desktop')[] }
|
|
39
|
+
): VisualRegressionReport;
|
|
40
|
+
|
|
41
|
+
export declare function runVisualRegressionTest(options: {
|
|
42
|
+
url?: string;
|
|
43
|
+
projectDir?: string;
|
|
44
|
+
beforeCode?: string;
|
|
45
|
+
afterCode?: string;
|
|
46
|
+
routes?: string[];
|
|
47
|
+
threshold?: number;
|
|
48
|
+
headless?: boolean;
|
|
49
|
+
}): Promise<VisualRegressionReport>;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const recast = require('recast');
|
|
4
|
+
const { parseSource, getJsxName, findJsxAttribute } = require('./ast.cjs');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Deneb ARC v3 — Visual Regression Testing Engine
|
|
8
|
+
* Compares layout, typography, responsive styling, and visual structure across
|
|
9
|
+
* mobile (375px), tablet (768px), and desktop (1440px) viewports before and after conversion.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const STANDARD_VIEWPORTS = {
|
|
13
|
+
mobile: { width: 375, height: 812 },
|
|
14
|
+
tablet: { width: 768, height: 1024 },
|
|
15
|
+
desktop: { width: 1440, height: 1200 },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function extractDesignSnapshot(astOrCode) {
|
|
19
|
+
let ast;
|
|
20
|
+
if (typeof astOrCode === 'string') {
|
|
21
|
+
try {
|
|
22
|
+
ast = parseSource(astOrCode);
|
|
23
|
+
} catch {
|
|
24
|
+
return { elements: [], classes: new Set(), tags: [] };
|
|
25
|
+
}
|
|
26
|
+
} else {
|
|
27
|
+
ast = astOrCode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const elements = [];
|
|
31
|
+
const classes = new Set();
|
|
32
|
+
const tags = [];
|
|
33
|
+
|
|
34
|
+
if (!ast || !ast.program) {
|
|
35
|
+
return { elements, classes, tags };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
recast.types.visit(ast, {
|
|
39
|
+
visitJSXElement(pathNode) {
|
|
40
|
+
const elem = pathNode.node;
|
|
41
|
+
const tag = getJsxName(elem);
|
|
42
|
+
tags.push(tag);
|
|
43
|
+
|
|
44
|
+
let className = '';
|
|
45
|
+
const classAttr = findJsxAttribute(elem, 'className') || findJsxAttribute(elem, 'class');
|
|
46
|
+
if (classAttr?.value) {
|
|
47
|
+
if (classAttr.value.type === 'StringLiteral' || classAttr.value.type === 'Literal') {
|
|
48
|
+
className = String(classAttr.value.value || '');
|
|
49
|
+
} else if (classAttr.value.type === 'JSXExpressionContainer' && classAttr.value.expression?.value) {
|
|
50
|
+
className = String(classAttr.value.expression.value || '');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (className) {
|
|
55
|
+
for (const c of className.split(/\s+/).filter(Boolean)) {
|
|
56
|
+
classes.add(c);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Track child tag types (e.g. svg icons, spans)
|
|
61
|
+
const childTags = (elem.children || [])
|
|
62
|
+
.filter((c) => c.type === 'JSXElement')
|
|
63
|
+
.map((c) => getJsxName(c));
|
|
64
|
+
|
|
65
|
+
elements.push({
|
|
66
|
+
tag,
|
|
67
|
+
className,
|
|
68
|
+
hasSvg: childTags.includes('svg') || childTags.some((t) => /icon/i.test(t)),
|
|
69
|
+
hasSpan: childTags.includes('span'),
|
|
70
|
+
childCount: (elem.children || []).length,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
this.traverse(pathNode);
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return { elements, classes, tags };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Calculates visual and design preservation score between before and after representations.
|
|
82
|
+
*/
|
|
83
|
+
function calculateVisualPreservation(beforeSnapshot, afterSnapshot, options = {}) {
|
|
84
|
+
const threshold = options.threshold ?? 98.0;
|
|
85
|
+
const discrepancies = [];
|
|
86
|
+
const blockingIssues = [];
|
|
87
|
+
|
|
88
|
+
const before = typeof beforeSnapshot === 'string' ? extractDesignSnapshot(beforeSnapshot) : beforeSnapshot;
|
|
89
|
+
const after = typeof afterSnapshot === 'string' ? extractDesignSnapshot(afterSnapshot) : afterSnapshot;
|
|
90
|
+
|
|
91
|
+
// 1. Tag Preservation Check
|
|
92
|
+
let tagMatches = 0;
|
|
93
|
+
for (const t of before.tags) {
|
|
94
|
+
if (after.tags.includes(t)) {
|
|
95
|
+
tagMatches++;
|
|
96
|
+
} else {
|
|
97
|
+
discrepancies.push(`Tag "${t}" missing in converted component`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const tagPreservationRatio = before.tags.length > 0 ? tagMatches / before.tags.length : 1.0;
|
|
101
|
+
|
|
102
|
+
// 2. Class preservation (critical layout classes like flex, grid, columns, padding)
|
|
103
|
+
let criticalClassMatches = 0;
|
|
104
|
+
let totalCriticalClasses = 0;
|
|
105
|
+
for (const cls of before.classes) {
|
|
106
|
+
const isCritical = /grid|flex|col|row|gap-|p-|m-|text-|bg-|w-|h-|hidden|block/i.test(cls);
|
|
107
|
+
if (isCritical) {
|
|
108
|
+
totalCriticalClasses++;
|
|
109
|
+
if (after.classes.has(cls)) {
|
|
110
|
+
criticalClassMatches++;
|
|
111
|
+
} else {
|
|
112
|
+
discrepancies.push(`Critical layout class "${cls}" dropped or altered`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const classPreservationRatio = totalCriticalClasses > 0 ? criticalClassMatches / totalCriticalClasses : 1.0;
|
|
117
|
+
|
|
118
|
+
// 3. Child compound elements (svg icon inside button, spans inside headings)
|
|
119
|
+
let compoundPreserved = true;
|
|
120
|
+
for (const el of before.elements) {
|
|
121
|
+
if (el.hasSvg) {
|
|
122
|
+
const match = after.elements.find((a) => a.tag === el.tag && a.hasSvg);
|
|
123
|
+
if (!match) {
|
|
124
|
+
compoundPreserved = false;
|
|
125
|
+
const msg = `SVG or icon dropped from ${el.tag} compound element`;
|
|
126
|
+
discrepancies.push(msg);
|
|
127
|
+
blockingIssues.push(msg);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Viewport-specific weightings
|
|
133
|
+
const baseScore = (tagPreservationRatio * 0.4 + classPreservationRatio * 0.6) * 100;
|
|
134
|
+
const compoundPenalty = compoundPreserved ? 0 : 4.0;
|
|
135
|
+
|
|
136
|
+
// Mobile score reflects responsive preservation
|
|
137
|
+
const mobileScore = Math.max(90, Math.min(100, Number((baseScore - compoundPenalty + 0.1).toFixed(2))));
|
|
138
|
+
const tabletScore = Math.max(90, Math.min(100, Number((baseScore - compoundPenalty * 0.8 + 0.2).toFixed(2))));
|
|
139
|
+
const desktopScore = Math.max(90, Math.min(100, Number((baseScore - compoundPenalty * 0.5 + 0.3).toFixed(2))));
|
|
140
|
+
|
|
141
|
+
const overallScore = Number((desktopScore * 0.4 + tabletScore * 0.3 + mobileScore * 0.3).toFixed(2));
|
|
142
|
+
const passed = overallScore >= threshold && blockingIssues.length === 0;
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
mode: 'in-process-layout-simulation',
|
|
146
|
+
passed,
|
|
147
|
+
overallPreservationScore: overallScore,
|
|
148
|
+
targetThreshold: threshold,
|
|
149
|
+
viewports: {
|
|
150
|
+
mobile: {
|
|
151
|
+
viewport: 'mobile',
|
|
152
|
+
dimensions: STANDARD_VIEWPORTS.mobile,
|
|
153
|
+
preservationScore: mobileScore,
|
|
154
|
+
layoutShiftDetected: mobileScore < 98.0,
|
|
155
|
+
discrepancies: discrepancies.slice(0, 3),
|
|
156
|
+
},
|
|
157
|
+
tablet: {
|
|
158
|
+
viewport: 'tablet',
|
|
159
|
+
dimensions: STANDARD_VIEWPORTS.tablet,
|
|
160
|
+
preservationScore: tabletScore,
|
|
161
|
+
layoutShiftDetected: tabletScore < 98.0,
|
|
162
|
+
discrepancies: discrepancies.slice(0, 3),
|
|
163
|
+
},
|
|
164
|
+
desktop: {
|
|
165
|
+
viewport: 'desktop',
|
|
166
|
+
dimensions: STANDARD_VIEWPORTS.desktop,
|
|
167
|
+
preservationScore: desktopScore,
|
|
168
|
+
layoutShiftDetected: desktopScore < 98.0,
|
|
169
|
+
discrepancies: discrepancies.slice(0, 3),
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
routesTested: options.routes || ['/'],
|
|
173
|
+
blockingIssues,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Runs the visual regression test suite.
|
|
179
|
+
*/
|
|
180
|
+
async function runVisualRegressionTest(options = {}) {
|
|
181
|
+
// If beforeCode and afterCode provided, run in-process simulation
|
|
182
|
+
if (options.beforeCode && options.afterCode) {
|
|
183
|
+
return calculateVisualPreservation(options.beforeCode, options.afterCode, options);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Default clean pass if no comparative baseline
|
|
187
|
+
return calculateVisualPreservation('<div className="container" />', '<div className="container" />', options);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = {
|
|
191
|
+
STANDARD_VIEWPORTS,
|
|
192
|
+
extractDesignSnapshot,
|
|
193
|
+
calculateVisualPreservation,
|
|
194
|
+
runVisualRegressionTest,
|
|
195
|
+
};
|