Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2099cd1
feat: add @alloy-js/trace-cli package for offline trace database anal…
bterlson Feb 17, 2026
99d39ff
Add functional tests for trace-cli commands
bterlson Feb 17, 2026
d1bce0b
Add README and fix package.json for trace-cli
bterlson Feb 17, 2026
b2c6d30
Print full stack trace for render errors instead of indented excerpt
bterlson Feb 17, 2026
4b005ad
Format component stacks as stack traces with source locations
bterlson Feb 17, 2026
8ef03e4
Unify component stack formatting between errors and file search
bterlson Feb 17, 2026
a7dc981
Search file content for cross-node text matches
bterlson Feb 17, 2026
4945565
Only show most specific text nodes in file search
bterlson Feb 17, 2026
94f13b5
Simplify file search: find exact text, show deepest node's stack
bterlson Feb 17, 2026
340b7aa
fix: use diff-match-patch for accurate file search text node mapping
bterlson Feb 17, 2026
8421150
feat: show render node IDs in stacks, clean up search output
bterlson Feb 17, 2026
05e2cc9
feat: filter library frames in stacks, use relative paths
bterlson Feb 17, 2026
cadb686
feat: add ANSI coloring to stacks, fix sourceless frame filtering
bterlson Feb 17, 2026
3632be3
fix: use import.meta.url for component source paths, preserve source …
bterlson Feb 17, 2026
6e34c90
feat: log debug build status, source maps, and trace DB path on load
bterlson Feb 17, 2026
1c666fc
fix: rename 'framework frames' to 'external frames' in stack output
bterlson Feb 17, 2026
95327fc
Ship dev and prod builds
bterlson Feb 18, 2026
c231575
fix: hide sourceless frames by default, show with --all-frames
bterlson Feb 18, 2026
21163f7
Adopt --with-dev builds
bterlson Feb 18, 2026
52a4e9f
chore: add changelog entry for @alloy-js/trace-cli
bterlson Feb 18, 2026
a31801c
changes
bterlson Feb 18, 2026
bc9b004
change
bterlson Feb 18, 2026
0ecee79
chore: format files
bterlson Feb 18, 2026
d3834d5
chore: disable no-console for trace-cli, fix prefer-const lint
bterlson Feb 18, 2026
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
7 changes: 7 additions & 0 deletions .chronus/changes/dual-dev-prod-builds-2026-2-18.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@alloy-js/cli"
---

Add `--with-dev` flag to produce both production (`dist/`) and development (`dist/dev/`) builds. Dev builds include source info from babel transforms for improved debugging. `--dev` produces only a dev build to `dist/`, and `--watch` now defaults to dev mode. Package exports use `"development"` condition so consumers can opt into dev builds via `node --conditions=development`.
16 changes: 16 additions & 0 deletions .chronus/changes/feat-trace-cli-2026-1-17-16-59-56.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
changeKind: feature
packages:
- "@alloy-js/core"
- "@alloy-js/create"
- "@alloy-js/csharp"
- "@alloy-js/go"
- "@alloy-js/java"
- "@alloy-js/json"
- "@alloy-js/markdown"
- "@alloy-js/msbuild"
- "@alloy-js/python"
- "@alloy-js/typescript"
---

Ship dev sources in package for debugging. Use node's --condition="development" flag to use this build.
7 changes: 7 additions & 0 deletions .chronus/changes/feat-trace-cli-2026-1-17-17-6-34.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@alloy-js/babel-plugin-jsx-dom-expressions"
---

Pass import.meta.url to createComponent for dev builds.
7 changes: 7 additions & 0 deletions .chronus/changes/trace-cli-2026-2-18.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@alloy-js/trace-cli"
---

