@burdenoff/fe-libs 2026.725.4 → 2026.730.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/dist/shared/components/assistantWidget/AssistantWidget.d.ts +12 -0
- package/dist/shared/components/assistantWidget/AssistantWidget.d.ts.map +1 -1
- package/dist/shared/components/assistantWidget/AssistantWidget.js +241 -200
- package/dist/shared/components/assistantWidget/types.d.ts +8 -0
- package/dist/shared/components/assistantWidget/types.d.ts.map +1 -1
- package/package.json +8 -2
- package/scripts/graphql-schema-drift/README.md +137 -0
- package/scripts/graphql-schema-drift/cli.ts +83 -0
- package/scripts/graphql-schema-drift/extract.ts +95 -0
- package/scripts/graphql-schema-drift/fragments.ts +92 -0
- package/scripts/graphql-schema-drift/fs-util.ts +22 -0
- package/scripts/graphql-schema-drift/index.ts +11 -0
- package/scripts/graphql-schema-drift/schema.ts +42 -0
- package/scripts/graphql-schema-drift/types.ts +73 -0
- package/scripts/graphql-schema-drift/validate.ts +159 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { Kind, parse, validate, type DocumentNode, type FragmentDefinitionNode } from 'graphql';
|
|
4
|
+
import { extractBlocks } from './extract';
|
|
5
|
+
import { safeListFilesRecursive } from './fs-util';
|
|
6
|
+
import { loadFragmentDefinitionsFromDirs, resolveFragments } from './fragments';
|
|
7
|
+
import { loadSchemaGroups } from './schema';
|
|
8
|
+
import type { SchemaDriftConfig } from './types';
|
|
9
|
+
|
|
10
|
+
export interface DriftError {
|
|
11
|
+
file: string;
|
|
12
|
+
index: number;
|
|
13
|
+
operationName: string | null;
|
|
14
|
+
messages: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DriftReport {
|
|
18
|
+
errors: DriftError[];
|
|
19
|
+
/** Count of operation documents actually validated (fragment-only blocks are not counted). */
|
|
20
|
+
documentCount: number;
|
|
21
|
+
fileCount: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getOperationName(doc: DocumentNode): string | null {
|
|
25
|
+
for (const def of doc.definitions) {
|
|
26
|
+
if (def.kind === Kind.OPERATION_DEFINITION || def.kind === Kind.FRAGMENT_DEFINITION) {
|
|
27
|
+
return def.name?.value ?? null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Run the configured extraction + validation pipeline against a repo.
|
|
35
|
+
*
|
|
36
|
+
* `repoRoot` is the directory `schema-drift.config.json` lives in — all
|
|
37
|
+
* paths in the config (`sourceRoots[].dir`, `fragmentDirs`,
|
|
38
|
+
* `schemaGroups[].files`) are resolved relative to it.
|
|
39
|
+
*/
|
|
40
|
+
export function runValidation(config: SchemaDriftConfig, repoRoot: string): DriftReport {
|
|
41
|
+
const schemaGroups = loadSchemaGroups(config.schemaGroups, repoRoot);
|
|
42
|
+
const treatUnresolvedAsError = config.treatUnresolvedInterpolationAsError ?? true;
|
|
43
|
+
|
|
44
|
+
// Pass 1: scan every configured source file once, extracting both operation
|
|
45
|
+
// blocks and inline `fragment` blocks. Inline fragments are pooled globally
|
|
46
|
+
// (a fragment defined in one file is commonly spread from a hook/component
|
|
47
|
+
// in another within the same MFE).
|
|
48
|
+
const sourceFiles: string[] = [];
|
|
49
|
+
for (const root of config.sourceRoots) {
|
|
50
|
+
const extensions = root.extensions ?? ['.ts', '.tsx'];
|
|
51
|
+
sourceFiles.push(...safeListFilesRecursive(join(repoRoot, root.dir), extensions));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const inlineFragmentPool = new Map<string, FragmentDefinitionNode>();
|
|
55
|
+
const pendingOperations: Array<{
|
|
56
|
+
relFile: string;
|
|
57
|
+
index: number;
|
|
58
|
+
source: string;
|
|
59
|
+
unresolved: string[];
|
|
60
|
+
}> = [];
|
|
61
|
+
|
|
62
|
+
for (const file of sourceFiles) {
|
|
63
|
+
const relFile = file.slice(repoRoot.length + 1);
|
|
64
|
+
const text = readFileSync(file, 'utf8');
|
|
65
|
+
for (const block of extractBlocks(text, relFile)) {
|
|
66
|
+
if (block.isFragment) {
|
|
67
|
+
if (block.unresolved.length > 0) continue; // can't parse a fragment with unresolved interpolation; surfaces via any spread that needs it
|
|
68
|
+
try {
|
|
69
|
+
const fragDoc = parse(block.source);
|
|
70
|
+
for (const def of fragDoc.definitions) {
|
|
71
|
+
if (def.kind === Kind.FRAGMENT_DEFINITION) inlineFragmentPool.set(def.name.value, def);
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// Malformed inline fragment: fall through silently here — if it's
|
|
75
|
+
// actually spread anywhere, the consuming operation will fail to
|
|
76
|
+
// resolve the fragment name and be reported as an error there.
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
pendingOperations.push({
|
|
81
|
+
relFile,
|
|
82
|
+
index: block.index,
|
|
83
|
+
source: block.source,
|
|
84
|
+
unresolved: block.unresolved,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const fileFragmentPool = config.fragmentDirs
|
|
90
|
+
? loadFragmentDefinitionsFromDirs(config.fragmentDirs, repoRoot)
|
|
91
|
+
: new Map<string, FragmentDefinitionNode>();
|
|
92
|
+
const fragmentPool = new Map<string, FragmentDefinitionNode>([
|
|
93
|
+
...fileFragmentPool,
|
|
94
|
+
...inlineFragmentPool,
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
// Pass 2: parse + resolve fragments + validate every operation block against
|
|
98
|
+
// every schema group, in order. A document is clean if ANY group accepts it.
|
|
99
|
+
const errors: DriftError[] = [];
|
|
100
|
+
let documentCount = 0;
|
|
101
|
+
|
|
102
|
+
for (const op of pendingOperations) {
|
|
103
|
+
documentCount++;
|
|
104
|
+
|
|
105
|
+
if (op.unresolved.length > 0 && treatUnresolvedAsError) {
|
|
106
|
+
errors.push({
|
|
107
|
+
file: op.relFile,
|
|
108
|
+
index: op.index,
|
|
109
|
+
operationName: null,
|
|
110
|
+
messages: [
|
|
111
|
+
`Unresolved interpolation(s): ${[...new Set(op.unresolved)].join(', ')}. Define them as ` +
|
|
112
|
+
`top-level backtick string constants in the same file so this operation can be validated, ` +
|
|
113
|
+
`or set "treatUnresolvedInterpolationAsError": false in schema-drift.config.json if this is ` +
|
|
114
|
+
`verified to be validation-irrelevant.`,
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let doc: DocumentNode;
|
|
121
|
+
try {
|
|
122
|
+
doc = parse(op.source);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
errors.push({
|
|
125
|
+
file: op.relFile,
|
|
126
|
+
index: op.index,
|
|
127
|
+
operationName: null,
|
|
128
|
+
messages: [`Parse error: ${(e as Error).message}`],
|
|
129
|
+
});
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const resolvedDoc = resolveFragments(doc, fragmentPool);
|
|
134
|
+
|
|
135
|
+
let bestErrors: string[] | null = null;
|
|
136
|
+
let passed = false;
|
|
137
|
+
for (const group of schemaGroups) {
|
|
138
|
+
const groupErrors = validate(group.schema, resolvedDoc);
|
|
139
|
+
if (groupErrors.length === 0) {
|
|
140
|
+
passed = true;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
// Report the FIRST group's errors as the actionable ones if nothing passes —
|
|
144
|
+
// it's the group most likely to be this document's "home" schema.
|
|
145
|
+
if (bestErrors === null) bestErrors = groupErrors.map((e) => e.message);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!passed) {
|
|
149
|
+
errors.push({
|
|
150
|
+
file: op.relFile,
|
|
151
|
+
index: op.index,
|
|
152
|
+
operationName: getOperationName(doc),
|
|
153
|
+
messages: bestErrors ?? ['No schema group accepted this document.'],
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { errors, documentCount, fileCount: sourceFiles.length };
|
|
159
|
+
}
|