Skip to content

Feat: import timings and bundle size analysis #2114

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/late-dancers-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"trigger.dev": patch
"@trigger.dev/core": patch
---

Add import timings and bundle size analysis, the dev command will now warn about slow imports
3 changes: 2 additions & 1 deletion .cursorignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ apps/proxy/
apps/coordinator/
packages/rsc/
.changeset
.zed
.zed
.env
2 changes: 2 additions & 0 deletions packages/cli-v3/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { configureLogoutCommand } from "../commands/logout.js";
import { configureWhoamiCommand } from "../commands/whoami.js";
import { COMMAND_NAME } from "../consts.js";
import { configureListProfilesCommand } from "../commands/list-profiles.js";
import { configureAnalyzeCommand } from "../commands/analyze.js";
import { configureUpdateCommand } from "../commands/update.js";
import { VERSION } from "../version.js";
import { configureDeployCommand } from "../commands/deploy.js";
Expand Down Expand Up @@ -34,6 +35,7 @@ configureListProfilesCommand(program);
configureSwitchProfilesCommand(program);
configureUpdateCommand(program);
configurePreviewCommand(program);
configureAnalyzeCommand(program);
// configureWorkersCommand(program);
// configureTriggerTaskCommand(program);

Expand Down
150 changes: 150 additions & 0 deletions packages/cli-v3/src/commands/analyze.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { Command } from "commander";
import { z } from "zod";
import { CommonCommandOptions, handleTelemetry, wrapCommandAction } from "../cli/common.js";
import { printInitialBanner } from "../utilities/initialBanner.js";
import { logger } from "../utilities/logger.js";
import { printBundleTree, printBundleSummaryTable } from "../utilities/analyze.js";
import path from "node:path";
import fs from "node:fs";
import { readJSONFile } from "../utilities/fileSystem.js";
import { WorkerManifest } from "@trigger.dev/core/v3";
import { tryCatch } from "@trigger.dev/core";

const AnalyzeOptions = CommonCommandOptions.pick({
logLevel: true,
skipTelemetry: true,
}).extend({
verbose: z.boolean().optional().default(false),
});

type AnalyzeOptions = z.infer<typeof AnalyzeOptions>;

export function configureAnalyzeCommand(program: Command) {
return program
.command("analyze [dir]", { hidden: true })
.description("Analyze your build output (bundle size, timings, etc)")
.option(
"-l, --log-level <level>",
"The CLI log level to use (debug, info, log, warn, error, none). This does not effect the log level of your trigger.dev tasks.",
"log"
)
.option("--skip-telemetry", "Opt-out of sending telemetry")
.option("--verbose", "Show detailed bundle tree (do not collapse bundles)")
.action(async (dir, options) => {
await handleTelemetry(async () => {
await analyzeCommand(dir, options);
});
});
}

export async function analyzeCommand(dir: string | undefined, options: unknown) {
return await wrapCommandAction("analyze", AnalyzeOptions, options, async (opts) => {
await printInitialBanner(false);
return await analyze(dir, opts);
});
}

export async function analyze(dir: string | undefined, options: AnalyzeOptions) {
const cwd = process.cwd();
const targetDir = dir ? path.resolve(cwd, dir) : cwd;
const metafilePath = path.join(targetDir, "metafile.json");
const manifestPath = path.join(targetDir, "index.json");

if (!fs.existsSync(metafilePath)) {
logger.error(`Could not find metafile.json in ${targetDir}`);
logger.info("Make sure you have built your project and metafile.json exists.");
return;
}
if (!fs.existsSync(manifestPath)) {
logger.error(`Could not find index.json (worker manifest) in ${targetDir}`);
logger.info("Make sure you have built your project and index.json exists.");
return;
}

const [metafileError, metafile] = await tryCatch(readMetafile(metafilePath));

if (metafileError) {
logger.error(`Failed to parse metafile.json: ${metafileError.message}`);
return;
}

const [manifestError, manifest] = await tryCatch(readManifest(manifestPath));

if (manifestError) {
logger.error(`Failed to parse index.json: ${manifestError.message}`);
return;
}

printBundleTree(manifest, metafile, {
preservePath: true,
collapseBundles: !options.verbose,
});

printBundleSummaryTable(manifest, metafile, {
preservePath: true,
});
}

async function readMetafile(metafilePath: string): Promise<Metafile> {
const json = await readJSONFile(metafilePath);
const metafile = MetafileSchema.parse(json);
return metafile;
}

async function readManifest(manifestPath: string): Promise<WorkerManifest> {
const json = await readJSONFile(manifestPath);
const manifest = WorkerManifest.parse(json);
return manifest;
}

