tangled
alpha
login
or
join now
mary.my.id
/
teardown
13
fork
atom
Find the cost of adding an npm package to your app's bundle size
teardown.kelinci.dev
13
fork
atom
overview
issues
pulls
pipelines
refactor: enable noUncheckedIndexedAccess
mary.my.id
1 month ago
519f26a7
f8888bd8
verified
This commit was signed with the committer's
known signature
.
mary.my.id
SSH Key Fingerprint:
SHA256:ZlTP/auFSGpGnaoDg4mCTG1g9OZvXp62jWR4c6H4O3c=
+71
-68
15 changed files
expand all
collapse all
unified
split
src
components
package-dependencies.tsx
package-search-input.tsx
lib
emitter.ts
package-name.ts
use-search-params.ts
npm
lib
fetch.ts
installed-packages.test.ts
module-type.ts
resolve.test.ts
resolve.ts
subpaths.ts
worker-entry.ts
primitives
lib
create-active-descendant.ts
create-roving-tabindex.ts
tsconfig.app.json
+12
-12
src/components/package-dependencies.tsx
···
106
return [];
107
}
108
if (numSegments === 1) {
109
-
return [canonicalIndices[0]];
110
}
111
112
// fall back to greedy for very large inputs
113
if (numSegments > COLOR_RESOLUTION_DP_LIMIT) {
114
-
const resolved: number[] = [canonicalIndices[0]];
115
for (let i = 1; i < numSegments; i++) {
116
-
const canonical = canonicalIndices[i];
117
resolved.push(canonical === resolved[i - 1] ? (canonical + 1) % numColors : canonical);
118
}
119
return resolved;
···
128
129
// base case: position 0
130
for (let c = 0; c < numColors; c++) {
131
-
prev[c] = c === canonicalIndices[0] ? 0 : 1;
132
}
133
134
// fill DP table
135
for (let i = 1; i < numSegments; i++) {
136
-
const canonical = canonicalIndices[i];
137
const parentRow = Array.from({ length: numColors }, () => 0);
138
139
for (let c = 0; c < numColors; c++) {
···
143
let bestPrevCost = Infinity;
144
let bestPrevColor = 0;
145
for (let cp = 0; cp < numColors; cp++) {
146
-
if (cp !== c && prev[cp] < bestPrevCost) {
147
-
bestPrevCost = prev[cp];
148
bestPrevColor = cp;
149
}
150
}
151
152
-
curr[c] = cost + bestPrevCost;
153
-
parentRow[c] = bestPrevColor;
154
}
155
156
parent.push(parentRow);
···
160
// find best final color
161
let bestFinal = 0;
162
for (let c = 1; c < numColors; c++) {
163
-
if (prev[c] < prev[bestFinal]) {
164
bestFinal = c;
165
}
166
}
···
169
let color = bestFinal;
170
const reversed = [color];
171
for (let i = parent.length - 1; i >= 0; i--) {
172
-
color = parent[i][color];
173
reversed.push(color);
174
}
175
return reversed.reverse();
···
192
return sorted.map((pkg, i) => ({
193
pkg,
194
percent: (pkg.size / props.installSize) * 100,
195
-
color: SEGMENT_COLORS[resolvedIndices[i]],
196
}));
197
});
198
···
106
return [];
107
}
108
if (numSegments === 1) {
109
+
return [canonicalIndices[0]!];
110
}
111
112
// fall back to greedy for very large inputs
113
if (numSegments > COLOR_RESOLUTION_DP_LIMIT) {
114
+
const resolved: number[] = [canonicalIndices[0]!];
115
for (let i = 1; i < numSegments; i++) {
116
+
const canonical = canonicalIndices[i]!;
117
resolved.push(canonical === resolved[i - 1] ? (canonical + 1) % numColors : canonical);
118
}
119
return resolved;
···
128
129
// base case: position 0
130
for (let c = 0; c < numColors; c++) {
131
+
prev[c] = c === canonicalIndices[0]! ? 0 : 1;
132
}
133
134
// fill DP table
135
for (let i = 1; i < numSegments; i++) {
136
+
const canonical = canonicalIndices[i]!;
137
const parentRow = Array.from({ length: numColors }, () => 0);
138
139
for (let c = 0; c < numColors; c++) {
···
143
let bestPrevCost = Infinity;
144
let bestPrevColor = 0;
145
for (let cp = 0; cp < numColors; cp++) {
146
+
if (cp !== c && prev[cp]! < bestPrevCost) {
147
+
bestPrevCost = prev[cp]!;
148
bestPrevColor = cp;
149
}
150
}
151
152
+
curr[c]! = cost + bestPrevCost;
153
+
parentRow[c]! = bestPrevColor;
154
}
155
156
parent.push(parentRow);
···
160
// find best final color
161
let bestFinal = 0;
162
for (let c = 1; c < numColors; c++) {
163
+
if (prev[c]! < prev[bestFinal]!) {
164
bestFinal = c;
165
}
166
}
···
169
let color = bestFinal;
170
const reversed = [color];
171
for (let i = parent.length - 1; i >= 0; i--) {
172
+
color = parent[i]![color]!;
173
reversed.push(color);
174
}
175
return reversed.reverse();
···
192
return sorted.map((pkg, i) => ({
193
pkg,
194
percent: (pkg.size / props.installSize) * 100,
195
+
color: SEGMENT_COLORS[resolvedIndices[i]!]!,
196
}));
197
});
198
+1
-1
src/components/package-search-input.tsx
···
184
ev.preventDefault();
185
const idx = activeIndex();
186
if (idx >= 0 && idx < items.length) {
187
-
handleSelect(items[idx]);
188
} else {
189
const parsed = parsePackageSpecifier(props.value.trim());
190
if (parsed) {
···
184
ev.preventDefault();
185
const idx = activeIndex();
186
if (idx >= 0 && idx < items.length) {
187
+
handleSelect(items[idx]!);
188
} else {
189
const parsed = parsePackageSpecifier(props.value.trim());
190
if (parsed) {
+1
-1
src/lib/emitter.ts
···
87
listener.apply(this, args);
88
} else {
89
for (let idx = 0, len = listener.length; idx < len; idx++) {
90
-
listener[idx].apply(this, args);
91
}
92
}
93
},
···
87
listener.apply(this, args);
88
} else {
89
for (let idx = 0, len = listener.length; idx < len; idx++) {
90
+
listener[idx]!.apply(this, args);
91
}
92
}
93
},
+1
-1
src/lib/package-name.ts
···
25
}
26
return {
27
registry: (match[1] as Registry) ?? 'npm',
28
-
name: match[2],
29
range: match[3] ?? 'latest',
30
};
31
};
···
25
}
26
return {
27
registry: (match[1] as Registry) ?? 'npm',
28
+
name: match[2]!,
29
range: match[3] ?? 'latest',
30
};
31
};
+3
-3
src/lib/use-search-params.ts
···
45
const result: Record<string, unknown> = {};
46
47
for (const key in definition) {
48
-
const schema = definition[key];
49
50
let raw: string | string[] | undefined;
51
if (schema.type === 'array') {
···
58
if (raw === undefined) {
59
result[key] = undefined;
60
} else {
61
-
const parsed = v.safeParse(schema, raw);
62
result[key] = parsed.success ? parsed.output : undefined;
63
}
64
}
···
91
continue;
92
}
93
94
-
const parsed = v.safeParse(definition[key], value);
95
if (!parsed.success) {
96
result[key] = undefined;
97
continue;
···
45
const result: Record<string, unknown> = {};
46
47
for (const key in definition) {
48
+
const schema = definition[key]!;
49
50
let raw: string | string[] | undefined;
51
if (schema.type === 'array') {
···
58
if (raw === undefined) {
59
result[key] = undefined;
60
} else {
61
+
const parsed = v.safeParse(schema!, raw);
62
result[key] = parsed.success ? parsed.output : undefined;
63
}
64
}
···
91
continue;
92
}
93
94
+
const parsed = v.safeParse(definition[key]!, value);
95
if (!parsed.success) {
96
result[key] = undefined;
97
continue;
+1
-1
src/npm/lib/fetch.ts
···
182
break;
183
}
184
185
-
const { node, basePath } = queue[i];
186
const packagePath = `${basePath}/${node.name}`;
187
188
const extractedSize = await fetchTarballToVolume(node.tarball, packagePath, volume, exclude);
···
182
break;
183
}
184
185
+
const { node, basePath } = queue[i]!;
186
const packagePath = `${basePath}/${node.name}`;
187
188
const extractedSize = await fetchTarballToVolume(node.tarball, packagePath, volume, exclude);
+9
-9
src/npm/lib/installed-packages.test.ts
···
6
describe('buildInstalledPackages', () => {
7
it('builds packages from a simple dependency tree', async () => {
8
const result = await resolve(['is-odd@3.0.1']);
9
-
const packages = buildInstalledPackages(result.roots[0], new Set());
10
11
// should have is-odd and is-number
12
const names = packages.map((p) => p.name);
···
25
26
it('correctly sets dependents', async () => {
27
const result = await resolve(['is-odd@3.0.1']);
28
-
const packages = buildInstalledPackages(result.roots[0], new Set());
29
30
// is-odd is the root, no dependents
31
const isOdd = packages.find((p) => p.name === 'is-odd')!;
···
34
// is-number is depended on by is-odd
35
const isNumber = packages.find((p) => p.name === 'is-number')!;
36
expect(isNumber.dependents.length).toBe(1);
37
-
expect(isNumber.dependents[0].name).toBe('is-odd');
38
});
39
40
it('correctly sets dependencies', async () => {
41
const result = await resolve(['is-odd@3.0.1']);
42
-
const packages = buildInstalledPackages(result.roots[0], new Set());
43
44
// is-odd has 1 dependency (is-number)
45
const isOdd = packages.find((p) => p.name === 'is-odd')!;
46
expect(isOdd.dependencies.length).toBe(1);
47
-
expect(isOdd.dependencies[0].name).toBe('is-number');
48
});
49
50
it('marks peer dependencies correctly', async () => {
51
// use-sync-external-store has react as a peer dependency
52
const result = await resolve(['use-sync-external-store@1.2.0']);
53
const peerDepNames = new Set(['react']);
54
-
const packages = buildInstalledPackages(result.roots[0], peerDepNames);
55
56
// react and its deps should be marked as peer
57
const react = packages.find((p) => p.name === 'react');
···
74
// loose-envify should be marked as peer (only reachable through react)
75
const result = await resolve(['use-sync-external-store@1.2.0']);
76
const peerDepNames = new Set(['react']);
77
-
const packages = buildInstalledPackages(result.roots[0], peerDepNames);
78
79
const looseEnvify = packages.find((p) => p.name === 'loose-envify');
80
// loose-envify is a dep of react, which is peer-only
···
90
91
// pretend is-number is also a peer dep (but it's already a regular dep)
92
const peerDepNames = new Set(['is-number']);
93
-
const packages = buildInstalledPackages(result.roots[0], peerDepNames);
94
95
// is-number should be marked as peer because it's a direct peer dep of root
96
const isNumber = packages.find((p) => p.name === 'is-number')!;
···
103
// graphql should still be marked as peer since it's a direct peer dep of root
104
const result = await resolve(['graphql-request@7.4.0']);
105
const peerDepNames = new Set(['graphql']);
106
-
const packages = buildInstalledPackages(result.roots[0], peerDepNames);
107
108
// graphql should be marked as peer
109
const graphql = packages.find((p) => p.name === 'graphql');
···
6
describe('buildInstalledPackages', () => {
7
it('builds packages from a simple dependency tree', async () => {
8
const result = await resolve(['is-odd@3.0.1']);
9
+
const packages = buildInstalledPackages(result.roots[0]!, new Set());
10
11
// should have is-odd and is-number
12
const names = packages.map((p) => p.name);
···
25
26
it('correctly sets dependents', async () => {
27
const result = await resolve(['is-odd@3.0.1']);
28
+
const packages = buildInstalledPackages(result.roots[0]!, new Set());
29
30
// is-odd is the root, no dependents
31
const isOdd = packages.find((p) => p.name === 'is-odd')!;
···
34
// is-number is depended on by is-odd
35
const isNumber = packages.find((p) => p.name === 'is-number')!;
36
expect(isNumber.dependents.length).toBe(1);
37
+
expect(isNumber.dependents[0]!.name).toBe('is-odd');
38
});
39
40
it('correctly sets dependencies', async () => {
41
const result = await resolve(['is-odd@3.0.1']);
42
+
const packages = buildInstalledPackages(result.roots[0]!, new Set());
43
44
// is-odd has 1 dependency (is-number)
45
const isOdd = packages.find((p) => p.name === 'is-odd')!;
46
expect(isOdd.dependencies.length).toBe(1);
47
+
expect(isOdd.dependencies[0]!.name).toBe('is-number');
48
});
49
50
it('marks peer dependencies correctly', async () => {
51
// use-sync-external-store has react as a peer dependency
52
const result = await resolve(['use-sync-external-store@1.2.0']);
53
const peerDepNames = new Set(['react']);
54
+
const packages = buildInstalledPackages(result.roots[0]!, peerDepNames);
55
56
// react and its deps should be marked as peer
57
const react = packages.find((p) => p.name === 'react');
···
74
// loose-envify should be marked as peer (only reachable through react)
75
const result = await resolve(['use-sync-external-store@1.2.0']);
76
const peerDepNames = new Set(['react']);
77
+
const packages = buildInstalledPackages(result.roots[0]!, peerDepNames);
78
79
const looseEnvify = packages.find((p) => p.name === 'loose-envify');
80
// loose-envify is a dep of react, which is peer-only
···
90
91
// pretend is-number is also a peer dep (but it's already a regular dep)
92
const peerDepNames = new Set(['is-number']);
93
+
const packages = buildInstalledPackages(result.roots[0]!, peerDepNames);
94
95
// is-number should be marked as peer because it's a direct peer dep of root
96
const isNumber = packages.find((p) => p.name === 'is-number')!;
···
103
// graphql should still be marked as peer since it's a direct peer dep of root
104
const result = await resolve(['graphql-request@7.4.0']);
105
const peerDepNames = new Set(['graphql']);
106
+
const packages = buildInstalledPackages(result.roots[0]!, peerDepNames);
107
108
// graphql should be marked as peer
109
const graphql = packages.find((p) => p.name === 'graphql');
+2
-2
src/npm/lib/module-type.ts
···
174
if (prop.type === 'Identifier' && prop.name === 'defineProperty') {
175
const args = expr.arguments;
176
if (args.length >= 2) {
177
-
const target = args[0];
178
-
const propArg = args[1];
179
180
if (
181
target.type !== 'SpreadElement' &&
···
174
if (prop.type === 'Identifier' && prop.name === 'defineProperty') {
175
const args = expr.arguments;
176
if (args.length >= 2) {
177
+
const target = args[0]!;
178
+
const propArg = args[1]!;
179
180
if (
181
target.type !== 'SpreadElement' &&
+14
-14
src/npm/lib/resolve.test.ts
···
415
const result = await resolve(['is-odd@3.0.1']);
416
417
expect(result.roots).toHaveLength(1);
418
-
expect(result.roots[0].name).toBe('is-odd');
419
-
expect(result.roots[0].version).toBe('3.0.1');
420
-
expect(result.roots[0].dependencies.has('is-number')).toBe(true);
421
});
422
423
it('resolves multiple packages', async () => {
424
const result = await resolve(['is-odd@3.0.1', 'is-even@1.0.0']);
425
426
expect(result.roots).toHaveLength(2);
427
-
expect(result.roots[0].name).toBe('is-odd');
428
-
expect(result.roots[1].name).toBe('is-even');
429
});
430
431
it('deduplicates shared dependencies', async () => {
···
446
const result = await resolve(['jsr:@luca/flag@1.0.1']);
447
448
expect(result.roots).toHaveLength(1);
449
-
expect(result.roots[0].name).toBe('@luca/flag');
450
-
expect(result.roots[0].version).toBe('1.0.1');
451
-
expect(result.roots[0].tarball).toContain('npm.jsr.io');
452
});
453
454
it('resolves JSR package with JSR dependencies', async () => {
455
const result = await resolve(['jsr:@std/path@1.1.4']);
456
457
-
expect(result.roots[0].name).toBe('@std/path');
458
// dependency stored under npm-compatible name, resolved to canonical
459
-
expect(result.roots[0].dependencies.has('@jsr/std__internal')).toBe(true);
460
-
const internal = result.roots[0].dependencies.get('@jsr/std__internal')!;
461
expect(internal.name).toBe('@std/internal');
462
expect(internal.tarball).toContain('npm.jsr.io');
463
});
···
467
it('auto-installs required peer dependencies', async () => {
468
const result = await resolve(['use-sync-external-store@1.2.0']);
469
470
-
const mainPkg = result.roots[0];
471
expect(mainPkg.dependencies.has('react')).toBe(true);
472
expect(Array.from(result.packages.values()).some((p) => p.name === 'react')).toBe(true);
473
});
···
476
const result = await resolve(['use-sync-external-store@1.2.0']);
477
478
// react is required, should be present
479
-
const mainPkg = result.roots[0];
480
expect(mainPkg.dependencies.has('react')).toBe(true);
481
});
482
···
484
const result = await resolve(['use-sync-external-store@1.2.0'], { installPeers: false });
485
486
expect(result.roots).toHaveLength(1);
487
-
expect(result.roots[0].name).toBe('use-sync-external-store');
488
});
489
});
490
});
···
415
const result = await resolve(['is-odd@3.0.1']);
416
417
expect(result.roots).toHaveLength(1);
418
+
expect(result.roots[0]!.name).toBe('is-odd');
419
+
expect(result.roots[0]!.version).toBe('3.0.1');
420
+
expect(result.roots[0]!.dependencies.has('is-number')).toBe(true);
421
});
422
423
it('resolves multiple packages', async () => {
424
const result = await resolve(['is-odd@3.0.1', 'is-even@1.0.0']);
425
426
expect(result.roots).toHaveLength(2);
427
+
expect(result.roots[0]!.name).toBe('is-odd');
428
+
expect(result.roots[1]!.name).toBe('is-even');
429
});
430
431
it('deduplicates shared dependencies', async () => {
···
446
const result = await resolve(['jsr:@luca/flag@1.0.1']);
447
448
expect(result.roots).toHaveLength(1);
449
+
expect(result.roots[0]!.name).toBe('@luca/flag');
450
+
expect(result.roots[0]!.version).toBe('1.0.1');
451
+
expect(result.roots[0]!.tarball).toContain('npm.jsr.io');
452
});
453
454
it('resolves JSR package with JSR dependencies', async () => {
455
const result = await resolve(['jsr:@std/path@1.1.4']);
456
457
+
expect(result.roots[0]!.name).toBe('@std/path');
458
// dependency stored under npm-compatible name, resolved to canonical
459
+
expect(result.roots[0]!.dependencies.has('@jsr/std__internal')).toBe(true);
460
+
const internal = result.roots[0]!.dependencies.get('@jsr/std__internal')!;
461
expect(internal.name).toBe('@std/internal');
462
expect(internal.tarball).toContain('npm.jsr.io');
463
});
···
467
it('auto-installs required peer dependencies', async () => {
468
const result = await resolve(['use-sync-external-store@1.2.0']);
469
470
+
const mainPkg = result.roots[0]!;
471
expect(mainPkg.dependencies.has('react')).toBe(true);
472
expect(Array.from(result.packages.values()).some((p) => p.name === 'react')).toBe(true);
473
});
···
476
const result = await resolve(['use-sync-external-store@1.2.0']);
477
478
// react is required, should be present
479
+
const mainPkg = result.roots[0]!;
480
expect(mainPkg.dependencies.has('react')).toBe(true);
481
});
482
···
484
const result = await resolve(['use-sync-external-store@1.2.0'], { installPeers: false });
485
486
expect(result.roots).toHaveLength(1);
487
+
expect(result.roots[0]!.name).toBe('use-sync-external-store');
488
});
489
});
490
});
+5
-5
src/npm/lib/resolve.ts
···
85
): AbbreviatedManifest | null {
86
// empty range means latest
87
if (range === '') {
88
-
return versions[distTags.latest] ?? null;
89
}
90
91
// check if range is a dist-tag
92
if (range in distTags) {
93
-
const taggedVersion = distTags[range];
94
return versions[taggedVersion] ?? null;
95
}
96
···
131
}
132
133
// prefer non-deprecated versions (pnpm behavior)
134
-
const nonDeprecated = validVersions.find((v) => !versions[v].deprecated);
135
if (nonDeprecated !== undefined) {
136
-
return versions[nonDeprecated];
137
}
138
139
// fall back to deprecated if no alternatives
140
-
return versions[validVersions[0]];
141
}
142
143
/**
···
85
): AbbreviatedManifest | null {
86
// empty range means latest
87
if (range === '') {
88
+
return versions[distTags.latest!] ?? null;
89
}
90
91
// check if range is a dist-tag
92
if (range in distTags) {
93
+
const taggedVersion = distTags[range]!;
94
return versions[taggedVersion] ?? null;
95
}
96
···
131
}
132
133
// prefer non-deprecated versions (pnpm behavior)
134
+
const nonDeprecated = validVersions.find((v) => !versions[v]!.deprecated);
135
if (nonDeprecated !== undefined) {
136
+
return versions[nonDeprecated]!;
137
}
138
139
// fall back to deprecated if no alternatives
140
+
return versions[validVersions[0]!]!;
141
}
142
143
/**
+5
-3
src/npm/lib/subpaths.ts
···
115
return entries;
116
}
117
118
-
const [prefix, suffix] = targetParts;
0
119
const subpathParts = subpath.split('*');
120
if (subpathParts.length !== 2) {
121
return entries;
122
}
123
124
-
const [subpathPrefix, subpathSuffix] = subpathParts;
0
125
126
// normalize the prefix to match volume paths
127
// target like "./src/*.js" becomes "/node_modules/pkg/src"
···
270
} else if (entries.length > 0) {
271
// otherwise, pick first alphabetically
272
entries.sort((a, b) => a.subpath.localeCompare(b.subpath));
273
-
defaultSubpath = entries[0].subpath;
274
}
275
276
return {
···
115
return entries;
116
}
117
118
+
const prefix = targetParts[0]!;
119
+
const suffix = targetParts[1]!;
120
const subpathParts = subpath.split('*');
121
if (subpathParts.length !== 2) {
122
return entries;
123
}
124
125
+
const subpathPrefix = subpathParts[0]!;
126
+
const subpathSuffix = subpathParts[1]!;
127
128
// normalize the prefix to match volume paths
129
// target like "./src/*.js" becomes "/node_modules/pkg/src"
···
272
} else if (entries.length > 0) {
273
// otherwise, pick first alphabetically
274
entries.sort((a, b) => a.subpath.localeCompare(b.subpath));
275
+
defaultSubpath = entries[0]!.subpath;
276
}
277
278
return {
+2
-2
src/npm/lib/worker-entry.ts
···
58
59
await fetchPackagesToVolume(hoisted, volume, options.fetch);
60
61
-
const mainPackage = resolution.roots[0];
62
const pkgJsonPath = `/node_modules/${mainPackage.name}/package.json`;
63
const pkgJsonContent = volume.readFileSync(pkgJsonPath, 'utf8') as string;
64
const manifest = JSON.parse(pkgJsonContent) as PackageJson;
···
71
const peerDependencies = Object.keys(manifest.peerDependencies ?? {});
72
const peerDepNames = new Set(peerDependencies);
73
74
-
const packages = buildInstalledPackages(mainPackage, peerDepNames);
75
const installSize = packages.reduce((sum, pkg) => sum + pkg.size, 0);
76
77
initResult = {
···
58
59
await fetchPackagesToVolume(hoisted, volume, options.fetch);
60
61
+
const mainPackage = resolution.roots[0]!;
62
const pkgJsonPath = `/node_modules/${mainPackage.name}/package.json`;
63
const pkgJsonContent = volume.readFileSync(pkgJsonPath, 'utf8') as string;
64
const manifest = JSON.parse(pkgJsonContent) as PackageJson;
···
71
const peerDependencies = Object.keys(manifest.peerDependencies ?? {});
72
const peerDepNames = new Set(peerDependencies);
73
74
+
const packages = buildInstalledPackages(mainPackage!, peerDepNames);
75
const installSize = packages.reduce((sum, pkg) => sum + pkg.size, 0);
76
77
initResult = {
+5
-5
src/primitives/lib/create-active-descendant.ts
···
82
if (enabled.length === 0) {
83
return null;
84
}
85
-
const id = enabled[0].id;
86
setActiveId(id);
87
return id;
88
};
···
92
if (enabled.length === 0) {
93
return null;
94
}
95
-
const id = enabled[enabled.length - 1].id;
96
setActiveId(id);
97
return id;
98
};
···
106
const currentIndex = getActiveIndex();
107
// circular: wrap to first if at end or no current
108
const nextIndex = currentIndex === -1 || currentIndex >= enabled.length - 1 ? 0 : currentIndex + 1;
109
-
const id = enabled[nextIndex].id;
110
setActiveId(id);
111
return id;
112
};
···
120
const currentIndex = getActiveIndex();
121
// circular: wrap to last if at start or no current
122
const prevIndex = currentIndex <= 0 ? enabled.length - 1 : currentIndex - 1;
123
-
const id = enabled[prevIndex].id;
124
setActiveId(id);
125
return id;
126
};
···
146
147
for (let i = 0; i < enabled.length; i++) {
148
const index = (startIndex + i) % enabled.length;
149
-
const item = enabled[index];
150
const textValue = item.textValue?.toLowerCase() ?? '';
151
152
if (textValue.startsWith(searchBuffer)) {
···
82
if (enabled.length === 0) {
83
return null;
84
}
85
+
const id = enabled[0]!.id;
86
setActiveId(id);
87
return id;
88
};
···
92
if (enabled.length === 0) {
93
return null;
94
}
95
+
const id = enabled[enabled.length - 1]!.id;
96
setActiveId(id);
97
return id;
98
};
···
106
const currentIndex = getActiveIndex();
107
// circular: wrap to first if at end or no current
108
const nextIndex = currentIndex === -1 || currentIndex >= enabled.length - 1 ? 0 : currentIndex + 1;
109
+
const id = enabled[nextIndex]!.id;
110
setActiveId(id);
111
return id;
112
};
···
120
const currentIndex = getActiveIndex();
121
// circular: wrap to last if at start or no current
122
const prevIndex = currentIndex <= 0 ? enabled.length - 1 : currentIndex - 1;
123
+
const id = enabled[prevIndex]!.id;
124
setActiveId(id);
125
return id;
126
};
···
146
147
for (let i = 0; i < enabled.length; i++) {
148
const index = (startIndex + i) % enabled.length;
149
+
const item = enabled[index]!;
150
const textValue = item.textValue?.toLowerCase() ?? '';
151
152
if (textValue.startsWith(searchBuffer)) {
+8
-8
src/primitives/lib/create-roving-tabindex.ts
···
67
options?.onFocusChange?.(index);
68
69
if (focus && index >= 0 && index < items.length) {
70
-
items[index].el.focus();
71
}
72
};
73
74
const getEnabledIndices = (): number[] => {
75
const indices: number[] = [];
76
for (let i = 0; i < items.length; i++) {
77
-
if (!items[i].disabled) {
78
indices.push(i);
79
}
80
}
···
86
if (enabled.length === 0) {
87
return;
88
}
89
-
setFocusedIndex(enabled[0]);
90
};
91
92
const last = () => {
···
94
if (enabled.length === 0) {
95
return;
96
}
97
-
setFocusedIndex(enabled[enabled.length - 1]);
98
};
99
100
const next = () => {
···
108
const currentPos = enabled.indexOf(current);
109
// circular: wrap to first if at end or not found
110
const nextPos = currentPos === -1 || currentPos >= enabled.length - 1 ? 0 : currentPos + 1;
111
-
setFocusedIndex(enabled[nextPos]);
112
};
113
114
const prev = () => {
···
122
const currentPos = enabled.indexOf(current);
123
// circular: wrap to last if at start or not found
124
const prevPos = currentPos <= 0 ? enabled.length - 1 : currentPos - 1;
125
-
setFocusedIndex(enabled[prevPos]);
126
};
127
128
const search = (char: string) => {
···
147
148
for (let i = 0; i < enabled.length; i++) {
149
const pos = (startPos + i) % enabled.length;
150
-
const index = enabled[pos];
151
-
const item = items[index];
152
const textValue = item.textValue?.toLowerCase() ?? item.el.textContent?.toLowerCase() ?? '';
153
154
if (textValue.startsWith(searchBuffer)) {
···
67
options?.onFocusChange?.(index);
68
69
if (focus && index >= 0 && index < items.length) {
70
+
items[index]!.el.focus();
71
}
72
};
73
74
const getEnabledIndices = (): number[] => {
75
const indices: number[] = [];
76
for (let i = 0; i < items.length; i++) {
77
+
if (!items[i]!.disabled) {
78
indices.push(i);
79
}
80
}
···
86
if (enabled.length === 0) {
87
return;
88
}
89
+
setFocusedIndex(enabled[0]!);
90
};
91
92
const last = () => {
···
94
if (enabled.length === 0) {
95
return;
96
}
97
+
setFocusedIndex(enabled[enabled.length - 1]!);
98
};
99
100
const next = () => {
···
108
const currentPos = enabled.indexOf(current);
109
// circular: wrap to first if at end or not found
110
const nextPos = currentPos === -1 || currentPos >= enabled.length - 1 ? 0 : currentPos + 1;
111
+
setFocusedIndex(enabled[nextPos]!);
112
};
113
114
const prev = () => {
···
122
const currentPos = enabled.indexOf(current);
123
// circular: wrap to last if at start or not found
124
const prevPos = currentPos <= 0 ? enabled.length - 1 : currentPos - 1;
125
+
setFocusedIndex(enabled[prevPos]!);
126
};
127
128
const search = (char: string) => {
···
147
148
for (let i = 0; i < enabled.length; i++) {
149
const pos = (startPos + i) % enabled.length;
150
+
const index = enabled[pos]!;
151
+
const item = items[index]!;
152
const textValue = item.textValue?.toLowerCase() ?? item.el.textContent?.toLowerCase() ?? '';
153
154
if (textValue.startsWith(searchBuffer)) {
+2
-1
tsconfig.app.json
···
23
"noUnusedParameters": true,
24
"erasableSyntaxOnly": true,
25
"noFallthroughCasesInSwitch": true,
26
-
"noUncheckedSideEffectImports": true
0
27
},
28
"include": ["src"]
29
}
···
23
"noUnusedParameters": true,
24
"erasableSyntaxOnly": true,
25
"noFallthroughCasesInSwitch": true,
26
+
"noUncheckedSideEffectImports": true,
27
+
"noUncheckedIndexedAccess": true
28
},
29
"include": ["src"]
30
}