Skip to content

Commit aa29d1c

Browse files
AlpAlp
authored andcommitted
feat: add --experimental-lazy flag for faster CLI startup in monorepos
Skip loading all source files from all tsconfigs when --experimental-lazy is passed. Instead, only load the config/schema file that was explicitly requested. This dramatically reduces startup time in large monorepos. Benchmarks: - Synthetic (502 files): 277ms -> 3.3ms (83.8x faster) - Real monorepo (453 files): 24.51s -> 6.16s (3.98x faster) - Output is byte-for-byte identical
1 parent aa835a4 commit aa29d1c

File tree

516 files changed

+62915
-7
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

516 files changed

+62915
-7
lines changed

src/cli/index.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export interface GeneratorOptions {
6767
enableLegacyMutators?: boolean;
6868
enableLegacyQueries?: boolean;
6969
suppressDefaultsWarning?: boolean;
70+
experimentalLazy?: boolean;
7071
}
7172

7273
async function main(opts: GeneratorOptions = {}) {
@@ -85,6 +86,7 @@ async function main(opts: GeneratorOptions = {}) {
8586
enableLegacyMutators,
8687
enableLegacyQueries,
8788
suppressDefaultsWarning,
89+
experimentalLazy,
8890
} = {...opts};
8991

9092
const resolvedTsConfigPath = tsConfigPath ?? defaultTsConfigFile;
@@ -99,18 +101,21 @@ async function main(opts: GeneratorOptions = {}) {
99101
'😶‍🌫️ drizzle-zero: Using all tables/columns from Drizzle schema',
100102
);
101103
}
102-
const allTsConfigPaths = await discoverAllTsConfigs(resolvedTsConfigPath);
103104

