Skip to content

Commit 6432199

Browse files
authored
perf: Speed up dev prefix generation for long file paths (#1466)
1 parent 96dd466 commit 6432199

File tree

6 files changed

+136
-11
lines changed

6 files changed

+136
-11
lines changed

.changeset/lemon-colts-allow.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@vanilla-extract/css': patch
3+
---
4+
5+
Speed up dev prefix generation for long file paths

packages/css/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@
124124
"dedent": "^1.5.3",
125125
"deep-object-diff": "^1.1.9",
126126
"deepmerge": "^4.2.2",
127+
"lru-cache": "^10.4.3",
127128
"media-query-parser": "^2.0.2",
128129
"modern-ahocorasick": "^1.0.0",
129130
"picocolors": "^1.0.0"
+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { getDebugFileName } from './getDebugFileName';
2+
3+
const testCases = [
4+
{
5+
name: 'longPath',
6+
input:
7+
'node_modules/.pnpm/braid-design-system@32.23.0_@types+react@18.3.3_babel-plugin-macros@3.1.0_react-dom@18.3.1_re_ctndskkf4y74v2qksfjwfz6ezy/node_modules/braid-design-system/dist/lib/components/ButtonDir/Button.css.mjs',
8+
expected: 'Button',
9+
},
10+
{
11+
name: 'emojiPath',
12+
input: 'node_modules/my_package/dist/👨‍👩‍👦Test🎉Dir👨‍🚀/Test🎉File👨‍🚀.css.ts',
13+
expected: 'Test🎉File👨‍🚀',
14+
},
15+
{
16+
name: 'loneSurrogates',
17+
input: 'node_modules/my_package/dist/Test\uD801Dir/Test\uDC01File.css.ts',
18+
expected: 'Test\uDC01File',
19+
},
20+
{
21+
name: 'noExtension',
22+
input: 'node_modules/my_package/dist/TestDir/TestFile',
23+
expected: '',
24+
},
25+
{
26+
name: 'singleFileSparator',
27+
input: 'src/Button.css.ts',
28+
expected: 'Button',
29+
},
30+
{
31+
name: 'noDir',
32+
input: 'myFile.css.ts',
33+
expected: 'myFile',
34+
},
35+
];
36+
37+
describe('getDebugFileName', () => {
38+
testCases.forEach(({ name, input, expected }) => {
39+
it(name, () => {
40+
const result = getDebugFileName(input);
41+
42+
expect(result).toStrictEqual(expected);
43+
});
44+
});
45+
});

packages/css/src/getDebugFileName.ts

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { LRUCache } from 'lru-cache';
2+
3+
const getLastSlashBeforeIndex = (path: string, index: number) => {
4+
let pathIndex = index - 1;
5+
6+
while (pathIndex >= 0) {
7+
if (path[pathIndex] === '/') {
8+
return pathIndex;
9+
}
10+
11+
pathIndex--;
12+
}
13+
14+
return -1;
15+
};
16+
17+
/**
18+
* Assumptions:
19+
* - The path is always normalized to use posix file separators (/) (see `addFileScope`)
20+
* - The path is always relative to the project root, i.e. there will never be a leading slash (see `addFileScope`)
21+
* - As long as `.css` is there, we have a valid `.css.*` file path, because otherwise there wouldn't
22+
* be a file scope to begin with
23+
*
24+
* The LRU cache we use can't cache undefined/null values, so we opt to return an empty string,
25+
* rather than using a custom Symbol or something similar.
26+
*/
27+
const _getDebugFileName = (path: string): string => {
28+
let file: string;
29+
30+
const lastIndexOfDotCss = path.lastIndexOf('.css');
31+
32+
if (lastIndexOfDotCss === -1) {
33+
return '';
34+
}
35+
36+
const lastSlashIndex = getLastSlashBeforeIndex(path, lastIndexOfDotCss);
37+
file = path.slice(lastSlashIndex + 1, lastIndexOfDotCss);
38+
39+
// There are no slashes, therefore theres no directory to extract
40+
if (lastSlashIndex === -1) {
41+
return file;
42+
}
43+
44+
let secondLastSlashIndex = getLastSlashBeforeIndex(path, lastSlashIndex - 1);
45+
// If secondLastSlashIndex is -1, it means that the path looks like `directory/file.css.ts`,
46+
// in which case dir will still be sliced starting at 0, which is what we want
47+
const dir = path.slice(secondLastSlashIndex + 1, lastSlashIndex);
48+
49+
const debugFileName = file !== 'index' ? file : dir;
50+
51+
return debugFileName;
52+
};
53+
54+
const memoizedGetDebugFileName = () => {
55+
const cache = new LRUCache<string, string>({
56+
max: 500,
57+
});
58+
59+
return (path: string) => {
60+
const cachedResult = cache.get(path);
61+
62+
if (cachedResult) {
63+
return cachedResult;
64+
}
65+
66+
const result = _getDebugFileName(path);
67+
cache.set(path, result);
68+
69+
return result;
70+
};
71+
};
72+
73+
export const getDebugFileName = memoizedGetDebugFileName();

packages/css/src/identifier.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import hash from '@emotion/hash';
22

33
import { getIdentOption } from './adapter';
44
import { getAndIncrementRefCounter, getFileScope } from './fileScope';
5+
import { getDebugFileName } from './getDebugFileName';
56

67
function getDevPrefix({
78
debugId,
@@ -15,13 +16,11 @@ function getDevPrefix({
1516
if (debugFileName) {
1617
const { filePath } = getFileScope();
1718

18-
const matches = filePath.match(
19-
/(?<dir>[^\/\\]*)?[\/\\]?(?<file>[^\/\\]*)\.css\.(ts|js|tsx|jsx|cjs|mjs)$/,
20-
);
19+
const debugFileName = getDebugFileName(filePath);
2120

22-
if (matches && matches.groups) {
23-
const { dir, file } = matches.groups;
24-
parts.unshift(file && file !== 'index' ? file : dir);
21+
// debugFileName could be an empty string
22+
if (debugFileName) {
23+
parts.unshift(debugFileName);
2524
}
2625
}
2726

pnpm-lock.yaml

+7-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)