@alloy-js/core 0.23.0-dev.15 → 0.23.0-dev.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dev/src/debug/diagnostics-broadcast.test.js +100 -0
- package/dist/dev/src/debug/diagnostics-broadcast.test.js.map +1 -0
- package/dist/dev/src/debug/trace-writer.js +18 -2
- package/dist/dev/src/debug/trace-writer.js.map +1 -1
- package/dist/dev/src/diagnostics.js +27 -19
- package/dist/dev/src/diagnostics.js.map +1 -1
- package/dist/dev/testing/vitest.d.js +12 -0
- package/dist/dev/testing/vitest.d.js.map +1 -1
- package/dist/src/debug/diagnostics-broadcast.test.d.ts +2 -0
- package/dist/src/debug/diagnostics-broadcast.test.d.ts.map +1 -0
- package/dist/src/debug/diagnostics-broadcast.test.js +100 -0
- package/dist/src/debug/diagnostics-broadcast.test.js.map +1 -0
- package/dist/src/debug/trace-writer.d.ts +2 -1
- package/dist/src/debug/trace-writer.d.ts.map +1 -1
- package/dist/src/debug/trace-writer.js +18 -2
- package/dist/src/debug/trace-writer.js.map +1 -1
- package/dist/src/diagnostics.d.ts +1 -1
- package/dist/src/diagnostics.d.ts.map +1 -1
- package/dist/src/diagnostics.js +27 -19
- package/dist/src/diagnostics.js.map +1 -1
- package/dist/testing/vitest.d.js +12 -0
- package/dist/testing/vitest.d.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/debug/diagnostics-broadcast.test.tsx +98 -0
- package/src/debug/trace-writer.ts +18 -3
- package/src/diagnostics.ts +44 -31
- package/testing/vitest.d.ts +21 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alloy-js/core",
|
|
3
|
-
"version": "0.23.0-dev.
|
|
3
|
+
"version": "0.23.0-dev.17",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"ws": "^8.19.0"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
|
-
"@alloy-js/cli": "~0.22.0 || >= 0.23.0-dev.
|
|
69
|
+
"@alloy-js/cli": "~0.22.0 || >= 0.23.0-dev.6",
|
|
70
70
|
"@alloy-js/rollup-plugin": "~0.1.0 || >= 0.1.1-dev.2",
|
|
71
71
|
"@microsoft/api-extractor": "~7.52.8",
|
|
72
72
|
"@rollup/plugin-typescript": "^12.1.2",
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, expect, it } from "vitest";
|
|
5
|
+
import { DiagnosticsCollector } from "../diagnostics.js";
|
|
6
|
+
import { closeTrace, initTrace, setChangeListener } from "./trace-writer.js";
|
|
7
|
+
|
|
8
|
+
let tmpDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
tmpDir = mkdtempSync(join(tmpdir(), "alloy-diag-test-"));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
setChangeListener(null);
|
|
16
|
+
closeTrace();
|
|
17
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("does not call insertDiagnostic when tracing is disabled", () => {
|
|
21
|
+
let events = 0;
|
|
22
|
+
setChangeListener(() => {
|
|
23
|
+
events++;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const collector = new DiagnosticsCollector();
|
|
27
|
+
for (let i = 0; i < 100; i++) {
|
|
28
|
+
collector.emit({ message: `diag ${i}`, severity: "warning" });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// No trace DB => no change events should fire.
|
|
32
|
+
expect(events).toBe(0);
|
|
33
|
+
expect(collector.getDiagnostics()).toHaveLength(100);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("emits one added event per diagnostic (linear, not quadratic)", async () => {
|
|
37
|
+
await initTrace(join(tmpDir, "trace.db"));
|
|
38
|
+
const events: Array<{ channel: string; action: string }> = [];
|
|
39
|
+
setChangeListener((event) => {
|
|
40
|
+
events.push({ channel: event.channel, action: event.action });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const collector = new DiagnosticsCollector();
|
|
44
|
+
const N = 50;
|
|
45
|
+
for (let i = 0; i < N; i++) {
|
|
46
|
+
collector.emit({ message: `diag ${i}`, severity: "warning" });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const added = events.filter(
|
|
50
|
+
(e) => e.channel === "diagnostics" && e.action === "added",
|
|
51
|
+
);
|
|
52
|
+
expect(added).toHaveLength(N); // was previously N*(N+1)/2 due to re-broadcast
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("emits one removed event per dismissal", async () => {
|
|
56
|
+
await initTrace(join(tmpDir, "trace.db"));
|
|
57
|
+
const events: Array<{ channel: string; action: string }> = [];
|
|
58
|
+
setChangeListener((event) => {
|
|
59
|
+
events.push({ channel: event.channel, action: event.action });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const collector = new DiagnosticsCollector();
|
|
63
|
+
const handles = [];
|
|
64
|
+
for (let i = 0; i < 10; i++) {
|
|
65
|
+
handles.push(collector.emit({ message: `diag ${i}`, severity: "warning" }));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const h of handles) h.dismiss();
|
|
69
|
+
|
|
70
|
+
const added = events.filter(
|
|
71
|
+
(e) => e.channel === "diagnostics" && e.action === "added",
|
|
72
|
+
);
|
|
73
|
+
const removed = events.filter(
|
|
74
|
+
(e) => e.channel === "diagnostics" && e.action === "removed",
|
|
75
|
+
);
|
|
76
|
+
expect(added).toHaveLength(10);
|
|
77
|
+
expect(removed).toHaveLength(10);
|
|
78
|
+
expect(collector.getDiagnostics()).toHaveLength(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("dismiss is idempotent and only fires one removal", async () => {
|
|
82
|
+
await initTrace(join(tmpDir, "trace.db"));
|
|
83
|
+
const events: Array<{ channel: string; action: string }> = [];
|
|
84
|
+
setChangeListener((event) => {
|
|
85
|
+
events.push({ channel: event.channel, action: event.action });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const collector = new DiagnosticsCollector();
|
|
89
|
+
const h = collector.emit({ message: "once", severity: "warning" });
|
|
90
|
+
h.dismiss();
|
|
91
|
+
h.dismiss();
|
|
92
|
+
h.dismiss();
|
|
93
|
+
|
|
94
|
+
const removed = events.filter(
|
|
95
|
+
(e) => e.channel === "diagnostics" && e.action === "removed",
|
|
96
|
+
);
|
|
97
|
+
expect(removed).toHaveLength(1);
|
|
98
|
+
});
|
|
@@ -801,6 +801,8 @@ export function insertRenderError(
|
|
|
801
801
|
});
|
|
802
802
|
}
|
|
803
803
|
|
|
804
|
+
let stmtDeleteDiagnostic: StatementSync | undefined;
|
|
805
|
+
|
|
804
806
|
export function insertDiagnostic(
|
|
805
807
|
message: string,
|
|
806
808
|
severity: string | undefined,
|
|
@@ -808,10 +810,10 @@ export function insertDiagnostic(
|
|
|
808
810
|
sourceLine: number | undefined,
|
|
809
811
|
sourceCol: number | undefined,
|
|
810
812
|
componentStack: string | undefined,
|
|
811
|
-
):
|
|
812
|
-
if (!db) return;
|
|
813
|
+
): number | undefined {
|
|
814
|
+
if (!db) return undefined;
|
|
813
815
|
const s = nextSeq();
|
|
814
|
-
stmtInsertDiagnostic.run(
|
|
816
|
+
const info = stmtInsertDiagnostic.run(
|
|
815
817
|
message,
|
|
816
818
|
severity ?? null,
|
|
817
819
|
sourceFile ?? null,
|
|
@@ -820,7 +822,9 @@ export function insertDiagnostic(
|
|
|
820
822
|
componentStack ?? null,
|
|
821
823
|
s,
|
|
822
824
|
);
|
|
825
|
+
const id = Number(info.lastInsertRowid);
|
|
823
826
|
notifyChange("diagnostics", "added", {
|
|
827
|
+
id,
|
|
824
828
|
message,
|
|
825
829
|
severity: severity ?? null,
|
|
826
830
|
source_file: sourceFile ?? null,
|
|
@@ -829,6 +833,16 @@ export function insertDiagnostic(
|
|
|
829
833
|
component_stack: componentStack ?? null,
|
|
830
834
|
seq: s,
|
|
831
835
|
});
|
|
836
|
+
return id;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
export function deleteDiagnostic(id: number): void {
|
|
840
|
+
if (!db) return;
|
|
841
|
+
if (!stmtDeleteDiagnostic) {
|
|
842
|
+
stmtDeleteDiagnostic = db.prepare(`DELETE FROM diagnostics WHERE id = ?`);
|
|
843
|
+
}
|
|
844
|
+
stmtDeleteDiagnostic.run(id);
|
|
845
|
+
notifyChange("diagnostics", "removed", { id, seq: nextSeq() });
|
|
832
846
|
}
|
|
833
847
|
|
|
834
848
|
/**
|
|
@@ -946,6 +960,7 @@ export function commitTransaction(): void {
|
|
|
946
960
|
export function closeTrace(): void {
|
|
947
961
|
db?.close();
|
|
948
962
|
db = null;
|
|
963
|
+
stmtDeleteDiagnostic = undefined;
|
|
949
964
|
}
|
|
950
965
|
|
|
951
966
|
export function resetTrace(): void {
|
package/src/diagnostics.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { getRenderNodeId } from "./debug/index.js";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
deleteDiagnostic,
|
|
4
|
+
insertDiagnostic,
|
|
5
|
+
isTraceEnabled,
|
|
6
|
+
} from "./debug/trace-writer.js";
|
|
3
7
|
import { getContext } from "./reactivity.js";
|
|
4
8
|
import { getRenderStackSnapshot } from "./render-stack.js";
|
|
5
9
|
import type { SourceLocation } from "./runtime/component.js";
|
|
@@ -31,22 +35,31 @@ export interface DiagnosticHandle {
|
|
|
31
35
|
dismiss(): void;
|
|
32
36
|
}
|
|
33
37
|
|
|
38
|
+
function buildComponentStack(): DiagnosticStackEntry[] {
|
|
39
|
+
return getRenderStackSnapshot().map((entry) => ({
|
|
40
|
+
name: entry.displayName,
|
|
41
|
+
renderNodeId:
|
|
42
|
+
entry.context?.meta?.renderNode ?
|
|
43
|
+
getRenderNodeId(entry.context.meta.renderNode)
|
|
44
|
+
: undefined,
|
|
45
|
+
source: entry.source,
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
|
|
34
49
|
export class DiagnosticsCollector {
|
|
35
50
|
private entries = new Map<string, Diagnostic>();
|
|
36
51
|
private order: string[] = [];
|
|
52
|
+
private traceRowIds = new Map<string, number>();
|
|
37
53
|
|
|
38
54
|
emit(input: DiagnosticInput): DiagnosticHandle {
|
|
55
|
+
const traceEnabled = isTraceEnabled();
|
|
56
|
+
// Component stack is only required when a trace consumer will read it,
|
|
57
|
+
// or when the caller explicitly provided one. Building it walks the full
|
|
58
|
+
// render stack, which is expensive on hot paths.
|
|
39
59
|
const componentStack =
|
|
40
60
|
input.componentStack ??
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
renderNodeId:
|
|
44
|
-
entry.context?.meta?.renderNode ?
|
|
45
|
-
getRenderNodeId(entry.context.meta.renderNode)
|
|
46
|
-
: undefined,
|
|
47
|
-
source: entry.source,
|
|
48
|
-
}));
|
|
49
|
-
const source = input.source ?? componentStack.at(-1)?.source ?? undefined;
|
|
61
|
+
(traceEnabled ? buildComponentStack() : undefined);
|
|
62
|
+
const source = input.source ?? componentStack?.at(-1)?.source ?? undefined;
|
|
50
63
|
const id = `diag-${this.order.length + 1}-${Date.now()}`;
|
|
51
64
|
const diagnostic: Diagnostic = {
|
|
52
65
|
id,
|
|
@@ -58,14 +71,30 @@ export class DiagnosticsCollector {
|
|
|
58
71
|
this.entries.set(id, diagnostic);
|
|
59
72
|
this.order.push(id);
|
|
60
73
|
|
|
61
|
-
|
|
62
|
-
|
|
74
|
+
if (traceEnabled) {
|
|
75
|
+
const rowId = insertDiagnostic(
|
|
76
|
+
diagnostic.message,
|
|
77
|
+
diagnostic.severity,
|
|
78
|
+
diagnostic.source?.fileName,
|
|
79
|
+
diagnostic.source?.lineNumber,
|
|
80
|
+
diagnostic.source?.columnNumber,
|
|
81
|
+
diagnostic.componentStack ?
|
|
82
|
+
JSON.stringify(diagnostic.componentStack)
|
|
83
|
+
: undefined,
|
|
84
|
+
);
|
|
85
|
+
if (rowId !== undefined) {
|
|
86
|
+
this.traceRowIds.set(id, rowId);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
63
89
|
|
|
64
90
|
return {
|
|
65
91
|
dismiss: () => {
|
|
66
|
-
this.entries.delete(id);
|
|
67
|
-
|
|
68
|
-
|
|
92
|
+
if (!this.entries.delete(id)) return;
|
|
93
|
+
const rowId = this.traceRowIds.get(id);
|
|
94
|
+
if (rowId !== undefined) {
|
|
95
|
+
this.traceRowIds.delete(id);
|
|
96
|
+
if (isTraceEnabled()) deleteDiagnostic(rowId);
|
|
97
|
+
}
|
|
69
98
|
},
|
|
70
99
|
};
|
|
71
100
|
}
|
|
@@ -75,22 +104,6 @@ export class DiagnosticsCollector {
|
|
|
75
104
|
.map((id) => this.entries.get(id))
|
|
76
105
|
.filter((entry): entry is Diagnostic => Boolean(entry));
|
|
77
106
|
}
|
|
78
|
-
|
|
79
|
-
private broadcast() {
|
|
80
|
-
const diagnostics = this.getDiagnostics();
|
|
81
|
-
for (const diagnostic of diagnostics) {
|
|
82
|
-
insertDiagnostic(
|
|
83
|
-
diagnostic.message,
|
|
84
|
-
diagnostic.severity,
|
|
85
|
-
diagnostic.source?.fileName,
|
|
86
|
-
diagnostic.source?.lineNumber,
|
|
87
|
-
diagnostic.source?.columnNumber,
|
|
88
|
-
diagnostic.componentStack ?
|
|
89
|
-
JSON.stringify(diagnostic.componentStack)
|
|
90
|
-
: undefined,
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
107
|
}
|
|
95
108
|
|
|
96
109
|
const DIAGNOSTICS_META_KEY = "diagnostics";
|
package/testing/vitest.d.ts
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vitest matcher type augmentations for alloy testing.
|
|
3
|
+
*
|
|
4
|
+
* Consumers load these types via `"types": ["@alloy-js/core/testing/matchers"]`
|
|
5
|
+
* in their tsconfig.json. This file is resolved directly from the source tree
|
|
6
|
+
* (not from dist/), so it **must remain self-contained** — do NOT add imports
|
|
7
|
+
* that resolve to .ts source files (e.g. `./extend-expect.js`). Doing so pulls
|
|
8
|
+
* the entire src/ tree into the consumer's compilation, creating a dual-module
|
|
9
|
+
* identity conflict between src/ types and dist/ types that produces TS errors.
|
|
10
|
+
*
|
|
11
|
+
* If matcher signatures change, update the inlined types here to match.
|
|
12
|
+
*/
|
|
1
13
|
import "vitest";
|
|
2
|
-
|
|
14
|
+
|
|
15
|
+
interface ToRenderToOptions {
|
|
16
|
+
/** Maximum line width before the formatter wraps output. */
|
|
17
|
+
printWidth?: number;
|
|
18
|
+
/** Number of spaces per indentation level. */
|
|
19
|
+
tabWidth?: number;
|
|
20
|
+
/** Use tab characters instead of spaces. */
|
|
21
|
+
useTabs?: boolean;
|
|
22
|
+
}
|
|
3
23
|
|
|
4
24
|
interface ExpectedDiagnostic {
|
|
5
25
|
message: string | RegExp;
|