Skip to content

Commit 82e7484

Browse files
authored
Feat: import timings and bundle size analysis (#2114)
* cursor should ignore .env files * fix for duplicate builds when starting dev * output metafile in dev * attach metafile to background worker * attach import timings to worker manifest * warn during dev if imports take more than 1s * add analyze command * update disable warnings flag message * add changeset
1 parent 0f66023 commit 82e7484

File tree

15 files changed

+836
-15
lines changed

15 files changed

+836
-15
lines changed

.changeset/late-dancers-smile.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"trigger.dev": patch
3+
"@trigger.dev/core": patch
4+
---
5+
6+
Add import timings and bundle size analysis, the dev command will now warn about slow imports

.cursorignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ apps/proxy/
44
apps/coordinator/
55
packages/rsc/
66
.changeset
7-
.zed
7+
.zed
8+
.env

packages/cli-v3/src/cli/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { configureLogoutCommand } from "../commands/logout.js";
66
import { configureWhoamiCommand } from "../commands/whoami.js";
77
import { COMMAND_NAME } from "../consts.js";
88
import { configureListProfilesCommand } from "../commands/list-profiles.js";
9+
import { configureAnalyzeCommand } from "../commands/analyze.js";
910
import { configureUpdateCommand } from "../commands/update.js";
1011
import { VERSION } from "../version.js";
1112
import { configureDeployCommand } from "../commands/deploy.js";
@@ -34,6 +35,7 @@ configureListProfilesCommand(program);
3435
configureSwitchProfilesCommand(program);
3536
configureUpdateCommand(program);
3637
configurePreviewCommand(program);
38+
configureAnalyzeCommand(program);
3739
// configureWorkersCommand(program);
3840
// configureTriggerTaskCommand(program);
3941

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { Command } from "commander";
2+
import { z } from "zod";
3+
import { CommonCommandOptions, handleTelemetry, wrapCommandAction } from "../cli/common.js";
4+
import { printInitialBanner } from "../utilities/initialBanner.js";
5+
import { logger } from "../utilities/logger.js";
6+
import { printBundleTree, printBundleSummaryTable } from "../utilities/analyze.js";
7+
import path from "node:path";
8+
import fs from "node:fs";
9+
import { readJSONFile } from "../utilities/fileSystem.js";
10+
import { WorkerManifest } from "@trigger.dev/core/v3";
11+
import { tryCatch } from "@trigger.dev/core";
12+
13+
const AnalyzeOptions = CommonCommandOptions.pick({
14+
logLevel: true,
15+
skipTelemetry: true,
16+
}).extend({
17+
verbose: z.boolean().optional().default(false),
18+
});
19+
20+
type AnalyzeOptions = z.infer<typeof AnalyzeOptions>;
21+
22+
export function configureAnalyzeCommand(program: Command) {
23+
return program
24+
.command("analyze [dir]", { hidden: true })
25+
.description("Analyze your build output (bundle size, timings, etc)")
26+
.option(
27+
"-l, --log-level <level>",
28+
"The CLI log level to use (debug, info, log, warn, error, none). This does not effect the log level of your trigger.dev tasks.",
29+
"log"
30+
)
31+
.option("--skip-telemetry", "Opt-out of sending telemetry")
32+
.option("--verbose", "Show detailed bundle tree (do not collapse bundles)")
33+
.action(async (dir, options) => {
34+
await handleTelemetry(async () => {
35+
await analyzeCommand(dir, options);
36+
});
37+
});
38+
}
39+
40+
export async function analyzeCommand(dir: string | undefined, options: unknown) {
41+
return await wrapCommandAction("analyze", AnalyzeOptions, options, async (opts) => {
42+
await printInitialBanner(false);
43+
return await analyze(dir, opts);
44+
});
45+
}
46+
47+
export async function analyze(dir: string | undefined, options: AnalyzeOptions) {
48+
const cwd = process.cwd();
49+
const targetDir = dir ? path.resolve(cwd, dir) : cwd;
50+
const metafilePath = path.join(targetDir, "metafile.json");
51+
const manifestPath = path.join(targetDir, "index.json");
52+
53+
if (!fs.existsSync(metafilePath)) {
54+
logger.error(`Could not find metafile.json in ${targetDir}`);
55+
logger.info("Make sure you have built your project and metafile.json exists.");
56+
return;
57+
}
58+
if (!fs.existsSync(manifestPath)) {
59+
logger.error(`Could not find index.json (worker manifest) in ${targetDir}`);
60+
logger.info("Make sure you have built your project and index.json exists.");
61+
return;
62+
}
63+
64+
const [metafileError, metafile] = await tryCatch(readMetafile(metafilePath));
65+
66+
if (metafileError) {
67+
logger.error(`Failed to parse metafile.json: ${metafileError.message}`);
68+
return;
69+
}
70+
71+
const [manifestError, manifest] = await tryCatch(readManifest(manifestPath));
72+
73+
if (manifestError) {
74+
logger.error(`Failed to parse index.json: ${manifestError.message}`);
75+
return;
76+
}
77+
78+
printBundleTree(manifest, metafile, {
79+
preservePath: true,
80+
collapseBundles: !options.verbose,
81+
});
82+
83+
printBundleSummaryTable(manifest, metafile, {
84+
preservePath: true,
85+
});
86+
}
87+
88+
async function readMetafile(metafilePath: string): Promise<Metafile> {
89+
const json = await readJSONFile(metafilePath);
90+
const metafile = MetafileSchema.parse(json);
91+
return metafile;
92+
}
93+
94+
async function readManifest(manifestPath: string): Promise<WorkerManifest> {
95+
const json = await readJSONFile(manifestPath);
96+
const manifest = WorkerManifest.parse(json);
97+
return manifest;
98+
}
99+
100+
const ImportKind = z.enum([
101+
"entry-point",
102+
"import-statement",
103+
"require-call",
104+
"dynamic-import",
105+
"require-resolve",
106+
"import-rule",
107+
"composes-from",
108+
"url-token",
109+
]);
110+
111+
const ImportSchema = z.object({
112+
path: z.string(),
113+
kind: ImportKind,
114+
external: z.boolean().optional(),
115+
original: z.string().optional(),
116+
with: z.record(z.string()).optional(),
117+
});
118+
119+
const InputSchema = z.object({
120+
bytes: z.number(),
121+
imports: z.array(ImportSchema),
122+
format: z.enum(["cjs", "esm"]).optional(),
123+
with: z.record(z.string()).optional(),
124+
});
125+
126+
const OutputImportSchema = z.object({
127+
path: z.string(),
128+
kind: z.union([ImportKind, z.literal("file-loader")]),
129+
external: z.boolean().optional(),
130+
});
131+
132+
const OutputInputSchema = z.object({
133+
bytesInOutput: z.number(),
134+
});
135+
136+
const OutputSchema = z.object({
137+
bytes: z.number(),
138+
inputs: z.record(z.string(), OutputInputSchema),
139+
imports: z.array(OutputImportSchema),
140+
exports: z.array(z.string()),
141+
entryPoint: z.string().optional(),
142+
cssBundle: z.string().optional(),
143+
});
144+
145+
const MetafileSchema = z.object({
146+
inputs: z.record(z.string(), InputSchema),
147+
outputs: z.record(z.string(), OutputSchema),
148+
});
149+
150+
type Metafile = z.infer<typeof MetafileSchema>;

packages/cli-v3/src/commands/dev.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ResolvedConfig } from "@trigger.dev/core/v3/build";
2-
import { Command } from "commander";
2+
import { Command, Option as CommandOption } from "commander";
33
import { z } from "zod";
44
import { CommonCommandOptions, commonOptions, wrapCommandAction } from "../cli/common.js";
55
import { watchConfig } from "../config.js";
@@ -24,6 +24,8 @@ const DevCommandOptions = CommonCommandOptions.extend({
2424
maxConcurrentRuns: z.coerce.number().optional(),
2525
mcp: z.boolean().default(false),
2626
mcpPort: z.coerce.number().optional().default(3333),
27+
analyze: z.boolean().default(false),
28+
disableWarnings: z.boolean().default(false),
2729
});
2830