104105
const tsProject = new Project({
105106
tsConfigFilePath: resolvedTsConfigPath,
106107
skipAddingFilesFromTsConfig: true,
107108
});
108-
for (const tsConfigPath of allTsConfigPaths) {
109-
addSourceFilesFromTsConfigSafe({
110-
tsProject,
111-
tsConfigPath,
112-
debug: Boolean(debug),
113-
});
109+
110+
if (!experimentalLazy) {
111+
const allTsConfigPaths = await discoverAllTsConfigs(resolvedTsConfigPath);
112+
for (const tsConfigPath of allTsConfigPaths) {
113+
addSourceFilesFromTsConfigSafe({
114+
tsProject,
115+
tsConfigPath,
116+
debug: Boolean(debug),
117+
});
118+
}
114119
}
115120

116121
if (configFilePath) {
@@ -226,6 +231,11 @@ function cli() {
226231
'Hide warnings for columns with default values',
227232
false,
228233
)
234+
.option(
235+
'--experimental-lazy',
236+
'Use lazy file loading for faster startup (only loads files reachable from config)',
237+
false,
238+
)
229239
.option(
230240
'--force',
231241
'Overwrite the output file even if it has been manually modified',
@@ -250,6 +260,7 @@ function cli() {
250260
enableLegacyMutators: command.enableLegacyMutators,
251261
enableLegacyQueries: command.enableLegacyQueries,
252262
suppressDefaultsWarning: command.suppressDefaultsWarning,
263+
experimentalLazy: command.experimentalLazy,
253264
});
254265

255266
if (command.output) {

tests/benchmark.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import * as path from 'node:path';
2+
import {Project} from 'ts-morph';
3+
import {test, expect} from 'vitest';
4+
import {discoverAllTsConfigs} from '../src/cli/tsconfig';
5+
import {
6+
addSourceFilesFromTsConfigSafe,
7+
ensureSourceFileInProject,
8+
} from '../src/cli/ts-project';
9+
10+
const fixtureRoot = path.resolve(
11+
__dirname,
12+
'benchmarks/synthetic-monorepo/tsconfig.json',
13+
);
14+
const schemaConfigFile = path.resolve(
15+
__dirname,
16+
'benchmarks/synthetic-monorepo/schema/drizzle-zero.config.ts',
17+
);
18+
19+
test(
20+
'lazy loading loads fewer files and is faster than eager loading',
21+
{timeout: 120_000},
22+
async () => {
23+
// -- EAGER approach: discover all tsconfigs, add all source files --
24+
const eagerStart = performance.now();
25+
26+
const allTsConfigPaths = await discoverAllTsConfigs(fixtureRoot);
27+
28+
const eagerProject = new Project({
29+
tsConfigFilePath: fixtureRoot,
30+
skipAddingFilesFromTsConfig: true,
31+
});
32+
33+
for (const tsConfigPath of allTsConfigPaths) {
34+
addSourceFilesFromTsConfigSafe({
35+
tsProject: eagerProject,
36+
tsConfigPath,
37+
debug: false,
38+
});
39+
}
40+
41+
ensureSourceFileInProject({
42+
tsProject: eagerProject,
43+
filePath: schemaConfigFile,
44+
debug: false,
45+
});
46+
47+
const eagerFileCount = eagerProject.getSourceFiles().length;
48+
const eagerTime = performance.now() - eagerStart;
49+
50+
// -- LAZY approach: only load the config file and resolve its dependencies --
51+
const lazyStart = performance.now();
52+
53+
const lazyProject = new Project({
54+
tsConfigFilePath: fixtureRoot,
55+
skipAddingFilesFromTsConfig: true,
56+
});
57+
58+
ensureSourceFileInProject({
59+
tsProject: lazyProject,
60+
filePath: schemaConfigFile,
61+
debug: false,
62+
});
63+
64+
const lazyFileCount = lazyProject.getSourceFiles().length;
65+
const lazyTime = performance.now() - lazyStart;
66+
67+
// -- Report --
68+
const ratio = eagerTime / lazyTime;
69+
console.log(`Eager: ${eagerFileCount} files in ${eagerTime.toFixed(1)}ms`);
70+
console.log(`Lazy: ${lazyFileCount} files in ${lazyTime.toFixed(1)}ms`);
71+
console.log(`Ratio: eager is ${ratio.toFixed(2)}x slower than lazy`);
72+
73+
// -- Assertions --
74+
expect(lazyFileCount).toBeLessThan(eagerFileCount);
75+
expect(lazyTime).toBeLessThan(eagerTime);
76+
},
77+
);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ESNext",
4+
"module": "ESNext",
5+
"moduleResolution": "bundler",
6+
"strict": true,
7+
"noEmit": true,
8+
"skipLibCheck": true,
9+
"composite": true,
10+
"declaration": true,
11+
"outDir": "out"
12+
},
13+
"include": ["./**/*.ts"]
14+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// pkg-01 / types-01 (seed 101) - expensive recursive & mapped types
2+
3+
// ── 1. DeepPartial over a large interface ────────────────────────────────────
4+
type DeepPartial<T> = {
5+
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
6+
};
7+
8+
interface BigRecord101 {
9+
a101: { x: number; y: string; z: boolean };
10+
b101: { p: string[]; q: Record<string, number> };
11+
c101: { nested: { deep: { deeper: { deepest: string } } } };
12+
d101: number;
13+
e101: string;
14+
f101: boolean;
15+
g101: null;
16+
h101: undefined;
17+
i101: bigint;
18+
j101: symbol;
19+
}
20+
21+
type PartialBig101 = DeepPartial<BigRecord101>;
22+
23+
// ── 2. Recursive Flatten ─────────────────────────────────────────────────────
24+
type Flatten101<T> = T extends Array<infer U> ? Flatten101<U> : T;
25+
type Nested101 = number[][][][][][][][][][];
26+
type Flat101 = Flatten101<Nested101>;
27+
28+
// ── 3. Deep readonly + required ──────────────────────────────────────────────
29+
type DeepReadonly101<T> = {
30+
readonly [K in keyof T]: T[K] extends object ? DeepReadonly101<T[K]> : T[K];
31+
};
32+
type DeepRequired101<T> = {
33+
[K in keyof T]-?: T[K] extends object ? DeepRequired101<T[K]> : T[K];
34+
};
35+
type FR101 = DeepReadonly101<DeepRequired101<PartialBig101>>;
36+
37+
// ── 4. Large union type (50 members) ─────────────────────────────────────────
38+
type BigUnion101 =
39+
| "alpha" | "bravo" | "charlie" | "delta" | "echo"
40+
| "foxtrot" | "golf" | "hotel" | "india" | "juliet"
41+
| "kilo" | "lima" | "mike" | "november" | "oscar"
42+
| "papa" | "quebec" | "romeo" | "sierra" | "tango"
43+
| "uniform" | "victor" | "whiskey" | "xray" | "yankee"
44+
| "zulu" | "one" | "two" | "three" | "four"
45+
| "five" | "six" | "seven" | "eight" | "nine"
46+
| "ten" | "eleven" | "twelve" | "thirteen" | "fourteen"
47+
| "fifteen" | "sixteen" | "seventeen" | "eighteen" | "nineteen"
48+
| "twenty" | "twentyone" | "twentytwo" | "twentythree" | "twentyfour"
49+
| "twentyfive";
50+
51+
type ExtractAlpha101 = Extract<BigUnion101, "alpha" | "bravo" | "charlie">;
52+
type ExcludeZulu101 = Exclude<BigUnion101, "zulu">;
53+
54+
// ── 5. Mapped type over intersection of interfaces ───────────────────────────
55+
interface ShapeA101 { width: number; height: number; depth: number }
56+
interface ShapeB101 { color: string; opacity: number; blend: string }
57+
interface ShapeC101 { x: number; y: number; z: number; w: number }
58+
interface ShapeD101 { label: string; title: string; summary: string }
59+
60+
type Combined101 = ShapeA101 & ShapeB101 & ShapeC101 & ShapeD101;
61+
type OptionalAll101 = { [K in keyof Combined101]?: Combined101[K] };
62+
type RequiredAll101 = { [K in keyof Combined101]-?: Combined101[K] };
63+
type ReadonlyAll101 = { readonly [K in keyof Combined101]: Combined101[K] };
64+
type NullableAll101 = { [K in keyof Combined101]: Combined101[K] | null };
65+
66+
// ── 6. Conditional type chains ───────────────────────────────────────────────
67+
type IsString101<T> = T extends string ? true : false;
68+
type IsNumber101<T> = T extends number ? true : false;
69+
type TypeName101<T> = T extends string
70+
? "string"
71+
: T extends number
72+
? "number"
73+
: T extends boolean
74+
? "boolean"
75+
: T extends null
76+
? "null"
77+
: T extends undefined
78+
? "undefined"
79+
: T extends symbol
80+
? "symbol"
81+
: T extends bigint
82+
? "bigint"
83+
: "object";
84+
85+
type TypeNames101 = {
86+
[K in keyof BigRecord101]: TypeName101<BigRecord101[K]>;
87+
};
88+
89+
// ── 7. Template literal type combinations ────────────────────────────────────
90+
type Verb101 = "get" | "set" | "delete" | "update" | "create" | "list";
91+
type Resource101 = "user" | "post" | "comment" | "tag" | "category";
92+
type Action101 = `${Verb101}_${Resource101}`;
93+
94+
// ── 8. Infer in conditional types ────────────────────────────────────────────
95+
type UnwrapPromise101<T> = T extends Promise<infer U> ? UnwrapPromise101<U> : T;
96+
type UnwrapArray101<T> = T extends (infer U)[] ? UnwrapArray101<U> : T;
97+
type Head101<T extends unknown[]> = T extends [infer H, ...infer _] ? H : never;
98+
type Tail101<T extends unknown[]> = T extends [infer _, ...infer R] ? R : never;
99+
100+
// ── 9. Permutation of union ───────────────────────────────────────────────────
101+
type Permutation101<T, K = T> = [T] extends [never]
102+
? []
103+
: K extends K
104+
? [K, ...Permutation101<Exclude<T, K>>]
105+
: never;
106+
107+
type SmallUnion101 = "a" | "b" | "c" | "d";
108+
type AllPerms101 = Permutation101<SmallUnion101>;
109+
110+
// ── 10. Re-export to force inclusion ─────────────────────────────────────────
111+
export type {
112+
PartialBig101,
113+
Flat101,
114+
FR101,
115+
BigUnion101,
116+
ExtractAlpha101,
117+
ExcludeZulu101,
118+
OptionalAll101,
119+
RequiredAll101,
120+
ReadonlyAll101,
121+
NullableAll101,
122+
TypeNames101,
123+
Action101,
124+
AllPerms101,
125+
};

0 commit comments

Comments
 (0)