Skip to content

Commit 794322d

Browse files
committed
perf(cli): actually lazy-load command implementations
buildLazyCommand() provided no laziness: cli-executable.ts statically imported { commandSpec } from every *.command.ts, and since each of those modules held both the spec and the implementation, the static import pulled in the whole module. esbuild then collapsed the dynamic import inside buildLazyCommand entirely, so `varlock --version` eagerly parsed the full proxy subsystem. Split each command's spec into a sibling *.command-spec.ts holding only name/description/args/examples, and register commands with gunshi's own lazy(loader, definition) helper. The entry now imports specs only, and the implementation arrives through a dynamic import when the command runs. For the three commands with nested subcommands (proxy, cache, keychain), the spec module declares each verb as its own lazy() whose loader pulls the run fn out of the implementation module, so the parent spec no longer embeds the implementations by construction.
1 parent 3fb4ae9 commit 794322d

50 files changed

Lines changed: 1339 additions & 1251 deletions

Some content is hidden

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

.bumpy/lazy-command-loading.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
varlock: patch
3+
---
4+
5+
lazy-load CLI command implementations so startup does not parse every command

packages/varlock/src/cli/cli-executable.ts

Lines changed: 52 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { cli, type Command } from 'gunshi';
1+
import { cli, lazy } from 'gunshi';
22
import completion from '@gunshi/plugin-completion';
33
import { gracefulExit } from 'exit-hook';
44

@@ -16,75 +16,65 @@ import { checkLocalVersionMismatch } from '../lib/check-local-version';
1616
import packageJson from '../../package.json';
1717
import { enforceProxyContextGuards } from './helpers/proxy-context-guard';
1818

19-
// we'll import just the spec from each, so the implementations can be lazy loaded
20-
import { commandSpec as initCommandSpec } from './commands/init.command';
21-
import { commandSpec as loadCommandSpec } from './commands/load.command';
22-
import { commandSpec as runCommandSpec } from './commands/run.command';
23-
import { commandSpec as printenvCommandSpec } from './commands/printenv.command';
24-
import { commandSpec as encryptCommandSpec } from './commands/encrypt.command';
25-
import { commandSpec as lockCommandSpec } from './commands/lock.command';
26-
import { commandSpec as revealCommandSpec } from './commands/reveal.command';
27-
// import { commandSpec as doctorCommandSpec } from './commands/doctor.command';
28-
import { commandSpec as helpCommandSpec } from './commands/help.command';
29-
import { commandSpec as telemetryCommandSpec } from './commands/telemetry.command';
30-
import { commandSpec as explainCommandSpec } from './commands/explain.command';
31-
import { commandSpec as flattenCommandSpec } from './commands/flatten.command';
32-
import { commandSpec as scanCommandSpec } from './commands/scan.command';
33-
import { commandSpec as codegenCommandSpec } from './commands/codegen.command';
34-
import { commandSpec as typegenCommandSpec } from './commands/typegen.command';
35-
import { commandSpec as installPluginCommandSpec } from './commands/install-plugin.command';
36-
import { commandSpec as auditCommandSpec } from './commands/audit.command';
37-
import { commandSpec as generateKeyCommandSpec } from './commands/generate-key.command';
38-
import { commandSpec as cacheCommandSpec } from './commands/cache.command';
39-
import { commandSpec as keychainCommandSpec } from './commands/keychain.command';
40-
import { commandSpec as proxyCommandSpec } from './commands/proxy.command';
41-
// import { commandSpec as loginCommandSpec } from './commands/login.command';
42-
// import { commandSpec as pluginCommandSpec } from './commands/plugin.command';
19+
// Only the spec (name/description/args/examples) is imported eagerly - each command's
20+
// implementation lives in a sibling `*.command.ts` that is pulled in via a dynamic
21+
// import when that command actually runs. Keeping the two in separate modules is what
22+
// makes the split real: importing anything from `*.command.ts` here would drag the whole
23+
// implementation into the entry chunk and collapse the dynamic import away.
24+
import { commandSpec as initCommandSpec } from './commands/init.command-spec';
25+
import { commandSpec as loadCommandSpec } from './commands/load.command-spec';
26+
import { commandSpec as runCommandSpec } from './commands/run.command-spec';
27+
import { commandSpec as printenvCommandSpec } from './commands/printenv.command-spec';
28+
import { commandSpec as encryptCommandSpec } from './commands/encrypt.command-spec';
29+
import { commandSpec as lockCommandSpec } from './commands/lock.command-spec';
30+
import { commandSpec as revealCommandSpec } from './commands/reveal.command-spec';
31+
// import { commandSpec as doctorCommandSpec } from './commands/doctor.command-spec';
32+
import { commandSpec as helpCommandSpec } from './commands/help.command-spec';
33+
import { commandSpec as telemetryCommandSpec } from './commands/telemetry.command-spec';
34+
import { commandSpec as explainCommandSpec } from './commands/explain.command-spec';
35+
import { commandSpec as flattenCommandSpec } from './commands/flatten.command-spec';
36+
import { commandSpec as scanCommandSpec } from './commands/scan.command-spec';
37+
import { commandSpec as codegenCommandSpec } from './commands/codegen.command-spec';
38+
import { commandSpec as typegenCommandSpec } from './commands/typegen.command-spec';
39+
import { commandSpec as installPluginCommandSpec } from './commands/install-plugin.command-spec';
40+
import { commandSpec as auditCommandSpec } from './commands/audit.command-spec';
41+
import { commandSpec as generateKeyCommandSpec } from './commands/generate-key.command-spec';
42+
import { commandSpec as cacheCommandSpec } from './commands/cache.command-spec';
43+
import { commandSpec as keychainCommandSpec } from './commands/keychain.command-spec';
44+
import { commandSpec as proxyCommandSpec } from './commands/proxy.command-spec';
45+
// import { commandSpec as loginCommandSpec } from './commands/login.command-spec';
46+
// import { commandSpec as pluginCommandSpec } from './commands/plugin.command-spec';
4347

