@juspay/svelte-ui-components 4.27.5 → 4.28.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/README.md +140 -36
- package/dist/Animations/ModalAnimation.svelte +15 -1
- package/dist/Animations/OverlayAnimation.svelte +7 -1
- package/dist/AreaChart/AreaChart.svelte +366 -85
- package/dist/AspectRatio/AspectRatio.svelte +51 -0
- package/dist/AspectRatio/AspectRatio.svelte.d.ts +3 -0
- package/dist/AspectRatio/properties.d.ts +27 -0
- package/dist/AspectRatio/properties.js +1 -0
- package/dist/Banner/Banner.svelte +10 -4
- package/dist/Banner/properties.d.ts +8 -0
- package/dist/BarChart/BarChart.svelte +156 -49
- package/dist/Book/Book.svelte +14 -1
- package/dist/BrandLoader/BrandLoader.svelte +76 -12
- package/dist/Button/Button.svelte +56 -11
- package/dist/Calendar/Calendar.svelte +88 -61
- package/dist/Card/Card.svelte +1 -1
- package/dist/Carousel/Carousel.svelte +6 -3
- package/dist/ChatComposer/ChatComposer.svelte +54 -4
- package/dist/ChatComposer/dictationState.d.ts +15 -0
- package/dist/ChatComposer/dictationState.js +9 -0
- package/dist/ChatComposer/properties.d.ts +28 -1
- package/dist/ChatMessage/ChatMessage.svelte +27 -1
- package/dist/CheckListItem/CheckListItem.svelte +3 -3
- package/dist/Checkbox/Checkbox.svelte +84 -3
- package/dist/Checkbox/properties.d.ts +45 -13
- package/dist/ChipInput/ChipInput.svelte +70 -3
- package/dist/ChipInput/properties.d.ts +16 -0
- package/dist/Choicebox/Choicebox.svelte +156 -1
- package/dist/Choicebox/group.d.ts +63 -0
- package/dist/Choicebox/group.js +127 -0
- package/dist/Choicebox/properties.d.ts +46 -0
- package/dist/ColorPicker/ColorPicker.svelte +169 -20
- package/dist/ColorPicker/properties.d.ts +16 -0
- package/dist/Combobox/Combobox.svelte +87 -25
- package/dist/Combobox/properties.d.ts +23 -0
- package/dist/CommandMenu/CommandMenu.svelte +27 -11
- package/dist/ContextMenu/ContextMenu.svelte +31 -37
- package/dist/DateRangePicker/DateRangePicker.svelte +171 -73
- package/dist/DeltaIndicator/DeltaIndicator.svelte +3 -3
- package/dist/Draggable/Draggable.svelte +12 -7
- package/dist/DualAxisBarChart/DualAxisBarChart.svelte +11 -4
- package/dist/EmptyState/EmptyState.svelte +1 -1
- package/dist/FileDropzoneTrigger/FileDropzoneTrigger.svelte +1 -1
- package/dist/FileInput/FileInput.svelte +50 -1
- package/dist/FileInput/properties.d.ts +7 -0
- package/dist/FunnelChart/FunnelChart.svelte +40 -12
- package/dist/FunnelChart/properties.d.ts +7 -1
- package/dist/Gallery/Gallery.svelte +20 -13
- package/dist/Gallery/properties.d.ts +8 -0
- package/dist/GridItem/GridItem.svelte +31 -1
- package/dist/HITL/HITL.svelte +35 -9
- package/dist/Icon/Icon.svelte +19 -3
- package/dist/Input/Input.svelte +24 -10
- package/dist/Input/properties.d.ts +37 -1
- package/dist/InputButton/InputButton.svelte +86 -11
- package/dist/InputButton/properties.d.ts +10 -1
- package/dist/Label/Label.svelte +54 -0
- package/dist/Label/Label.svelte.d.ts +3 -0
- package/dist/Label/properties.d.ts +26 -0
- package/dist/Label/properties.js +1 -0
- package/dist/LineChart/LineChart.svelte +385 -94
- package/dist/ListItem/ListItem.svelte +121 -15
- package/dist/Loader/Loader.svelte +22 -2
- package/dist/LoadingDots/LoadingDots.svelte +21 -0
- package/dist/MediaUpload/MediaUpload.svelte +13 -3
- package/dist/Menu/Menu.svelte +56 -28
- package/dist/Modal/Modal.svelte +77 -42
- package/dist/Pagination/Pagination.svelte +9 -2
- package/dist/Phone/Phone.svelte +4 -0
- package/dist/PieChart/PieChart.svelte +282 -38
- package/dist/PieChart/properties.d.ts +9 -0
- package/dist/Pill/Pill.svelte +14 -5
- package/dist/Progress/Progress.svelte +20 -0
- package/dist/Radio/Radio.svelte +58 -1
- package/dist/Radio/properties.d.ts +23 -0
- package/dist/RatingGroup/RatingGroup.svelte +283 -0
- package/dist/RatingGroup/RatingGroup.svelte.d.ts +4 -0
- package/dist/RatingGroup/properties.d.ts +73 -0
- package/dist/RatingGroup/properties.js +1 -0
- package/dist/RelativeTime/RelativeTime.svelte +1 -1
- package/dist/SankeyChart/SankeyChart.svelte +272 -50
- package/dist/SankeyChart/properties.d.ts +9 -0
- package/dist/Scroller/Scroller.svelte +37 -2
- package/dist/Select/Select.svelte +166 -46
- package/dist/Select/properties.d.ts +7 -0
- package/dist/Separator/Separator.svelte +48 -0
- package/dist/Separator/Separator.svelte.d.ts +3 -0
- package/dist/Separator/properties.d.ts +22 -0
- package/dist/Separator/properties.js +1 -0
- package/dist/Sheet/Sheet.svelte +60 -21
- package/dist/Shimmer/Shimmer.svelte +15 -0
- package/dist/Slider/Slider.svelte +87 -6
- package/dist/Slider/properties.d.ts +26 -0
- package/dist/Snippet/Snippet.svelte +4 -0
- package/dist/Snippet/copyState.svelte.d.ts +13 -1
- package/dist/Snippet/copyState.svelte.js +27 -1
- package/dist/Snippet/properties.d.ts +7 -0
- package/dist/SoundKit/SoundKit.js +9 -8
- package/dist/SplitInput/SplitInput.svelte +68 -2
- package/dist/SplitInput/properties.d.ts +23 -0
- package/dist/StatCard/StatCard.svelte +6 -6
- package/dist/Status/Status.svelte +2 -2
- package/dist/Stepper/Step.svelte +19 -6
- package/dist/Stepper/Stepper.svelte +108 -35
- package/dist/Table/BuiltinCell.svelte +2 -2
- package/dist/Table/Table.svelte +373 -55
- package/dist/Table/properties.d.ts +134 -3
- package/dist/Tabs/Tabs.svelte +263 -65
- package/dist/Tabs/properties.d.ts +23 -0
- package/dist/TaskList/TaskList.svelte +14 -5
- package/dist/ThemeSwitcher/ThemeSwitcher.svelte +23 -0
- package/dist/ThinkingIndicator/ThinkingIndicator.svelte +13 -5
- package/dist/Toast/Toast.svelte +27 -2
- package/dist/Toggle/Toggle.svelte +79 -7
- package/dist/Toggle/Toggle.svelte.d.ts +1 -1
- package/dist/Toggle/properties.d.ts +30 -7
- package/dist/ToolCallLog/ToolCallLog.svelte +59 -21
- package/dist/ToolCallLog/properties.d.ts +10 -0
- package/dist/Toolbar/Toolbar.svelte +1 -1
- package/dist/Tooltip/Tooltip.svelte +20 -2
- package/dist/Tooltip/tooltip-action.js +39 -4
- package/dist/TypewriterText/TypewriterText.svelte +34 -2
- package/dist/TypewriterText/properties.d.ts +18 -5
- package/dist/_chart/ChartContainer.svelte +12 -0
- package/dist/_chart/ChartTooltip.svelte +18 -4
- package/dist/_chart/Legend.svelte +21 -0
- package/dist/_chart/Legend.svelte.d.ts +15 -0
- package/dist/_chart/geometry.d.ts +54 -1
- package/dist/_chart/geometry.js +122 -14
- package/dist/_chart/interactions.d.ts +5 -1
- package/dist/_chart/interactions.js +9 -5
- package/dist/_chart/scales.js +16 -0
- package/dist/_chart/temporal.d.ts +45 -0
- package/dist/_chart/temporal.js +116 -0
- package/dist/_field/description.d.ts +50 -0
- package/dist/_field/description.js +51 -0
- package/dist/_interaction/dismissal.d.ts +67 -0
- package/dist/_interaction/dismissal.js +137 -0
- package/dist/_interaction/focus.d.ts +73 -0
- package/dist/_interaction/focus.js +143 -0
- package/dist/assets/star.svg +5 -0
- package/dist/index.d.ts +13 -1
- package/dist/index.js +21 -1
- package/dist/reduced-motion.svelte.d.ts +37 -0
- package/dist/reduced-motion.svelte.js +44 -0
- package/dist/styles/theme-dark.css +778 -0
- package/dist/types.d.ts +4 -2
- package/dist/utils.d.ts +62 -1
- package/dist/utils.js +80 -21
- package/dist-codemod/{bin.js → codemod/bin.js} +13 -2
- package/dist-codemod/{cli.js → codemod/cli.js} +48 -3
- package/dist-codemod/codemod/migrate-audit.js +395 -0
- package/dist-codemod/codemod/migration-assets/legacy-display.css +120 -0
- package/dist-codemod/codemod/migration-assets/legacy-palette.css +106 -0
- package/dist-codemod/codemod/migration-assets/wc-display.json +492 -0
- package/dist-codemod/migrate/analyze.js +804 -0
- package/dist-codemod/migrate/chart-tooltip-selector.js +301 -0
- package/dist-wc/index.js +13992 -4323
- package/package.json +13 -6
- package/dist/Modal/focus-trap.d.ts +0 -42
- package/dist/Modal/focus-trap.js +0 -57
- /package/dist-codemod/{legacy-pairs.js → codemod/legacy-pairs.js} +0 -0
- /package/dist-codemod/{transform.js → codemod/transform.js} +0 -0
- /package/dist-codemod/{wc-children.js → codemod/wc-children.js} +0 -0
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { parse } from 'svelte/compiler';
|
|
4
|
+
export const LIBRARY = '@juspay/svelte-ui-components';
|
|
5
|
+
/** Peer requirement of the 3.x line, from its published package.json. */
|
|
6
|
+
export const SVELTE_PEER_RANGE = '^5.41.2';
|
|
7
|
+
/**
|
|
8
|
+
* Reads an unknown value as a record of unknown fields.
|
|
9
|
+
*
|
|
10
|
+
* Copies the own enumerable keys rather than asserting the type. Assertions are
|
|
11
|
+
* banned repo-wide and so are type predicates, so there is no way to tell the
|
|
12
|
+
* compiler that a narrowed `object` really is keyed by string. The copy is
|
|
13
|
+
* shallow, so nested nodes keep their identity and traversal is unaffected.
|
|
14
|
+
*
|
|
15
|
+
* `Reflect.get` rather than `value[key]`, because indexing a value typed
|
|
16
|
+
* `object` is itself an error without an assertion — the very thing being
|
|
17
|
+
* avoided. `defineProperty` rather than assignment, because one input here is a
|
|
18
|
+
* consumer's `package.json`: `JSON.parse` turns `__proto__` into an ordinary
|
|
19
|
+
* own key, and assigning it would re-point this copy's prototype instead of
|
|
20
|
+
* adding a field, so a manifest declaring nothing could still answer for
|
|
21
|
+
* `dependencies` through the chain.
|
|
22
|
+
*
|
|
23
|
+
* Only string keys are copied. The Svelte AST and `package.json` have no
|
|
24
|
+
* symbol-keyed fields, and reading one is never the question being asked here.
|
|
25
|
+
*/
|
|
26
|
+
function asRecord(value) {
|
|
27
|
+
if (typeof value !== 'object' || value === null) {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
const record = {};
|
|
31
|
+
for (const key of Object.keys(value)) {
|
|
32
|
+
Object.defineProperty(record, key, {
|
|
33
|
+
value: Reflect.get(value, key),
|
|
34
|
+
enumerable: true,
|
|
35
|
+
writable: true,
|
|
36
|
+
configurable: true
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return record;
|
|
40
|
+
}
|
|
41
|
+
function readDependency(manifest, name) {
|
|
42
|
+
const root = asRecord(manifest);
|
|
43
|
+
for (const field of ['dependencies', 'devDependencies', 'peerDependencies']) {
|
|
44
|
+
const value = asRecord(root[field])[name];
|
|
45
|
+
if (typeof value === 'string') {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Whether a declared range permits any version satisfying the 3.x peer.
|
|
53
|
+
*
|
|
54
|
+
* Comparing major numbers is not enough in either direction: an exact `5.0.0`
|
|
55
|
+
* shares the major but does not satisfy `^5.41.2`, and `^4 || ^5` looks like a
|
|
56
|
+
* 4 to a first-number read while genuinely intersecting. Range intersection is
|
|
57
|
+
* the actual question being asked.
|
|
58
|
+
*/
|
|
59
|
+
/**
|
|
60
|
+
* Injected rather than imported, because this module ships.
|
|
61
|
+
*
|
|
62
|
+
* `semver` is a devDependency, so a `import { intersects } from 'semver'` here
|
|
63
|
+
* loaded fine from a repo checkout and threw ERR_MODULE_NOT_FOUND the moment
|
|
64
|
+
* the compiled module was run from a consumer's node_modules -- where the
|
|
65
|
+
* detectors below are exactly what `sui-codemod migrate` needs and the peer
|
|
66
|
+
* check is not used at all. `npm run check:codemod` could not see it either:
|
|
67
|
+
* type-checking resolves devDependencies happily. Only installing the packed
|
|
68
|
+
* tarball and running the binary surfaced it.
|
|
69
|
+
*
|
|
70
|
+
* `scripts/migrate/cli.ts` passes the real `semver` implementation; the
|
|
71
|
+
* shipped audit never calls the manifest path and so never needs one.
|
|
72
|
+
*/
|
|
73
|
+
let rangesIntersect = () => {
|
|
74
|
+
// Throws rather than defaulting to `true`. A permissive default would make a
|
|
75
|
+
// forgotten injection look like "every peer range is fine", which is the
|
|
76
|
+
// silent-pass shape this tool exists to catch in other people's code. The
|
|
77
|
+
// shipped audit never reaches `analyzeManifest`, so it never trips this.
|
|
78
|
+
throw new Error('analyzeManifest needs a range intersector: call setRangeIntersector() first ' +
|
|
79
|
+
'(scripts/migrate/cli.ts does). The shipped audit does not use this path.');
|
|
80
|
+
};
|
|
81
|
+
export function setRangeIntersector(fn) {
|
|
82
|
+
rangesIntersect = fn;
|
|
83
|
+
}
|
|
84
|
+
function satisfiesPeer(range) {
|
|
85
|
+
try {
|
|
86
|
+
// `loose` changes the answer for exactly one shape worth having: a version
|
|
87
|
+
// written with a leading zero (`^05.41.2`), which strict parsing throws on
|
|
88
|
+
// and would therefore report as a blocker it is not. It does not loosen
|
|
89
|
+
// prerelease handling -- `^5.41.2-alpha` and `>=5.0.0-0` resolve the same
|
|
90
|
+
// either way -- and genuinely unparseable ranges still throw.
|
|
91
|
+
return rangesIntersect(range, SVELTE_PEER_RANGE);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// An unparseable range (a git URL, a workspace protocol) cannot be shown to
|
|
95
|
+
// satisfy the peer, and silently passing it would be the wrong default.
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Checks a consumer's manifest for anything that would prevent taking 3.x.
|
|
101
|
+
*
|
|
102
|
+
* Deliberately reads the manifest rather than node_modules: an installed tree
|
|
103
|
+
* can be stale relative to what the project declares, and reading the wrong one
|
|
104
|
+
* produces a confidently wrong answer about which Svelte a project is on.
|
|
105
|
+
*/
|
|
106
|
+
export function analyzeManifest(manifest) {
|
|
107
|
+
const currentRange = readDependency(manifest, LIBRARY);
|
|
108
|
+
const svelteRange = readDependency(manifest, 'svelte');
|
|
109
|
+
const blockers = [];
|
|
110
|
+
if (currentRange === null) {
|
|
111
|
+
blockers.push(`${LIBRARY} is not a dependency of this project`);
|
|
112
|
+
}
|
|
113
|
+
if (svelteRange === null) {
|
|
114
|
+
blockers.push('svelte is not a dependency; 3.x requires svelte ^5.41.2');
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
if (!satisfiesPeer(svelteRange)) {
|
|
118
|
+
blockers.push(`svelte ${svelteRange} does not satisfy the 3.x peer requirement of ${SVELTE_PEER_RANGE}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { currentRange, svelteRange, blockers };
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Reads the {component, tag, display} of every `sui-*` wrapper straight from
|
|
125
|
+
* `src/wc/components`, rather than hardcoding the 98 -- a hardcoded list drifts
|
|
126
|
+
* the moment a component is added, renamed, or its display default changes,
|
|
127
|
+
* and would then silently under- or over-report `host-display-inline`,
|
|
128
|
+
* `chart-min-width`, and the `<sui-input-button>` spelling below it.
|
|
129
|
+
*
|
|
130
|
+
* `repoRoot` is this library's OWN repo root, not the consumer root the CLI is
|
|
131
|
+
* pointed at -- the wrapper sources this reads exist only here. A root that
|
|
132
|
+
* does not resolve to a checkout with these sources (a published tree with no
|
|
133
|
+
* `src/`, say) yields `[]` rather than a guess: the detectors that depend on
|
|
134
|
+
* this list then simply find nothing, which is what "the source of truth
|
|
135
|
+
* that used to answer this question is not present" ought to mean here.
|
|
136
|
+
*/
|
|
137
|
+
export function readWcComponents(repoRoot) {
|
|
138
|
+
const dir = join(repoRoot, 'src/wc/components');
|
|
139
|
+
let entries;
|
|
140
|
+
try {
|
|
141
|
+
entries = readdirSync(dir).filter((name) => name.endsWith('.wc.svelte'));
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
const components = [];
|
|
147
|
+
for (const file of entries) {
|
|
148
|
+
const component = file.slice(0, -'.wc.svelte'.length);
|
|
149
|
+
const source = readFileSync(join(dir, file), 'utf8');
|
|
150
|
+
// The wrapped component's own name, read off the import that brings it in
|
|
151
|
+
// -- `Card.wc.svelte` always imports `Card` from `$lib/Card/Card.svelte` --
|
|
152
|
+
// rather than assumed equal to the filename, so a wrapper that diverges
|
|
153
|
+
// from that convention is skipped instead of silently mismatched.
|
|
154
|
+
const importPattern = new RegExp(`import\\s+\\w+\\s+from\\s+'\\$lib/${component}/${component}\\.svelte'`);
|
|
155
|
+
const tagMatch = /tag:\s*'([^']+)'/.exec(source);
|
|
156
|
+
const displayMatch = /display:\s*var\(--sui-[\w-]+-display,\s*(block|inline-block)\)/.exec(source);
|
|
157
|
+
if (!importPattern.test(source) || tagMatch === null || displayMatch === null) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
components.push({
|
|
161
|
+
component,
|
|
162
|
+
tag: tagMatch[1],
|
|
163
|
+
display: displayMatch[1] === 'inline-block' ? 'inline-block' : 'block'
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return components;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Default context for callers that only care about the Toolbar/InputButton
|
|
170
|
+
* checks reachable through a named import: those never need the wrapper list,
|
|
171
|
+
* and every existing caller of `analyzeSvelte` predates its existence.
|
|
172
|
+
*/
|
|
173
|
+
const EMPTY_CONTEXT = { wcComponents: [] };
|
|
174
|
+
const EMPTY_NAMES = new Set();
|
|
175
|
+
/** Local names this file binds to each of the library's named imports. */
|
|
176
|
+
function importedNames(source) {
|
|
177
|
+
const byExport = new Map();
|
|
178
|
+
const importPattern = new RegExp(`import\\s*\\{([^}]*)\\}\\s*from\\s*['"]${LIBRARY.replace('/', '\\/')}['"]`, 'g');
|
|
179
|
+
for (const match of source.matchAll(importPattern)) {
|
|
180
|
+
for (const clause of match[1].split(',')) {
|
|
181
|
+
const trimmed = clause.trim();
|
|
182
|
+
if (trimmed.length === 0) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const [imported, local] = trimmed.split(/\s+as\s+/).map((part) => part.trim());
|
|
186
|
+
const set = byExport.get(imported) ?? new Set();
|
|
187
|
+
set.add(local ?? imported);
|
|
188
|
+
byExport.set(imported, set);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return byExport;
|
|
192
|
+
}
|
|
193
|
+
function lineOf(source, offset) {
|
|
194
|
+
return source.slice(0, offset).split('\n').length;
|
|
195
|
+
}
|
|
196
|
+
/** True only for `{false}` — not for a bound expression or the string "false". */
|
|
197
|
+
function isFalseLiteral(value) {
|
|
198
|
+
// Svelte types an attribute value as `true | ExpressionTag | Array<Text |
|
|
199
|
+
// ExpressionTag>`, and quoting a single expression (`="{false}"`) produces
|
|
200
|
+
// the one-element array rather than the bare tag. Reading only the bare shape
|
|
201
|
+
// reports a Toolbar whose control genuinely never renders.
|
|
202
|
+
const single = Array.isArray(value) && value.length === 1 ? value[0] : value;
|
|
203
|
+
const tag = asRecord(single);
|
|
204
|
+
if (tag.type !== 'ExpressionTag') {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
const expression = asRecord(tag.expression);
|
|
208
|
+
return expression.type === 'Literal' && expression.value === false;
|
|
209
|
+
}
|
|
210
|
+
function summarize(node) {
|
|
211
|
+
const attributes = Array.isArray(node.attributes) ? node.attributes : [];
|
|
212
|
+
let explicitlyDisabled = false;
|
|
213
|
+
let hasOwnIcon = false;
|
|
214
|
+
let hasSpread = false;
|
|
215
|
+
for (const raw of attributes) {
|
|
216
|
+
const attribute = asRecord(raw);
|
|
217
|
+
if (attribute.type === 'SpreadAttribute') {
|
|
218
|
+
hasSpread = true;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (attribute.name === 'showBackButton') {
|
|
222
|
+
// Only the Boolean literal `false` disables the control. This used to
|
|
223
|
+
// test `JSON.stringify(value).includes('false')`, which matched unrelated
|
|
224
|
+
// AST properties -- a MemberExpression carries `"computed":false`, so
|
|
225
|
+
// `showBackButton={cfg.showBack}` read as disabled and its affected
|
|
226
|
+
// Toolbar went unreported. A false negative is the worst outcome here.
|
|
227
|
+
explicitlyDisabled = isFalseLiteral(attribute.value);
|
|
228
|
+
}
|
|
229
|
+
if (attribute.name === 'backIcon') {
|
|
230
|
+
hasOwnIcon = true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return { explicitlyDisabled, hasOwnIcon, hasSpread };
|
|
234
|
+
}
|
|
235
|
+
function walk(node, visit) {
|
|
236
|
+
if (Array.isArray(node)) {
|
|
237
|
+
for (const child of node) {
|
|
238
|
+
walk(child, visit);
|
|
239
|
+
}
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const record = asRecord(node);
|
|
243
|
+
if (typeof record.type === 'string') {
|
|
244
|
+
visit(record);
|
|
245
|
+
}
|
|
246
|
+
for (const value of Object.values(record)) {
|
|
247
|
+
if (typeof value === 'object' && value !== null) {
|
|
248
|
+
walk(value, visit);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/** True for a `Text` node whose content is only formatting whitespace -- it collapses in rendered HTML, so it does not itself break an inline run. */
|
|
253
|
+
function isWhitespaceOnlyText(raw) {
|
|
254
|
+
const record = asRecord(raw);
|
|
255
|
+
return (record.type === 'Text' && typeof record.data === 'string' && record.data.trim().length === 0);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Standard HTML tags whose UA-stylesheet default is inline or inline-block --
|
|
259
|
+
* the shapes that genuinely sit *beside* a neighbour rather than stacking
|
|
260
|
+
* with it. Deliberately narrower than `TEXT_FLOW_ELEMENTS`: that set answers
|
|
261
|
+
* "is this tag's own content model text-flow" (so it includes block-level
|
|
262
|
+
* `p`/`li`/`td`/`h1`-`h6`), while this one answers "does this tag occupy a
|
|
263
|
+
* line the way an inline box does" -- a `<div>` sibling never counts, however
|
|
264
|
+
* text-flow-ish its own contents are.
|
|
265
|
+
*/
|
|
266
|
+
const INLINE_HTML_ELEMENTS = new Set([
|
|
267
|
+
'a',
|
|
268
|
+
'abbr',
|
|
269
|
+
'b',
|
|
270
|
+
'bdi',
|
|
271
|
+
'bdo',
|
|
272
|
+
'br',
|
|
273
|
+
'button',
|
|
274
|
+
'cite',
|
|
275
|
+
'code',
|
|
276
|
+
'data',
|
|
277
|
+
'dfn',
|
|
278
|
+
'em',
|
|
279
|
+
'i',
|
|
280
|
+
'img',
|
|
281
|
+
'input',
|
|
282
|
+
'kbd',
|
|
283
|
+
'label',
|
|
284
|
+
'mark',
|
|
285
|
+
'output',
|
|
286
|
+
'q',
|
|
287
|
+
'rp',
|
|
288
|
+
'rt',
|
|
289
|
+
'ruby',
|
|
290
|
+
's',
|
|
291
|
+
'samp',
|
|
292
|
+
'select',
|
|
293
|
+
'small',
|
|
294
|
+
'span',
|
|
295
|
+
'strong',
|
|
296
|
+
'sub',
|
|
297
|
+
'sup',
|
|
298
|
+
'textarea',
|
|
299
|
+
'time',
|
|
300
|
+
'u',
|
|
301
|
+
'var',
|
|
302
|
+
'wbr'
|
|
303
|
+
]);
|
|
304
|
+
/** True for a raw sibling node that itself sits inline: another `sui-*` element, non-whitespace text, or a known inline HTML tag. */
|
|
305
|
+
function isInlineLevelSibling(raw) {
|
|
306
|
+
const record = asRecord(raw);
|
|
307
|
+
if (record.type === 'Text') {
|
|
308
|
+
return typeof record.data === 'string' && record.data.trim().length > 0;
|
|
309
|
+
}
|
|
310
|
+
if (record.type === 'RegularElement' && typeof record.name === 'string') {
|
|
311
|
+
return record.name.startsWith('sui-') || INLINE_HTML_ELEMENTS.has(record.name);
|
|
312
|
+
}
|
|
313
|
+
// Everything else (an ExpressionTag, a Comment, an IfBlock/EachBlock, a
|
|
314
|
+
// Component) is neither skippable whitespace nor a known inline shape, so
|
|
315
|
+
// scanning stops here without a verdict -- a false negative rather than a
|
|
316
|
+
// guess at what that construct renders.
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Walks outward from `siblings[index]` in one direction, skipping only
|
|
321
|
+
* whitespace-only text (it collapses, so it does not separate two elements
|
|
322
|
+
* that are otherwise adjacent), and reports whether the first real neighbour
|
|
323
|
+
* found is itself inline-level.
|
|
324
|
+
*/
|
|
325
|
+
function nearestNeighbourIsInline(siblings, index, step) {
|
|
326
|
+
for (let i = index + step; i >= 0 && i < siblings.length; i += step) {
|
|
327
|
+
if (!isWhitespaceOnlyText(siblings[i])) {
|
|
328
|
+
return isInlineLevelSibling(siblings[i]);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
/** Whether the sibling at `index` has an inline-level neighbour on either side, formatting whitespace aside. */
|
|
334
|
+
function hasAdjacentInlineContent(siblings, index) {
|
|
335
|
+
return (nearestNeighbourIsInline(siblings, index, -1) || nearestNeighbourIsInline(siblings, index, 1));
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Same traversal as `walk`, threading the nearest enclosing `RegularElement`,
|
|
339
|
+
* and (for a node positioned directly in some Fragment's own child list)
|
|
340
|
+
* whether it has an inline-level sibling there. `host-display-inline` and
|
|
341
|
+
* `chart-min-width` both turn on "what does the immediate parent/neighbours
|
|
342
|
+
* look like", which a parent-blind walk cannot answer -- kept separate from
|
|
343
|
+
* `walk` itself rather than adding parameters there, so the existing Toolbar
|
|
344
|
+
* traversal is untouched.
|
|
345
|
+
*
|
|
346
|
+
* Non-element wrapper nodes (`IfBlock`, `EachBlock`, a `Component`) pass the
|
|
347
|
+
* ancestor through unchanged: an `{#if}` or another component between a chart
|
|
348
|
+
* and its styled `<div>` doesn't change which DOM element the chart actually
|
|
349
|
+
* lands inside.
|
|
350
|
+
*
|
|
351
|
+
* `Fragment` nodes (every element's `.fragment`, and the file's own root one)
|
|
352
|
+
* are intercepted rather than walked generically: only there is the ordered,
|
|
353
|
+
* same-processing-pass sibling list available to compute adjacency against.
|
|
354
|
+
* Once inside an `{#if}`/`{#each}`'s own nested fragment, siblings are that
|
|
355
|
+
* construct's, not the surrounding DOM's -- a sui-* element that only sits
|
|
356
|
+
* beside inline content through a conditional branch is a false negative,
|
|
357
|
+
* not a guess.
|
|
358
|
+
*/
|
|
359
|
+
function walkWithParent(node, parent, visit, hasInlineSibling = false) {
|
|
360
|
+
if (Array.isArray(node)) {
|
|
361
|
+
for (const child of node) {
|
|
362
|
+
walkWithParent(child, parent, visit, hasInlineSibling);
|
|
363
|
+
}
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const record = asRecord(node);
|
|
367
|
+
if (typeof record.type !== 'string') {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
visit(record, parent, hasInlineSibling);
|
|
371
|
+
const nextParent = record.type === 'RegularElement' && typeof record.name === 'string'
|
|
372
|
+
? { name: record.name, node: record }
|
|
373
|
+
: parent;
|
|
374
|
+
if (record.type === 'Fragment' && Array.isArray(record.nodes)) {
|
|
375
|
+
// Bound to a local rather than read back off `record.nodes` inside the
|
|
376
|
+
// callback below: narrowing a property access does not survive into a
|
|
377
|
+
// nested closure, and re-reading it there would need an assertion to
|
|
378
|
+
// recover the array type -- banned repo-wide. A local const keeps the
|
|
379
|
+
// narrowing without one.
|
|
380
|
+
const siblings = record.nodes;
|
|
381
|
+
siblings.forEach((child, index) => {
|
|
382
|
+
walkWithParent(child, nextParent, visit, hasAdjacentInlineContent(siblings, index));
|
|
383
|
+
});
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
for (const value of Object.values(record)) {
|
|
387
|
+
if (typeof value === 'object' && value !== null) {
|
|
388
|
+
walkWithParent(value, nextParent, visit, false);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const CHART_TOOLTIP_SLOT_DETAIL = "selector targets '.chart-tooltip-slot'; PieChart/SankeyChart's custom-tooltip " +
|
|
393
|
+
"wrapper is now '.chart-tooltip.unstyled' (plus '.portal' when tooltipPortal is set)";
|
|
394
|
+
/**
|
|
395
|
+
* `.chart-tooltip-slot` findings inside one block of CSS text (a `.css` file's
|
|
396
|
+
* whole contents, or one `<style>` block's contents). `cssOffset` bridges CSS
|
|
397
|
+
* offsets back to `source` offsets for a `<style>` block, where the two differ
|
|
398
|
+
* by the length of everything before the block; it is `0` for a standalone
|
|
399
|
+
* `.css` file, where they are the same text.
|
|
400
|
+
*/
|
|
401
|
+
function chartTooltipSlotFindings(css, file, source, cssOffset) {
|
|
402
|
+
const findings = [];
|
|
403
|
+
for (const match of css.matchAll(/\.chart-tooltip-slot\b/g)) {
|
|
404
|
+
findings.push({
|
|
405
|
+
file,
|
|
406
|
+
line: lineOf(source, cssOffset + match.index),
|
|
407
|
+
reason: 'chart-tooltip-slot-selector',
|
|
408
|
+
detail: CHART_TOOLTIP_SLOT_DETAIL
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
return findings;
|
|
412
|
+
}
|
|
413
|
+
const CHAT_COMPOSER_RECORDING_CSS_DETAIL = "selector targets 'sui-chat-composer[recording]'; recording lost reflect: true (it is now " +
|
|
414
|
+
"{ type: 'String' }), so the attribute is never set by the property and this selector will " +
|
|
415
|
+
'never match — setting el.recording still works, and a presence attribute you set yourself ' +
|
|
416
|
+
'still means true, so read el.recording rather than chasing this as a real regression';
|
|
417
|
+
/**
|
|
418
|
+
* `sui-chat-composer[recording]` findings inside one block of CSS text, same
|
|
419
|
+
* split as `chartTooltipSlotFindings` above and for the same reason: a `.css`
|
|
420
|
+
* file's whole contents, or one `<style>` block's contents, with `cssOffset`
|
|
421
|
+
* bridging back to `source` offsets. The tag is a literal here rather than
|
|
422
|
+
* read off `readWcComponents`: unlike the 98-wrapper detectors, this reason
|
|
423
|
+
* targets exactly one named component with a tag that cannot rename itself
|
|
424
|
+
* out from under a hardcoded string any more than `.chart-tooltip-slot` above
|
|
425
|
+
* can, so there is no drift risk to design around.
|
|
426
|
+
*/
|
|
427
|
+
function chatComposerRecordingSelectorFindings(css, file, source, cssOffset) {
|
|
428
|
+
const findings = [];
|
|
429
|
+
for (const match of css.matchAll(/sui-chat-composer\[\s*recording\b[^\]]*\]/g)) {
|
|
430
|
+
findings.push({
|
|
431
|
+
file,
|
|
432
|
+
line: lineOf(source, cssOffset + match.index),
|
|
433
|
+
reason: 'chat-composer-recording-reflect',
|
|
434
|
+
detail: CHAT_COMPOSER_RECORDING_CSS_DETAIL
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
return findings;
|
|
438
|
+
}
|
|
439
|
+
const CHAT_COMPOSER_RECORDING_JS_DETAIL = "reads 'recording' back with getAttribute/hasAttribute on what looks like a chat-composer — it " +
|
|
440
|
+
'lost reflect: true, so this now returns null/false even while recording is set; read the ' +
|
|
441
|
+
'el.recording PROPERTY instead. Setting the property still works, and a presence attribute you ' +
|
|
442
|
+
'set yourself still means true';
|
|
443
|
+
/**
|
|
444
|
+
* `getAttribute('recording')` / `hasAttribute('recording')`, gated on the
|
|
445
|
+
* file also spelling out the literal tag somewhere (markup, or a
|
|
446
|
+
* `querySelector('sui-chat-composer')` string) -- the same same-file textual
|
|
447
|
+
* co-occurrence bound `legacy-back-selector` already relies on rather than
|
|
448
|
+
* tracing which element a variable actually holds, which this format cannot
|
|
449
|
+
* do without a real type checker. A file that reaches a chat-composer through
|
|
450
|
+
* a differently-named variable with no literal tag anywhere in the same file
|
|
451
|
+
* is a false negative this cannot see; a file that mentions the tag once and
|
|
452
|
+
* reads an unrelated element's `recording` attribute elsewhere is the false
|
|
453
|
+
* positive this bound accepts in exchange.
|
|
454
|
+
*/
|
|
455
|
+
function chatComposerRecordingAttributeFindings(source, file) {
|
|
456
|
+
if (!source.includes('sui-chat-composer')) {
|
|
457
|
+
return [];
|
|
458
|
+
}
|
|
459
|
+
const findings = [];
|
|
460
|
+
for (const match of source.matchAll(/\.(?:getAttribute|hasAttribute)\(\s*(['"])recording\1\s*\)/g)) {
|
|
461
|
+
findings.push({
|
|
462
|
+
file,
|
|
463
|
+
line: lineOf(source, match.index),
|
|
464
|
+
reason: 'chat-composer-recording-reflect',
|
|
465
|
+
detail: CHAT_COMPOSER_RECORDING_JS_DETAIL
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
return findings;
|
|
469
|
+
}
|
|
470
|
+
function styleBlocks(source) {
|
|
471
|
+
const blocks = [];
|
|
472
|
+
for (const match of source.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/g)) {
|
|
473
|
+
// match.index points at the opening <style> tag, while the content starts
|
|
474
|
+
// further in by the tag's own (variable) length -- bridged here or a
|
|
475
|
+
// multiline block reports the tag's line instead of the content's.
|
|
476
|
+
const contentStart = match.index + match[0].indexOf(match[1]);
|
|
477
|
+
blocks.push({ content: match[1], contentStart });
|
|
478
|
+
}
|
|
479
|
+
return blocks;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Checks a `<InputButton>` / `<sui-input-button>` node for the 4.0.0 defect:
|
|
483
|
+
* `mandatory` used to draw only a decorative asterisk, and now also sets
|
|
484
|
+
* native `required`/`aria-required` (`typeof required === 'boolean' ?
|
|
485
|
+
* required : mandatory === true` -- `required` wins whenever it is passed at
|
|
486
|
+
* all, matching its declared `boolean` prop type). A form that relied on
|
|
487
|
+
* submitting this field empty now fails client-side validation.
|
|
488
|
+
*/
|
|
489
|
+
function inputButtonFinding(node, displayName, file, source) {
|
|
490
|
+
const attributes = Array.isArray(node.attributes) ? node.attributes : [];
|
|
491
|
+
let hasRequired = false;
|
|
492
|
+
let mandatoryTruthy = false;
|
|
493
|
+
for (const raw of attributes) {
|
|
494
|
+
const attribute = asRecord(raw);
|
|
495
|
+
if (attribute.type === 'SpreadAttribute') {
|
|
496
|
+
const line = lineOf(source, typeof node.start === 'number' ? node.start : 0);
|
|
497
|
+
return {
|
|
498
|
+
file,
|
|
499
|
+
line,
|
|
500
|
+
reason: 'indeterminate-spread',
|
|
501
|
+
detail: `<${displayName}> spreads props, so mandatory/required cannot be read statically — confirm by hand`
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
if (attribute.name === 'required') {
|
|
505
|
+
hasRequired = true;
|
|
506
|
+
}
|
|
507
|
+
if (attribute.name === 'mandatory') {
|
|
508
|
+
// A literal `mandatory={false}` behaves identically before and after
|
|
509
|
+
// 4.0.0 (no asterisk then, no `required` now), so it is not a defect.
|
|
510
|
+
mandatoryTruthy = !isFalseLiteral(attribute.value);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (hasRequired || !mandatoryTruthy) {
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
const line = lineOf(source, typeof node.start === 'number' ? node.start : 0);
|
|
517
|
+
return {
|
|
518
|
+
file,
|
|
519
|
+
line,
|
|
520
|
+
reason: 'inputbutton-mandatory',
|
|
521
|
+
detail: `<${displayName}> passes mandatory without required — mandatory now also sets native required/aria-required on the underlying Input, so a form that used to submit this field empty will fail client-side validation`
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
const TEXT_FLOW_ELEMENTS = new Set([
|
|
525
|
+
'p',
|
|
526
|
+
'span',
|
|
527
|
+
'li',
|
|
528
|
+
'td',
|
|
529
|
+
'label',
|
|
530
|
+
'h1',
|
|
531
|
+
'h2',
|
|
532
|
+
'h3',
|
|
533
|
+
'h4',
|
|
534
|
+
'h5',
|
|
535
|
+
'h6',
|
|
536
|
+
'a',
|
|
537
|
+
'button'
|
|
538
|
+
]);
|
|
539
|
+
function hostDisplayInlineFinding(node, wc, parent, file, source, trigger) {
|
|
540
|
+
const line = lineOf(source, typeof node.start === 'number' ? node.start : 0);
|
|
541
|
+
// The text-flow-parent wording is unchanged from before this reason grew a
|
|
542
|
+
// second trigger, so an existing report naming this shape reads exactly as
|
|
543
|
+
// it always has.
|
|
544
|
+
const detail = trigger === 'text-flow-parent'
|
|
545
|
+
? `<${wc.tag}> is a direct child of <${parent.name}>, a text-flow element — the wrapper ` +
|
|
546
|
+
`now declares display: ${wc.display} (previously the browser's implicit inline for an ` +
|
|
547
|
+
`unknown element), which can change this line's layout; override with --${wc.tag}-display ` +
|
|
548
|
+
'if inline is still wanted'
|
|
549
|
+
: `<${wc.tag}> sits beside other inline-level content inside <${parent.name}> — the wrapper ` +
|
|
550
|
+
`now declares display: ${wc.display} (previously the browser's implicit inline for an ` +
|
|
551
|
+
'unknown element), which can pull it onto its own line instead of alongside its neighbour; ' +
|
|
552
|
+
`override with --${wc.tag}-display if inline is still wanted`;
|
|
553
|
+
return { file, line, reason: 'host-display-inline', detail };
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* A parent's inline `style` attribute text, when it is expressible as a single
|
|
557
|
+
* plain string literal (`style="..."`) rather than a bound expression or a
|
|
558
|
+
* `style:` directive. Those aren't readable from source alone, so this
|
|
559
|
+
* returns `null` for them -- "unknown", not "absent" -- and callers must
|
|
560
|
+
* treat a `null` as "cannot tell", never as "no style".
|
|
561
|
+
*/
|
|
562
|
+
function inlineStyleText(parent) {
|
|
563
|
+
const attributes = Array.isArray(parent.attributes) ? parent.attributes : [];
|
|
564
|
+
for (const raw of attributes) {
|
|
565
|
+
const attribute = asRecord(raw);
|
|
566
|
+
if (attribute.type !== 'Attribute' || attribute.name !== 'style') {
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
const value = attribute.value;
|
|
570
|
+
if (!Array.isArray(value) || value.length !== 1) {
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
const text = asRecord(value[0]);
|
|
574
|
+
if (text.type !== 'Text' || typeof text.data !== 'string') {
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
return text.data;
|
|
578
|
+
}
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* A parent's inline `width`/`flex-basis`, when it is a plain literal string
|
|
583
|
+
* (`style="width: 120px"`) below the chart's new 160px floor. Anything else --
|
|
584
|
+
* a bound style, a `style:width` directive, a percentage or `rem` value, a
|
|
585
|
+
* class-based width -- is not readable from this file alone and is not
|
|
586
|
+
* reported, rather than guessed.
|
|
587
|
+
*/
|
|
588
|
+
function inlineNarrowWidth(parent) {
|
|
589
|
+
const style = inlineStyleText(parent);
|
|
590
|
+
if (style === null) {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
const match = /(width|flex-basis)\s*:\s*(\d+(?:\.\d+)?)px/.exec(style);
|
|
594
|
+
if (match === null) {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
const px = Number(match[2]);
|
|
598
|
+
return px < 160 ? { property: match[1], px } : null;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* True when the parent's inline style makes it a flex or grid container. A
|
|
602
|
+
* flex/grid item's OWN `display` is overridden by the layout mode regardless
|
|
603
|
+
* of what it declares (the used value becomes `block` either way per the
|
|
604
|
+
* flex/grid box spec), so a `sui-*` child's new block default changes
|
|
605
|
+
* nothing about whether it shares a line with its siblings there -- the
|
|
606
|
+
* container decides that, not the child. Anything not readable as a plain
|
|
607
|
+
* literal (a class, a bound style, a `style:display` directive) is
|
|
608
|
+
* "unknown", not "false", and this fails open the same way `inlineNarrowWidth`
|
|
609
|
+
* does for a width it cannot read: it returns false, so the sibling check
|
|
610
|
+
* below still runs rather than silently skipping a real positive.
|
|
611
|
+
*/
|
|
612
|
+
function parentIsFlexOrGridContainer(parent) {
|
|
613
|
+
const style = inlineStyleText(parent);
|
|
614
|
+
return style !== null && /display\s*:\s*(inline-)?(flex|grid)\b/.test(style);
|
|
615
|
+
}
|
|
616
|
+
function chartMinWidthFinding(node, componentName, parent, file, source) {
|
|
617
|
+
if (parent === null) {
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
const narrow = inlineNarrowWidth(parent.node);
|
|
621
|
+
if (narrow === null) {
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
const line = lineOf(source, typeof node.start === 'number' ? node.start : 0);
|
|
625
|
+
return {
|
|
626
|
+
file,
|
|
627
|
+
line,
|
|
628
|
+
reason: 'chart-min-width',
|
|
629
|
+
detail: `<${componentName}> renders inside a <${parent.name}> with an inline ${narrow.property} ` +
|
|
630
|
+
`of ${narrow.px}px — the chart's new 160px min-width (--chart-min-width) will overflow it ` +
|
|
631
|
+
'instead of shrinking to fit'
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Finds everything in one `.svelte` file that a 3.x-or-later change affects,
|
|
636
|
+
* across every reason this module knows about.
|
|
637
|
+
*
|
|
638
|
+
* `context.wcComponents` (from `readWcComponents`, over THIS library's own
|
|
639
|
+
* `src/wc/components`) is what lets `host-display-inline`, `chart-min-width`'s
|
|
640
|
+
* `<sui-*-chart>` form, and the `<sui-input-button>` spelling of
|
|
641
|
+
* `inputbutton-mandatory` recognise a raw custom-element tag; without it
|
|
642
|
+
* (the default) only the named-import forms — `<Toolbar>`, `<InputButton>`,
|
|
643
|
+
* `<PieChart>` and friends — are reachable, since only a resolved import name
|
|
644
|
+
* is available for free.
|
|
645
|
+
*/
|
|
646
|
+
export function analyzeSvelte(source, file, context = EMPTY_CONTEXT) {
|
|
647
|
+
const findings = [];
|
|
648
|
+
// Textual and independent of every other check: a stylesheet can select
|
|
649
|
+
// `.chart-tooltip-slot` without this file rendering PieChart/SankeyChart
|
|
650
|
+
// itself (a shared global stylesheet, say), so this must not be gated behind
|
|
651
|
+
// a chart import the way legacy-back-selector below is gated behind Toolbar.
|
|
652
|
+
const blocks = styleBlocks(source);
|
|
653
|
+
for (const block of blocks) {
|
|
654
|
+
findings.push(...chartTooltipSlotFindings(block.content, file, source, block.contentStart));
|
|
655
|
+
findings.push(...chatComposerRecordingSelectorFindings(block.content, file, source, block.contentStart));
|
|
656
|
+
}
|
|
657
|
+
// Same independence, for the JS half of the same defect: a script can read
|
|
658
|
+
// `getAttribute('recording')` off a chat-composer without this file ever
|
|
659
|
+
// importing ChatComposer as a named export (a raw custom element, or an
|
|
660
|
+
// element obtained via `document.querySelector`), so this is not gated
|
|
661
|
+
// behind `context.wcComponents` the way the AST-based checks below are.
|
|
662
|
+
findings.push(...chatComposerRecordingAttributeFindings(source, file));
|
|
663
|
+
const byExport = importedNames(source);
|
|
664
|
+
const toolbarNames = byExport.get('Toolbar') ?? EMPTY_NAMES;
|
|
665
|
+
const inputButtonNames = byExport.get('InputButton') ?? EMPTY_NAMES;
|
|
666
|
+
const chartComponents = context.wcComponents.filter((wc) => wc.component.endsWith('Chart'));
|
|
667
|
+
const chartLocalNames = new Map();
|
|
668
|
+
for (const chart of chartComponents) {
|
|
669
|
+
for (const local of byExport.get(chart.component) ?? EMPTY_NAMES) {
|
|
670
|
+
chartLocalNames.set(local, chart.component);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
const chartTagToComponent = new Map(chartComponents.map((wc) => [wc.tag, wc.component]));
|
|
674
|
+
const hostTagMap = new Map(context.wcComponents.map((wc) => [wc.tag, wc]));
|
|
675
|
+
const inputButtonTag = context.wcComponents.find((wc) => wc.component === 'InputButton')?.tag ?? null;
|
|
676
|
+
const mightUseCustomElement = hostTagMap.size > 0 && source.includes('sui-');
|
|
677
|
+
const needsAst = toolbarNames.size > 0 ||
|
|
678
|
+
inputButtonNames.size > 0 ||
|
|
679
|
+
chartLocalNames.size > 0 ||
|
|
680
|
+
mightUseCustomElement;
|
|
681
|
+
if (!needsAst) {
|
|
682
|
+
return findings;
|
|
683
|
+
}
|
|
684
|
+
let ast;
|
|
685
|
+
try {
|
|
686
|
+
ast = parse(source, { modern: true });
|
|
687
|
+
}
|
|
688
|
+
catch {
|
|
689
|
+
findings.push({
|
|
690
|
+
file,
|
|
691
|
+
line: 1,
|
|
692
|
+
reason: 'indeterminate-spread',
|
|
693
|
+
detail: 'file could not be parsed; review this usage by hand'
|
|
694
|
+
});
|
|
695
|
+
return findings;
|
|
696
|
+
}
|
|
697
|
+
// Toolbar: unchanged traversal, kept on the original parent-blind `walk` --
|
|
698
|
+
// this check never needed to know its parent, and reusing the newer,
|
|
699
|
+
// ancestor-tracking walker here would be a change with nothing to show for it.
|
|
700
|
+
if (toolbarNames.size > 0) {
|
|
701
|
+
walk(ast, (node) => {
|
|
702
|
+
if (node.type !== 'Component' ||
|
|
703
|
+
typeof node.name !== 'string' ||
|
|
704
|
+
!toolbarNames.has(node.name)) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const { explicitlyDisabled, hasOwnIcon, hasSpread } = summarize(node);
|
|
708
|
+
const line = lineOf(source, typeof node.start === 'number' ? node.start : 0);
|
|
709
|
+
if (explicitlyDisabled || hasOwnIcon) {
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (hasSpread) {
|
|
713
|
+
findings.push({
|
|
714
|
+
file,
|
|
715
|
+
line,
|
|
716
|
+
reason: 'indeterminate-spread',
|
|
717
|
+
detail: `<${node.name}> spreads props, so showBackButton/backIcon cannot be read statically — confirm by hand`
|
|
718
|
+
});
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
findings.push({
|
|
722
|
+
file,
|
|
723
|
+
line,
|
|
724
|
+
reason: 'default-back-control',
|
|
725
|
+
detail: `<${node.name}> renders the default back control, whose markup changed from <div><img></div> to <button><svg></button>`
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
// Styles are checked textually: a selector reaching into the back control's
|
|
729
|
+
// markup breaks regardless of which Toolbar instance it was written for.
|
|
730
|
+
for (const block of blocks) {
|
|
731
|
+
const legacy = /\.back[^{}]*\bimg\b/.exec(block.content);
|
|
732
|
+
if (legacy !== null) {
|
|
733
|
+
findings.push({
|
|
734
|
+
file,
|
|
735
|
+
line: lineOf(source, block.contentStart + legacy.index),
|
|
736
|
+
reason: 'legacy-back-selector',
|
|
737
|
+
detail: 'selector targets an <img> inside the back control; the default control now renders an inline <svg>'
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
walkWithParent(ast, null, (node, parent, hasInlineSibling) => {
|
|
743
|
+
if (node.type === 'Component' && typeof node.name === 'string') {
|
|
744
|
+
if (inputButtonNames.has(node.name)) {
|
|
745
|
+
const finding = inputButtonFinding(node, node.name, file, source);
|
|
746
|
+
if (finding !== null) {
|
|
747
|
+
findings.push(finding);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
const chartComponent = chartLocalNames.get(node.name);
|
|
751
|
+
if (typeof chartComponent !== 'undefined') {
|
|
752
|
+
const finding = chartMinWidthFinding(node, node.name, parent, file, source);
|
|
753
|
+
if (finding !== null) {
|
|
754
|
+
findings.push(finding);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if (node.type === 'RegularElement' && typeof node.name === 'string') {
|
|
760
|
+
if (node.name === inputButtonTag) {
|
|
761
|
+
const finding = inputButtonFinding(node, node.name, file, source);
|
|
762
|
+
if (finding !== null) {
|
|
763
|
+
findings.push(finding);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
const chartTagComponent = chartTagToComponent.get(node.name);
|
|
767
|
+
if (typeof chartTagComponent !== 'undefined') {
|
|
768
|
+
const finding = chartMinWidthFinding(node, node.name, parent, file, source);
|
|
769
|
+
if (finding !== null) {
|
|
770
|
+
findings.push(finding);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
const hostDefault = hostTagMap.get(node.name);
|
|
774
|
+
if (typeof hostDefault !== 'undefined' && parent !== null) {
|
|
775
|
+
// Two independent triggers, checked in this order so the more
|
|
776
|
+
// specific (and pre-existing) text-flow-parent wording wins when a
|
|
777
|
+
// usage happens to satisfy both, e.g. `<p>Status: <sui-badge/></p>`.
|
|
778
|
+
// The flex/grid guard applies only to the newer sibling trigger --
|
|
779
|
+
// widening it to also suppress the text-flow-parent path would be a
|
|
780
|
+
// behaviour change to a rule this pass was told to leave alone.
|
|
781
|
+
if (TEXT_FLOW_ELEMENTS.has(parent.name)) {
|
|
782
|
+
findings.push(hostDisplayInlineFinding(node, hostDefault, parent, file, source, 'text-flow-parent'));
|
|
783
|
+
}
|
|
784
|
+
else if (hasInlineSibling && !parentIsFlexOrGridContainer(parent.node)) {
|
|
785
|
+
findings.push(hostDisplayInlineFinding(node, hostDefault, parent, file, source, 'inline-sibling'));
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
return findings;
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* `chart-tooltip-slot-selector` and `chat-composer-recording-reflect`'s CSS
|
|
794
|
+
* shape in a standalone `.css` file. Split out from `analyzeSvelte` because a
|
|
795
|
+
* `.css` file has no `<style>` tags to locate first — its whole content
|
|
796
|
+
* already is the CSS. Does not also run `chatComposerRecordingAttributeFindings`:
|
|
797
|
+
* that one is JS-shaped, and a `.css` file has none.
|
|
798
|
+
*/
|
|
799
|
+
export function analyzeStylesheet(source, file) {
|
|
800
|
+
return [
|
|
801
|
+
...chartTooltipSlotFindings(source, file, source, 0),
|
|
802
|
+
...chatComposerRecordingSelectorFindings(source, file, source, 0)
|
|
803
|
+
];
|
|
804
|
+
}
|