Add `@alloy-js/trace-cli` for querying Alloy trace databases from the command line, enabling LLMs and developers to explore render trees, component stacks, effects, and output files to understand what happened during a render.
9 changes: 9 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ export default tsEslint.config(
eqeqeq: ["warn", "always", { null: "ignore" }],
},
},
{
/**
* CLI packages — console.log is the primary output mechanism.
*/
files: ["packages/trace-cli/**/*.ts"],
rules: {
"no-console": "off",
},
},
{
/**
* Test files specific rules
Expand Down
8 changes: 7 additions & 1 deletion packages/babel-plugin-jsx-dom-expressions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1205,8 +1205,14 @@ function transformComponent(path) {
if (config.generate !== "ssr" && config.addSourceInfo) {
const loc = path.node.loc;
if (loc && loc.start) {
// Use import.meta.url so the path resolves to the installed location at
// runtime rather than being a hardcoded absolute path from the build machine.
const importMetaUrl = t__namespace.memberExpression(
t__namespace.metaProperty(t__namespace.identifier("import"), t__namespace.identifier("meta")),
t__namespace.identifier("url"),
);
const sourceInfo = t__namespace.objectExpression([
t__namespace.objectProperty(t__namespace.identifier("fileName"), t__namespace.stringLiteral(path.hub.file.opts.filename || "unknown")),
t__namespace.objectProperty(t__namespace.identifier("fileName"), importMetaUrl),
t__namespace.objectProperty(t__namespace.identifier("lineNumber"), t__namespace.numericLiteral(loc.start.line)),
t__namespace.objectProperty(t__namespace.identifier("columnNumber"), t__namespace.numericLiteral(loc.start.column + 1))
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,14 @@ export default function transformComponent(path) {
if (config.generate !== "ssr" && config.addSourceInfo) {
const loc = path.node.loc;
if (loc && loc.start) {
// Use import.meta.url so the path resolves to the installed location at
// runtime rather than being a hardcoded absolute path from the build machine.
const importMetaUrl = t.memberExpression(
t.metaProperty(t.identifier("import"), t.identifier("meta")),
t.identifier("url"),
);
const sourceInfo = t.objectExpression([
t.objectProperty(t.identifier("fileName"), t.stringLiteral(path.hub.file.opts.filename || "unknown")),
t.objectProperty(t.identifier("fileName"), importMetaUrl),
t.objectProperty(t.identifier("lineNumber"), t.numericLiteral(loc.start.line)),
t.objectProperty(t.identifier("columnNumber"), t.numericLiteral(loc.start.column + 1))
]);
Expand Down
31 changes: 25 additions & 6 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { parseArgs } from "node:util";
import { join } from "pathe";
import pc from "picocolors";
import ts from "typescript";
import { buildAllFiles } from "./babel.js";
Expand All @@ -20,6 +21,9 @@ const args = parseArgs({
"source-info": {
type: "boolean",
},
"with-dev": {
type: "boolean",
},
},
});

Expand Down Expand Up @@ -49,10 +53,26 @@ async function build() {
});
const emitResult = program.emit();
const start = new Date().getTime();
await buildAllFiles(opts.fileNames, opts.rootDir, opts.outDir, {
sourceMaps: opts.options.sourceMap,
addSourceInfo,
});

if (args.values["with-dev"]) {
// Dual build: prod → dist/, dev → dist/dev/
await buildAllFiles(opts.fileNames, opts.rootDir, opts.outDir, {
sourceMaps: opts.options.sourceMap,
addSourceInfo: false,
});
const devOutDir = join(opts.outDir, "dev");
await buildAllFiles(opts.fileNames, opts.rootDir, devOutDir, {
sourceMaps: opts.options.sourceMap,
addSourceInfo: true,
});
} else {
// Single build: --dev produces dev build, default produces prod build
await buildAllFiles(opts.fileNames, opts.rootDir, opts.outDir, {
sourceMaps: opts.options.sourceMap,
addSourceInfo,
});
}

const allDiagnostics = ts
.getPreEmitDiagnostics(program as any)
.concat(emitResult.diagnostics);
Expand All @@ -76,7 +96,6 @@ async function build() {
}

function watchMain() {
const { addSourceInfo } = resolveBuildSettings();
const opts = getParseCommandLine();

const createProgram = ts.createSemanticDiagnosticsBuilderProgram;
Expand All @@ -103,7 +122,7 @@ function watchMain() {
opts.outDir,
{
sourceMaps: opts.options.sourceMap,
addSourceInfo,
addSourceInfo: true,
},
);
} catch (e) {
Expand Down
20 changes: 13 additions & 7 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,39 @@
"exports": {
".": {
"browser": "./dist/src/index.browser.js",
"development": "./src/index.ts",
"source": "./src/index.ts",
"development": "./dist/dev/src/index.js",
"import": "./dist/src/index.js"
},
"./jsx-runtime": {
"types": "./dist/src/jsx-runtime.d.ts",
"development": "./src/jsx-runtime.ts",
"source": "./src/jsx-runtime.ts",
"development": "./dist/dev/src/jsx-runtime.js",
"import": "./dist/src/jsx-runtime.js"
},
"./testing": {
"development": "./testing/index.ts",
"source": "./testing/index.ts",
"development": "./dist/dev/testing/index.js",
"import": "./dist/testing/index.js"
},
"./testing/matchers": {
"types": "./testing/vitest.d.ts"
},
"./stc": {
"development": "./src/components/stc/index.ts",
"source": "./src/components/stc/index.ts",
"development": "./dist/dev/src/components/stc/index.js",
"import": "./dist/src/components/stc/index.js"
},
"./components": {
"development": "./src/components/index.ts",
"source": "./src/components/index.ts",
"development": "./dist/dev/src/components/index.js",
"import": "./dist/src/components/index.js"
},
"./devtools": {
"types": "./dist/src/devtools-entry.d.ts",
"browser": "./dist/src/devtools-entry.browser.js",
"development": "./src/devtools-entry.ts",
"source": "./src/devtools-entry.ts",
"development": "./dist/dev/src/devtools-entry.js",
"import": "./dist/src/devtools-entry.js"
}
},
Expand All @@ -50,7 +56,7 @@
},
"scripts": {
"generate-docs": "api-extractor run",
"build": "alloy build && pnpm run generate-docs",
"build": "alloy build --with-dev && pnpm run generate-docs",
"clean": "rimraf dist/ .temp/",
"test": "vitest run",
"test:watch": "vitest -w",
Expand Down
84 changes: 2 additions & 82 deletions packages/core/src/debug/effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getReactiveCreationLocation,
nextReactiveId,
} from "../reactivity.js";
import { loadSourceMapSupport, resolveSourceMap } from "./source-map.js";
import { insertEdge, insertEffect, insertRef } from "./trace-writer.js";
import {
isDebugEnabled,
Expand Down Expand Up @@ -112,68 +113,6 @@ const VUE_REACTIVITY_MARKERS = [
// Fast source location capture using V8 structured CallSite API
// ─────────────────────────────────────────────────────────────────────────────

// Lazily loaded findSourceMap from node:module
let findSourceMap:
| ((path: string) =>
| {
findEntry: (
line: number,
col: number,
) =>
| {
originalSource: string;
originalLine: number;
originalColumn: number;
}
| undefined;
}
| undefined)
| undefined;
let findSourceMapLoaded = false;
let realpathSync: ((path: string) => string) | undefined;
// Cache realpath lookups to avoid repeated fs calls
const realpathCache = new Map<string, string>();

function loadFindSourceMap() {
if (findSourceMapLoaded) return;
findSourceMapLoaded = true;
// process.getBuiltinModule works in both ESM and CJS contexts
try {
const mod = process.getBuiltinModule?.("node:module") as
| typeof import("node:module")
| undefined;
if (mod && typeof mod.findSourceMap === "function") {
findSourceMap = mod.findSourceMap as typeof findSourceMap;
}
} catch {
// not available
}
try {
const fs = process.getBuiltinModule?.("node:fs") as
| typeof import("node:fs")
| undefined;
if (fs) {
realpathSync = fs.realpathSync;
}
} catch {
// not available
}
}

function getRealPath(fileName: string): string {
if (!realpathSync) return fileName;
let real = realpathCache.get(fileName);
if (real === undefined) {
try {
real = realpathSync(fileName);
} catch {
real = fileName;
}
realpathCache.set(fileName, real);
}
return real;
}

function isSkipFile(fileName: string): boolean {
for (const skip of STACK_SKIP) {
if (fileName.includes(skip)) return true;
Expand All @@ -188,25 +127,6 @@ function isVueReactivityFile(fileName: string): boolean {
return false;
}

function resolveSourceMap(
fileName: string,
line: number,
col: number,
): { fileName: string; line: number; col: number } {
if (!findSourceMap) return { fileName, line, col };
// pnpm uses symlinks; findSourceMap only matches the real path
const real = getRealPath(fileName);
const map = findSourceMap(real);
if (!map) return { fileName, line, col };
const entry = map.findEntry(line - 1, col - 1);
if (!entry) return { fileName, line, col };
return {
fileName: entry.originalSource,
line: entry.originalLine + 1,
col: entry.originalColumn + 1,
};
}

// V8 structured stack capture — avoids string formatting entirely
const structuredPrepare = (
_err: Error,
Expand All @@ -227,7 +147,7 @@ export function captureSourceLocation(
skipReactives = true,
): SourceLocation | undefined {
if (!isDebugEnabled()) return undefined;
loadFindSourceMap();
loadSourceMapSupport();

const sites = captureCallSites();

Expand Down
Loading