4448
// must happen before anything writes to stdio
4549
handleBrokenPipe();
4650

4751
let versionId = packageJson.version;
4852
if (__VARLOCK_BUILD_TYPE__ !== 'release') versionId += `-${__VARLOCK_BUILD_TYPE__}`;
4953

50-
// TODO: this is not splitting the bundle correctly to actually lazy load the command fns
51-
function buildLazyCommand(
52-
commandSpec: Command<any>,
53-
loadCommandFn: () => Promise<{ commandSpec: Command<any>, commandFn: any }>,
54-
) {
55-
return {
56-
...commandSpec,
57-
run: async (...args: Array<any>) => {
58-
const commandSpecAndFn = await loadCommandFn();
59-
return await commandSpecAndFn.commandFn(...args);
60-
},
61-
};
62-
}
63-
6454
const subCommands = new Map();
65-
subCommands.set('init', buildLazyCommand(initCommandSpec, async () => await import('./commands/init.command')));
66-
subCommands.set('load', buildLazyCommand(loadCommandSpec, async () => await import('./commands/load.command')));
67-
subCommands.set('run', buildLazyCommand(runCommandSpec, async () => await import('./commands/run.command')));
68-
subCommands.set('printenv', buildLazyCommand(printenvCommandSpec, async () => await import('./commands/printenv.command')));
69-
subCommands.set('encrypt', buildLazyCommand(encryptCommandSpec, async () => await import('./commands/encrypt.command')));
70-
subCommands.set('lock', buildLazyCommand(lockCommandSpec, async () => await import('./commands/lock.command')));
71-
subCommands.set('reveal', buildLazyCommand(revealCommandSpec, async () => await import('./commands/reveal.command')));
72-
// subCommands.set('doctor', buildLazyCommand(doctorCommandSpec, async () => await import('./commands/doctor.command')));
73-
subCommands.set('explain', buildLazyCommand(explainCommandSpec, async () => await import('./commands/explain.command')));
74-
subCommands.set('flatten', buildLazyCommand(flattenCommandSpec, async () => await import('./commands/flatten.command')));
75-
subCommands.set('help', buildLazyCommand(helpCommandSpec, async () => await import('./commands/help.command')));
76-
subCommands.set('telemetry', buildLazyCommand(telemetryCommandSpec, async () => await import('./commands/telemetry.command')));
77-
subCommands.set('scan', buildLazyCommand(scanCommandSpec, async () => await import('./commands/scan.command')));
78-
subCommands.set('audit', buildLazyCommand(auditCommandSpec, async () => await import('./commands/audit.command')));
79-
subCommands.set('codegen', buildLazyCommand(codegenCommandSpec, async () => await import('./commands/codegen.command')));
80-
subCommands.set('typegen', buildLazyCommand(typegenCommandSpec, async () => await import('./commands/typegen.command')));
81-
subCommands.set('install-plugin', buildLazyCommand(installPluginCommandSpec, async () => await import('./commands/install-plugin.command')));
82-
subCommands.set('generate-key', buildLazyCommand(generateKeyCommandSpec, async () => await import('./commands/generate-key.command')));
83-
subCommands.set('cache', buildLazyCommand(cacheCommandSpec, async () => await import('./commands/cache.command')));
84-
subCommands.set('keychain', buildLazyCommand(keychainCommandSpec, async () => await import('./commands/keychain.command')));
85-
subCommands.set('proxy', buildLazyCommand(proxyCommandSpec, async () => await import('./commands/proxy.command')));
86-
// subCommands.set('login', buildLazyCommand(loginCommandSpec, async () => await import('./commands/login.command')));
87-
// subCommands.set('plugin', buildLazyCommand(pluginCommandSpec, async () => await import('./commands/plugin.command')));
55+
subCommands.set('init', lazy(async () => (await import('./commands/init.command')).commandFn, initCommandSpec));
56+
subCommands.set('load', lazy(async () => (await import('./commands/load.command')).commandFn, loadCommandSpec));
57+
subCommands.set('run', lazy(async () => (await import('./commands/run.command')).commandFn, runCommandSpec));
58+
subCommands.set('printenv', lazy(async () => (await import('./commands/printenv.command')).commandFn, printenvCommandSpec));
59+
subCommands.set('encrypt', lazy(async () => (await import('./commands/encrypt.command')).commandFn, encryptCommandSpec));
60+
subCommands.set('lock', lazy(async () => (await import('./commands/lock.command')).commandFn, lockCommandSpec));
61+
subCommands.set('reveal', lazy(async () => (await import('./commands/reveal.command')).commandFn, revealCommandSpec));
62+
// subCommands.set('doctor', lazy(async () => (await import('./commands/doctor.command')).commandFn, doctorCommandSpec));
63+
subCommands.set('explain', lazy(async () => (await import('./commands/explain.command')).commandFn, explainCommandSpec));
64+
subCommands.set('flatten', lazy(async () => (await import('./commands/flatten.command')).commandFn, flattenCommandSpec));
65+
subCommands.set('help', lazy(async () => (await import('./commands/help.command')).commandFn, helpCommandSpec));
66+
subCommands.set('telemetry', lazy(async () => (await import('./commands/telemetry.command')).commandFn, telemetryCommandSpec));
67+
subCommands.set('scan', lazy(async () => (await import('./commands/scan.command')).commandFn, scanCommandSpec));
68+
subCommands.set('audit', lazy(async () => (await import('./commands/audit.command')).commandFn, auditCommandSpec));
69+
subCommands.set('codegen', lazy(async () => (await import('./commands/codegen.command')).commandFn, codegenCommandSpec));
70+
subCommands.set('typegen', lazy(async () => (await import('./commands/typegen.command')).commandFn, typegenCommandSpec));
71+
subCommands.set('install-plugin', lazy(async () => (await import('./commands/install-plugin.command')).commandFn, installPluginCommandSpec));
72+
subCommands.set('generate-key', lazy(async () => (await import('./commands/generate-key.command')).commandFn, generateKeyCommandSpec));
73+
subCommands.set('cache', lazy(async () => (await import('./commands/cache.command')).commandFn, cacheCommandSpec));
74+
subCommands.set('keychain', lazy(async () => (await import('./commands/keychain.command')).commandFn, keychainCommandSpec));
75+
subCommands.set('proxy', lazy(async () => (await import('./commands/proxy.command')).commandFn, proxyCommandSpec));
76+
// subCommands.set('login', lazy(async () => (await import('./commands/login.command')).commandFn, loginCommandSpec));
77+
// subCommands.set('plugin', lazy(async () => (await import('./commands/plugin.command')).commandFn, pluginCommandSpec));
8878