const ImportKind = z.enum([
"entry-point",
"import-statement",
"require-call",
"dynamic-import",
"require-resolve",
"import-rule",
"composes-from",
"url-token",
]);

const ImportSchema = z.object({
path: z.string(),
kind: ImportKind,
external: z.boolean().optional(),
original: z.string().optional(),
with: z.record(z.string()).optional(),
});

const InputSchema = z.object({
bytes: z.number(),
imports: z.array(ImportSchema),
format: z.enum(["cjs", "esm"]).optional(),
with: z.record(z.string()).optional(),
});

const OutputImportSchema = z.object({
path: z.string(),
kind: z.union([ImportKind, z.literal("file-loader")]),
external: z.boolean().optional(),
});

const OutputInputSchema = z.object({
bytesInOutput: z.number(),
});

const OutputSchema = z.object({
bytes: z.number(),
inputs: z.record(z.string(), OutputInputSchema),
imports: z.array(OutputImportSchema),
exports: z.array(z.string()),
entryPoint: z.string().optional(),
cssBundle: z.string().optional(),
});

const MetafileSchema = z.object({
inputs: z.record(z.string(), InputSchema),
outputs: z.record(z.string(), OutputSchema),
});

type Metafile = z.infer<typeof MetafileSchema>;
8 changes: 7 additions & 1 deletion packages/cli-v3/src/commands/dev.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ResolvedConfig } from "@trigger.dev/core/v3/build";
import { Command } from "commander";
import { Command, Option as CommandOption } from "commander";
import { z } from "zod";
import { CommonCommandOptions, commonOptions, wrapCommandAction } from "../cli/common.js";
import { watchConfig } from "../config.js";
Expand All @@ -24,6 +24,8 @@ const DevCommandOptions = CommonCommandOptions.extend({
maxConcurrentRuns: z.coerce.number().optional(),
mcp: z.boolean().default(false),
mcpPort: z.coerce.number().optional().default(3333),
analyze: z.boolean().default(false),
disableWarnings: z.boolean().default(false),
});