2931
export type DevCommandOptions = z.infer<typeof DevCommandOptions>;
@@ -54,6 +56,10 @@ export function configureDevCommand(program: Command) {
5456
)
5557
.option("--mcp", "Start the MCP server")
5658
.option("--mcp-port", "The port to run the MCP server on", "3333")
59+
.addOption(
60+
new CommandOption("--analyze", "Analyze the build output and import timings").hideHelp()
61+
)
62+
.addOption(new CommandOption("--disable-warnings", "Suppress warnings output").hideHelp())
5763
).action(async (options) => {
5864
wrapCommandAction("dev", DevCommandOptions, options, async (opts) => {
5965
await devCommand(opts);

packages/cli-v3/src/dev/backgroundWorker.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { indexWorkerManifest } from "../indexing/indexWorkerManifest.js";
55
import { prettyError } from "../utilities/cliOutput.js";
66
import { writeJSONFile } from "../utilities/fileSystem.js";
77
import { logger } from "../utilities/logger.js";
8+
import type { Metafile } from "esbuild";
89

910
export type BackgroundWorkerOptions = {
1011
env: Record<string, string>;
@@ -19,6 +20,7 @@ export class BackgroundWorker {
1920

2021
constructor(
2122
public build: BuildManifest,
23+
public metafile: Metafile,
2224
public params: BackgroundWorkerOptions
2325
) {}
2426

packages/cli-v3/src/dev/devOutput.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { eventBus, EventBusEventArgs } from "../utilities/eventBus.js";
2626
import { logger } from "../utilities/logger.js";
2727
import { Socket } from "socket.io-client";
2828
import { BundleError } from "../build/bundle.js";
29+
import { analyzeWorker } from "../utilities/analyze.js";
2930

3031
export type DevOutputOptions = {
3132
name: string | undefined;
@@ -71,6 +72,8 @@ export function startDevOutput(options: DevOutputOptions) {
7172
const backgroundWorkerInitialized = (
7273
...[worker]: EventBusEventArgs<"backgroundWorkerInitialized">
7374
) => {
75+
analyzeWorker(worker, options.args.analyze, options.args.disableWarnings);
76+
7477
const logParts: string[] = [];
7578

7679
const testUrl = `${dashboardUrl}/projects/v3/${config.project}/test?environment=dev`;

packages/cli-v3/src/dev/devSession.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ import { clearTmpDirs, EphemeralDirectory, getTmpDir } from "../utilities/tempDi
2424
import { startDevOutput } from "./devOutput.js";
2525
import { startWorkerRuntime } from "./devSupervisor.js";
2626
import { startMcpServer, stopMcpServer } from "./mcpServer.js";
27-
import { aiHelpLink } from "../utilities/cliOutput.js";
27+
import { writeJSONFile } from "../utilities/fileSystem.js";
28+
import { join } from "node:path";
2829

2930
export type DevSessionOptions = {
3031
name: string | undefined;
@@ -105,12 +106,21 @@ export async function startDevSession({
105106

106107
logger.debug("Created build manifest from bundle", { buildManifest });
107108

109+
await writeJSONFile(
110+
join(workerDir?.path ?? destination.path, "metafile.json"),
111+
bundle.metafile
112+
);
113+
108114
buildManifest = await notifyExtensionOnBuildComplete(buildContext, buildManifest);
109115

110116
try {
111117
logger.debug("Updated bundle", { bundle, buildManifest });
112118

113-
await runtime.initializeWorker(buildManifest, workerDir?.remove ?? (() => {}));
119+
await runtime.initializeWorker(
120+
buildManifest,
121+
bundle.metafile,
122+
workerDir?.remove ?? (() => {})
123+
);
114124
} catch (error) {
115125
if (error instanceof Error) {
116126
eventBus.emit("backgroundWorkerIndexingError", buildManifest, error);
@@ -160,8 +170,9 @@ export async function startDevSession({
160170
}
161171

162172
if (!bundled) {
163-
// First bundle, no need to update bundle
164173
bundled = true;
174+
logger.debug("First bundle, no need to update bundle");
175+
return;
165176
}
166177

167178
const workerDir = getTmpDir(rawConfig.workingDir, "build", keepTmpFiles);

packages/cli-v3/src/dev/devSupervisor.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { CliApiClient } from "../apiClient.js";
1212
import { DevCommandOptions } from "../commands/dev.js";
1313
import { eventBus } from "../utilities/eventBus.js";
1414
import { logger } from "../utilities/logger.js";
15-
import { sanitizeEnvVars } from "../utilities/sanitizeEnvVars.js";
1615
import { resolveSourceFiles } from "../utilities/sourceFiles.js";
1716
import { BackgroundWorker } from "./backgroundWorker.js";
1817
import { WorkerRuntime } from "./workerRuntime.js";
@@ -25,6 +24,7 @@ import {
2524
} from "@trigger.dev/core/v3/workers";
2625
import pLimit from "p-limit";
2726
import { resolveLocalEnvVars } from "../utilities/localEnvVars.js";
27+
import type { Metafile } from "esbuild";
2828

2929
export type WorkerRuntimeOptions = {
3030
name: string | undefined;
@@ -113,7 +113,11 @@ class DevSupervisor implements WorkerRuntime {
113113
}
114114
}
115115

116-
async initializeWorker(manifest: BuildManifest, stop: () => void): Promise<void> {
116+
async initializeWorker(
117+
manifest: BuildManifest,
118+
metafile: Metafile,
119+
stop: () => void
120+
): Promise<void> {
117121
if (this.lastManifest && this.lastManifest.contentHash === manifest.contentHash) {
118122
logger.debug("worker skipped", { lastManifestContentHash: this.lastManifest?.contentHash });
119123
eventBus.emit("workerSkipped");
@@ -123,7 +127,7 @@ class DevSupervisor implements WorkerRuntime {
123127

124128
const env = await this.#getEnvVars();
125129

126-
const backgroundWorker = new BackgroundWorker(manifest, {
130+
const backgroundWorker = new BackgroundWorker(manifest, metafile, {
127131
env,
128132
cwd: this.options.config.workingDir,
129133
stop,

packages/cli-v3/src/dev/workerRuntime.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import { BuildManifest } from "@trigger.dev/core/v3";
22
import { ResolvedConfig } from "@trigger.dev/core/v3/build";
33
import { CliApiClient } from "../apiClient.js";
44
import { DevCommandOptions } from "../commands/dev.js";
5+
import type { Metafile } from "esbuild";
56

67
export interface WorkerRuntime {
78
shutdown(): Promise<void>;
8-
initializeWorker(manifest: BuildManifest, stop: () => void): Promise<void>;
9+
initializeWorker(manifest: BuildManifest, metafile: Metafile, stop: () => void): Promise<void>;
910
}
1011

1112
export type WorkerRuntimeOptions = {

packages/cli-v3/src/entryPoints/dev-index-worker.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,17 +86,18 @@ async function bootstrap() {
8686
forceFlushTimeoutMillis: 30_000,
8787
});
8888

89-
const importErrors = await registerResources(buildManifest);
89+
const { importErrors, timings } = await registerResources(buildManifest);
9090

9191
return {
9292
tracingSDK,
9393
config,
9494
buildManifest,
9595
importErrors,
96+
timings,
9697
};
9798
}
9899

99-
const { buildManifest, importErrors, config } = await bootstrap();
100+
const { buildManifest, importErrors, config, timings } = await bootstrap();
100101

101102
let tasks = resourceCatalog.listTaskManifests();
102103

@@ -158,6 +159,7 @@ await sendMessageInCatalog(
158159
loaderEntryPoint: buildManifest.loaderEntryPoint,
159160
customConditions: buildManifest.customConditions,
160161
initEntryPoint: buildManifest.initEntryPoint,
162+
timings,
161163
},
162164
importErrors,
163165
},

packages/cli-v3/src/entryPoints/managed-index-worker.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,17 +86,18 @@ async function bootstrap() {
8686
forceFlushTimeoutMillis: 30_000,
8787
});
8888

89-
const importErrors = await registerResources(buildManifest);
89+
const { importErrors, timings } = await registerResources(buildManifest);
9090

9191
return {
9292
tracingSDK,
9393
config,
9494
buildManifest,
9595
importErrors,
96+
timings,
9697
};
9798
}
9899

99-
const { buildManifest, importErrors, config } = await bootstrap();
100+
const { buildManifest, importErrors, config, timings } = await bootstrap();
100101

101102
let tasks = resourceCatalog.listTaskManifests();
102103

@@ -158,6 +159,7 @@ await sendMessageInCatalog(
158159
loaderEntryPoint: buildManifest.loaderEntryPoint,
159160
customConditions: buildManifest.customConditions,
160161
initEntryPoint: buildManifest.initEntryPoint,
162+
timings,
161163
},
162164
importErrors,
163165
},

0 commit comments

Comments
 (0)