8979
(async function go() {
9080
try {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { define } from 'gunshi';
2+
3+
export const commandSpec = define({
4+
name: 'audit',
5+
description: 'Audit code env var usage against your .env.schema',
6+
args: {
7+
targets: {
8+
type: 'positional',
9+
required: false,
10+
multiple: true,
11+
description: 'Directories to scan for env var references (defaults to the current project)',
12+
},
13+
path: {
14+
type: 'string',
15+
short: 'p',
16+
description: 'Path to a specific .env file or directory to use as the schema entry point',
17+
},
18+
ignore: {
19+
type: 'string',
20+
short: 'i',
21+
multiple: true,
22+
description: 'Directory to exclude from code scanning (can be specified multiple times)',
23+
},
24+
},
25+
examples: `
26+
Scans your source code for environment variable references and compares them
27+
to keys defined in your varlock schema.
28+
29+
Examples:
30+
varlock audit # Audit current project
31+
varlock audit --path .env.prod # Audit using a specific env entry point
32+
varlock audit ./src ./lib # Only scan specific directories
33+
varlock audit --ignore vendor # Exclude a directory from scanning
34+
varlock audit -i vendor -i generated # Exclude multiple directories
35+
`.trim(),
36+
});

packages/varlock/src/cli/commands/audit.command.ts

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import fs from 'node:fs/promises';
22
import path from 'node:path';
33
import ansis from 'ansis';
4-
import { define } from 'gunshi';
54

65
import { FileBasedDataSource } from '../../env-graph';
76
import { loadVarlockEnvGraph } from '../../lib/load-graph';
@@ -15,41 +14,9 @@ import {
1514
import { gracefulExit } from 'exit-hook';
1615
import { diffSchemaAndCodeKeys } from '../helpers/audit-diff';
1716
import { isWellKnownEnvKey } from '../helpers/well-known-env-keys';
17+
import { commandSpec } from './audit.command-spec';
1818

19-
export const commandSpec = define({
20-
name: 'audit',
21-
description: 'Audit code env var usage against your .env.schema',
22-
args: {
23-
targets: {
24-
type: 'positional',
25-
required: false,
26-
multiple: true,
27-
description: 'Directories to scan for env var references (defaults to the current project)',
28-
},
29-
path: {
30-
type: 'string',
31-
short: 'p',
32-
description: 'Path to a specific .env file or directory to use as the schema entry point',
33-
},
34-
ignore: {
35-
type: 'string',
36-
short: 'i',
37-
multiple: true,
38-
description: 'Directory to exclude from code scanning (can be specified multiple times)',
39-
},
40-
},
41-
examples: `
42-
Scans your source code for environment variable references and compares them
43-
to keys defined in your varlock schema.
44-
45-
Examples:
46-
varlock audit # Audit current project
47-
varlock audit --path .env.prod # Audit using a specific env entry point
48-
varlock audit ./src ./lib # Only scan specific directories
49-
varlock audit --ignore vendor # Exclude a directory from scanning
50-
varlock audit -i vendor -i generated # Exclude multiple directories
51-
`.trim(),
52-
});
19+
export { commandSpec };
5320

5421
function formatReference(cwd: string, ref: EnvVarReference): string {
5522
const relPath = path.relative(cwd, ref.filePath);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { define, lazy } from 'gunshi';
2+
3+
// --- `varlock cache status` -------------------------------------------------
4+
5+
export const statusCommandSpec = define({
6+
name: 'status',
7+
description: 'Print a cache status summary (non-interactive)',
8+
});
9+
10+
// --- `varlock cache clear` --------------------------------------------------
11+
12+
export const clearCommandSpec = define({
13+
name: 'clear',
14+
description: 'Clear cache entries',
15+
args: {
16+
plugin: {
17+
type: 'string',
18+
description: 'Clear cache for a specific plugin only',
19+
},
20+
yes: {
21+
type: 'boolean',
22+
short: 'y',
23+
description: 'Skip confirmation prompts (required when non-interactive)',
24+
},
25+
},
26+
examples: `
27+
varlock cache clear --yes # Clear all entries (no prompt)
28+
varlock cache clear --plugin 1password --yes # Clear cache for a specific plugin
29+
`.trim(),
30+
});
31+
32+
// --- `varlock cache` (parent) -----------------------------------------------
33+
34+
export const commandSpec = define({
35+
name: 'cache',
36+
description: 'Manage the varlock cache',
37+
subCommands: {
38+
status: lazy(async () => (await import('./cache.command')).statusCommandFn, statusCommandSpec),
39+
clear: lazy(async () => (await import('./cache.command')).clearCommandFn, clearCommandSpec),
40+
},
41+
examples: `
42+
Manage the encrypted value cache used by cache() and plugin authors.
43+
44+
Examples:
45+
varlock cache # Interactive cache browser (or status summary if non-TTY)
46+
varlock cache status # Print cache status summary (non-interactive)
47+
varlock cache clear --yes # Clear all entries (no prompt)
48+
varlock cache clear --plugin 1password --yes # Clear cache for a specific plugin
49+
`.trim(),
50+
});

0 commit comments

Comments
 (0)