Skip to content

Commit 024b48a

Browse files
committed
perf(linter/plugins): lazy-load tokens parsing code (#16011)
Tokens APIs (#15861 and further PRs) utilize TS-ESLint for parsing. TS-ESLint uses TypeScript internally. So this pulls in a ton of code (8 MB unminified). Lazy-load this code only when tokens APIs are used for the first time, so that users don't pay for it if their plugins don't use these APIs (or plugins only use tokens APIs for fixes, and their code has no errors that require fixes). Do this by compiling `@typescript-eslint/typescript-estree` into a separate bundle. It's bundled as CommonJS, so it can be synchonously `require`-ed without needing `require(esm)` support. Additionally, minify this bundle, which cuts the increase in package size due to TS-ESLint from 8 MB to 3.7 MB. That's still a lot, but not *completely* unacceptable, given that the `oxlint` binary is around 9.3 MB. As soon as we can, we'll re-implement token-generation in Oxc parser, and then we'll be able to remove TS-ESLint again.
1 parent 7491a3d commit 024b48a

File tree

3 files changed

+60
-27
lines changed

3 files changed

+60
-27
lines changed

apps/oxlint/src-js/plugins/tokens.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* `SourceCode` methods related to tokens.
33
*/
44

5-
import { parse } from '@typescript-eslint/typescript-estree';
5+
import { createRequire } from 'node:module';
66
import { sourceText, initSourceText } from './source_code.js';
77
import { debugAssertIsNonNull } from '../utils/asserts.js';
88

@@ -139,16 +139,30 @@ let tokens: Token[] | null = null;
139139
let comments: CommentToken[] | null = null;
140140
let tokensWithComments: Token[] | null = null;
141141

142+
// TS-ESLint `parse` method.
143+
// Lazy-loaded only when needed, as it's a lot of code.
144+
// Bundle contains both `@typescript-eslint/typescript-estree` and `typescript`.
145+
let tsEslintParse: typeof import('@typescript-eslint/typescript-estree').parse | null = null;
146+
142147
/**
143148
* Initialize TS-ESLint tokens for current file.
144149
*/
145150
function initTokens() {
146151
debugAssertIsNonNull(sourceText);
147-
({ tokens, comments } = parse(sourceText, {
152+
153+
// Lazy-load TS-ESLint.
154+
// `./ts_eslint.cjs` is path to the bundle in `dist` directory, as well as relative path in `src-js`,
155+
// so is valid both in bundled `dist` output, and in unit tests.
156+
if (tsEslintParse === null) {
157+
const require = createRequire(import.meta.url);
158+
tsEslintParse = (require('./ts_eslint.cjs') as typeof import('@typescript-eslint/typescript-estree')).parse;
159+
}
160+
161+
({ tokens, comments } = tsEslintParse(sourceText, {
148162
sourceType: 'module',
149163
tokens: true,
150164
comment: true,
151-
// TODO: Enable JSX only when needed
165+
// TODO: Set this option dependent on source type
152166
jsx: true,
153167
}));
154168
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
'use strict';
2+
3+
module.exports = require('@typescript-eslint/typescript-estree');

apps/oxlint/tsdown.config.ts

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,39 +7,55 @@ import type { Plugin } from 'rolldown';
77
// This is the build used in tests.
88
const DEBUG = process.env.DEBUG === 'true' || process.env.DEBUG === '1';
99

10-
export default defineConfig({
11-
entry: ['src-js/cli.ts', 'src-js/index.ts'],
12-
format: 'esm',
10+
const commonConfig = defineConfig({
1311
platform: 'node',
1412
target: 'node20',
1513
outDir: 'dist',
1614
clean: true,
1715
unbundle: false,
1816
hash: false,
19-
external: [
20-
// External native bindings
21-
'./oxlint.*.node',
22-
'@oxlint/*',
23-
],
2417
fixedExtension: false,
25-
// Handle `__filename`. Needed to bundle `typescript` for token methods.
26-
shims: true,
27-
// At present only compress syntax.
28-
// Don't mangle identifiers or remove whitespace, so `dist` code remains somewhat readable.
29-
minify: {
30-
compress: { keepNames: { function: true, class: true } },
31-
mangle: false,
32-
codegen: { removeWhitespace: false },
18+
});
19+
20+
export default defineConfig([
21+
// Main build
22+
{
23+
...commonConfig,
24+
entry: ['src-js/cli.ts', 'src-js/index.ts'],
25+
format: 'esm',
26+
external: [
27+
// External native bindings
28+
'./oxlint.*.node',
29+
'@oxlint/*',
30+
],
31+
// At present only compress syntax.
32+
// Don't mangle identifiers or remove whitespace, so `dist` code remains somewhat readable.
33+
minify: {
34+
compress: { keepNames: { function: true, class: true } },
35+
mangle: false,
36+
codegen: { removeWhitespace: false },
37+
},
38+
dts: { resolve: true },
39+
attw: true,
40+
define: { DEBUG: DEBUG ? 'true' : 'false' },
41+
plugins: DEBUG ? [] : [createReplaceAssertsPlugin()],
42+
inputOptions: {
43+
// For `replaceAssertsPlugin`
44+
experimental: { nativeMagicString: true },
45+
},
3346
},
34-
dts: { resolve: true },
35-
attw: true,
36-
define: { DEBUG: DEBUG ? 'true' : 'false' },
37-
plugins: DEBUG ? [] : [createReplaceAssertsPlugin()],
38-
inputOptions: {
39-
// For `replaceAssertsPlugin`
40-
experimental: { nativeMagicString: true },
47+
// TS-ESLint parser.
48+
// Bundled separately and lazy-loaded, as it's a lot of code.
49+
// Bundle contains both `@typescript-eslint/typescript-estree` and `typescript`.
50+
{
51+
...commonConfig,
52+
entry: 'src-js/plugins/ts_eslint.cjs',
53+
format: 'commonjs',
54+
// Minify as this bundle is just dependencies. We don't need to be able to debug it.
55+
// Minification halves the size of the bundle.
56+
minify: true,
4157
},
42-
});
58+
]);
4359

4460
/**
4561
* Create a plugin to remove imports of `assert*` functions from `src-js/utils/asserts.ts`,

0 commit comments

Comments
 (0)