export type DevCommandOptions = z.infer<typeof DevCommandOptions>;
Expand Down Expand Up @@ -54,6 +56,10 @@ export function configureDevCommand(program: Command) {
)
.option("--mcp", "Start the MCP server")
.option("--mcp-port", "The port to run the MCP server on", "3333")
.addOption(
new CommandOption("--analyze", "Analyze the build output and import timings").hideHelp()
)
.addOption(new CommandOption("--disable-warnings", "Suppress warnings output").hideHelp())
).action(async (options) => {
wrapCommandAction("dev", DevCommandOptions, options, async (opts) => {
await devCommand(opts);
Expand Down
2 changes: 2 additions & 0 deletions packages/cli-v3/src/dev/backgroundWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { indexWorkerManifest } from "../indexing/indexWorkerManifest.js";
import { prettyError } from "../utilities/cliOutput.js";
import { writeJSONFile } from "../utilities/fileSystem.js";
import { logger } from "../utilities/logger.js";
import type { Metafile } from "esbuild";

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

constructor(
public build: BuildManifest,
public metafile: Metafile,
public params: BackgroundWorkerOptions
) {}

Expand Down
3 changes: 3 additions & 0 deletions packages/cli-v3/src/dev/devOutput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { eventBus, EventBusEventArgs } from "../utilities/eventBus.js";
import { logger } from "../utilities/logger.js";
import { Socket } from "socket.io-client";
import { BundleError } from "../build/bundle.js";
import { analyzeWorker } from "../utilities/analyze.js";

export type DevOutputOptions = {
name: string | undefined;
Expand Down Expand Up @@ -71,6 +72,8 @@ export function startDevOutput(options: DevOutputOptions) {
const backgroundWorkerInitialized = (
...[worker]: EventBusEventArgs<"backgroundWorkerInitialized">
) => {
analyzeWorker(worker, options.args.analyze, options.args.disableWarnings);

const logParts: string[] = [];

const testUrl = `${dashboardUrl}/projects/v3/${config.project}/test?environment=dev`;
Expand Down
17 changes: 14 additions & 3 deletions packages/cli-v3/src/dev/devSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import { clearTmpDirs, EphemeralDirectory, getTmpDir } from "../utilities/tempDi
import { startDevOutput } from "./devOutput.js";
import { startWorkerRuntime } from "./devSupervisor.js";
import { startMcpServer, stopMcpServer } from "./mcpServer.js";
import { aiHelpLink } from "../utilities/cliOutput.js";
import { writeJSONFile } from "../utilities/fileSystem.js";
import { join } from "node:path";

export type DevSessionOptions = {
name: string | undefined;
Expand Down Expand Up @@ -105,12 +106,21 @@ export async function startDevSession({

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

await writeJSONFile(
join(workerDir?.path ?? destination.path, "metafile.json"),
bundle.metafile
);

buildManifest = await notifyExtensionOnBuildComplete(buildContext, buildManifest);

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

await runtime.initializeWorker(buildManifest, workerDir?.remove ?? (() => {}));
await runtime.initializeWorker(
buildManifest,
bundle.metafile,
workerDir?.remove ?? (() => {})
);
} catch (error) {
if (error instanceof Error) {
eventBus.emit("backgroundWorkerIndexingError", buildManifest, error);
Expand Down Expand Up @@ -160,8 +170,9 @@ export async function startDevSession({
}

if (!bundled) {
// First bundle, no need to update bundle
bundled = true;
logger.debug("First bundle, no need to update bundle");
return;
}

const workerDir = getTmpDir(rawConfig.workingDir, "build", keepTmpFiles);
Expand Down
10 changes: 7 additions & 3 deletions packages/cli-v3/src/dev/devSupervisor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { CliApiClient } from "../apiClient.js";
import { DevCommandOptions } from "../commands/dev.js";
import { eventBus } from "../utilities/eventBus.js";
import { logger } from "../utilities/logger.js";
import { sanitizeEnvVars } from "../utilities/sanitizeEnvVars.js";
import { resolveSourceFiles } from "../utilities/sourceFiles.js";
import { BackgroundWorker } from "./backgroundWorker.js";
import { WorkerRuntime } from "./workerRuntime.js";
Expand All @@ -25,6 +24,7 @@ import {
} from "@trigger.dev/core/v3/workers";
import pLimit from "p-limit";
import { resolveLocalEnvVars } from "../utilities/localEnvVars.js";
import type { Metafile } from "esbuild";

export type WorkerRuntimeOptions = {
name: string | undefined;
Expand Down Expand Up @@ -113,7 +113,11 @@ class DevSupervisor implements WorkerRuntime {
}
}

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

const env = await this.#getEnvVars();

const backgroundWorker = new BackgroundWorker(manifest, {
const backgroundWorker = new BackgroundWorker(manifest, metafile, {
env,
cwd: this.options.config.workingDir,
stop,
Expand Down
3 changes: 2 additions & 1 deletion packages/cli-v3/src/dev/workerRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { BuildManifest } from "@trigger.dev/core/v3";
import { ResolvedConfig } from "@trigger.dev/core/v3/build";
import { CliApiClient } from "../apiClient.js";
import { DevCommandOptions } from "../commands/dev.js";
import type { Metafile } from "esbuild";

export interface WorkerRuntime {
shutdown(): Promise<void>;
initializeWorker(manifest: BuildManifest, stop: () => void): Promise<void>;
initializeWorker(manifest: BuildManifest, metafile: Metafile, stop: () => void): Promise<void>;
}

export type WorkerRuntimeOptions = {
Expand Down
6 changes: 4 additions & 2 deletions packages/cli-v3/src/entryPoints/dev-index-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,18 @@ async function bootstrap() {
forceFlushTimeoutMillis: 30_000,
});

const importErrors = await registerResources(buildManifest);
const { importErrors, timings } = await registerResources(buildManifest);

return {
tracingSDK,
config,
buildManifest,
importErrors,
timings,
};
}

const { buildManifest, importErrors, config } = await bootstrap();
const { buildManifest, importErrors, config, timings } = await bootstrap();

let tasks = resourceCatalog.listTaskManifests();

Expand Down Expand Up @@ -158,6 +159,7 @@ await sendMessageInCatalog(
loaderEntryPoint: buildManifest.loaderEntryPoint,
customConditions: buildManifest.customConditions,
initEntryPoint: buildManifest.initEntryPoint,
timings,
},
importErrors,
},
Expand Down
6 changes: 4 additions & 2 deletions packages/cli-v3/src/entryPoints/managed-index-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,18 @@ async function bootstrap() {
forceFlushTimeoutMillis: 30_000,
});

const importErrors = await registerResources(buildManifest);
const { importErrors, timings } = await registerResources(buildManifest);

return {
tracingSDK,
config,
buildManifest,
importErrors,
timings,
};
}

const { buildManifest, importErrors, config } = await bootstrap();
const { buildManifest, importErrors, config, timings } = await bootstrap();

let tasks = resourceCatalog.listTaskManifests();

Expand Down Expand Up @@ -158,6 +159,7 @@ await sendMessageInCatalog(
loaderEntryPoint: buildManifest.loaderEntryPoint,
customConditions: buildManifest.customConditions,
initEntryPoint: buildManifest.initEntryPoint,
timings,
},
importErrors,
},
Expand Down
Loading