@alloy-js/core 0.23.0-dev.0 → 0.23.0-dev.8
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/CHANGELOG.md +0 -22
- package/dist/devtools/index.html +68 -0
- package/dist/src/binder.d.ts +2 -0
- package/dist/src/binder.d.ts.map +1 -1
- package/dist/src/binder.js +55 -12
- package/dist/src/binder.js.map +1 -1
- package/dist/src/components/AppendFile.d.ts.map +1 -1
- package/dist/src/components/AppendFile.js +14 -3
- package/dist/src/components/AppendFile.js.map +1 -1
- package/dist/src/components/Block.js +1 -1
- package/dist/src/components/Block.js.map +1 -1
- package/dist/src/components/Declaration.d.ts.map +1 -1
- package/dist/src/components/Declaration.js +2 -1
- package/dist/src/components/Declaration.js.map +1 -1
- package/dist/src/components/Scope.d.ts.map +1 -1
- package/dist/src/components/Scope.js +4 -1
- package/dist/src/components/Scope.js.map +1 -1
- package/dist/src/components/TemplateFile.d.ts.map +1 -1
- package/dist/src/components/TemplateFile.js +18 -3
- package/dist/src/components/TemplateFile.js.map +1 -1
- package/dist/src/content-slot.d.ts.map +1 -1
- package/dist/src/content-slot.js +6 -5
- package/dist/src/content-slot.js.map +1 -1
- package/dist/src/context.d.ts.map +1 -1
- package/dist/src/context.js +8 -1
- package/dist/src/context.js.map +1 -1
- package/dist/src/debug/cli.d.ts +6 -0
- package/dist/src/debug/cli.d.ts.map +1 -0
- package/dist/src/{debug.js → debug/cli.js} +78 -84
- package/dist/src/debug/cli.js.map +1 -0
- package/dist/src/debug/diagnostics.test.d.ts +2 -0
- package/dist/src/debug/diagnostics.test.d.ts.map +1 -0
- package/dist/src/debug/diagnostics.test.js +45 -0
- package/dist/src/debug/diagnostics.test.js.map +1 -0
- package/dist/src/debug/effects.d.ts +69 -0
- package/dist/src/debug/effects.d.ts.map +1 -0
- package/dist/src/debug/effects.js +228 -0
- package/dist/src/debug/effects.js.map +1 -0
- package/dist/src/debug/effects.test.d.ts +2 -0
- package/dist/src/debug/effects.test.d.ts.map +1 -0
- package/dist/src/debug/effects.test.js +86 -0
- package/dist/src/debug/effects.test.js.map +1 -0
- package/dist/src/debug/files.d.ts +14 -0
- package/dist/src/debug/files.d.ts.map +1 -0
- package/dist/src/debug/files.js +40 -0
- package/dist/src/debug/files.js.map +1 -0
- package/dist/src/debug/files.test.d.ts +2 -0
- package/dist/src/debug/files.test.d.ts.map +1 -0
- package/dist/src/debug/files.test.js +89 -0
- package/dist/src/debug/files.test.js.map +1 -0
- package/dist/src/debug/index.d.ts +60 -0
- package/dist/src/debug/index.d.ts.map +1 -0
- package/dist/src/debug/index.js +68 -0
- package/dist/src/debug/index.js.map +1 -0
- package/dist/src/debug/render.d.ts +57 -0
- package/dist/src/debug/render.d.ts.map +1 -0
- package/dist/src/debug/render.js +519 -0
- package/dist/src/debug/render.js.map +1 -0
- package/dist/src/debug/render.test.d.ts +2 -0
- package/dist/src/debug/render.test.d.ts.map +1 -0
- package/dist/src/debug/render.test.js +328 -0
- package/dist/src/debug/render.test.js.map +1 -0
- package/dist/src/debug/serialize.d.ts +9 -0
- package/dist/src/debug/serialize.d.ts.map +1 -0
- package/dist/src/debug/serialize.js +70 -0
- package/dist/src/debug/serialize.js.map +1 -0
- package/dist/src/debug/symbols.d.ts +9 -0
- package/dist/src/debug/symbols.d.ts.map +1 -0
- package/dist/src/debug/symbols.js +164 -0
- package/dist/src/debug/symbols.js.map +1 -0
- package/dist/src/debug/symbols.test.d.ts +2 -0
- package/dist/src/debug/symbols.test.d.ts.map +1 -0
- package/dist/src/debug/symbols.test.js +104 -0
- package/dist/src/debug/symbols.test.js.map +1 -0
- package/dist/src/debug/trace.d.ts +342 -0
- package/dist/src/debug/trace.d.ts.map +1 -0
- package/dist/src/debug/trace.js +443 -0
- package/dist/src/debug/trace.js.map +1 -0
- package/dist/src/devtools/devtools-protocol.d.ts +232 -0
- package/dist/src/devtools/devtools-protocol.d.ts.map +1 -0
- package/dist/src/devtools/devtools-protocol.js +2 -0
- package/dist/src/devtools/devtools-protocol.js.map +1 -0
- package/dist/src/devtools/devtools-server.browser.d.ts +28 -0
- package/dist/src/devtools/devtools-server.browser.d.ts.map +1 -0
- package/dist/src/devtools/devtools-server.browser.js +36 -0
- package/dist/src/devtools/devtools-server.browser.js.map +1 -0
- package/dist/src/devtools/devtools-server.d.ts +72 -0
- package/dist/src/devtools/devtools-server.d.ts.map +1 -0
- package/dist/src/devtools/devtools-server.js +256 -0
- package/dist/src/devtools/devtools-server.js.map +1 -0
- package/dist/src/devtools/devtools-transport.d.ts +23 -0
- package/dist/src/devtools/devtools-transport.d.ts.map +1 -0
- package/dist/src/devtools/devtools-transport.js +114 -0
- package/dist/src/devtools/devtools-transport.js.map +1 -0
- package/dist/src/devtools-entry.browser.d.ts +4 -0
- package/dist/src/devtools-entry.browser.d.ts.map +1 -0
- package/dist/src/devtools-entry.browser.js +2 -0
- package/dist/src/devtools-entry.browser.js.map +1 -0
- package/dist/src/devtools-entry.d.ts +4 -0
- package/dist/src/devtools-entry.d.ts.map +1 -0
- package/dist/src/devtools-entry.js +2 -0
- package/dist/src/devtools-entry.js.map +1 -0
- package/dist/src/diagnostics.d.ts +34 -0
- package/dist/src/diagnostics.d.ts.map +1 -0
- package/dist/src/diagnostics.js +89 -0
- package/dist/src/diagnostics.js.map +1 -0
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/print-hook.d.ts +14 -0
- package/dist/src/print-hook.d.ts.map +1 -0
- package/dist/src/print-hook.js +10 -0
- package/dist/src/print-hook.js.map +1 -0
- package/dist/src/reactive-union-set.d.ts.map +1 -1
- package/dist/src/reactive-union-set.js +15 -0
- package/dist/src/reactive-union-set.js.map +1 -1
- package/dist/src/reactivity.d.ts +17 -3
- package/dist/src/reactivity.d.ts.map +1 -1
- package/dist/src/reactivity.js +162 -14
- package/dist/src/reactivity.js.map +1 -1
- package/dist/src/render-stack.d.ts +29 -0
- package/dist/src/render-stack.d.ts.map +1 -0
- package/dist/src/render-stack.js +247 -0
- package/dist/src/render-stack.js.map +1 -0
- package/dist/src/render.d.ts +9 -19
- package/dist/src/render.d.ts.map +1 -1
- package/dist/src/render.js +363 -153
- package/dist/src/render.js.map +1 -1
- package/dist/src/resource.d.ts.map +1 -1
- package/dist/src/resource.js +5 -0
- package/dist/src/resource.js.map +1 -1
- package/dist/src/runtime/component.d.ts +7 -1
- package/dist/src/runtime/component.d.ts.map +1 -1
- package/dist/src/runtime/component.js +4 -1
- package/dist/src/runtime/component.js.map +1 -1
- package/dist/src/scheduler.d.ts +3 -0
- package/dist/src/scheduler.d.ts.map +1 -1
- package/dist/src/scheduler.js +45 -2
- package/dist/src/scheduler.js.map +1 -1
- package/dist/src/symbols/basic-symbol.d.ts.map +1 -1
- package/dist/src/symbols/basic-symbol.js +6 -1
- package/dist/src/symbols/basic-symbol.js.map +1 -1
- package/dist/src/symbols/decl.d.ts.map +1 -1
- package/dist/src/symbols/decl.js +5 -1
- package/dist/src/symbols/decl.js.map +1 -1
- package/dist/src/symbols/output-scope.d.ts +2 -1
- package/dist/src/symbols/output-scope.d.ts.map +1 -1
- package/dist/src/symbols/output-scope.js +13 -8
- package/dist/src/symbols/output-scope.js.map +1 -1
- package/dist/src/symbols/output-symbol.d.ts +1 -0
- package/dist/src/symbols/output-symbol.d.ts.map +1 -1
- package/dist/src/symbols/output-symbol.js +23 -6
- package/dist/src/symbols/output-symbol.js.map +1 -1
- package/dist/src/symbols/symbol-flow.d.ts.map +1 -1
- package/dist/src/symbols/symbol-flow.js +22 -6
- package/dist/src/symbols/symbol-flow.js.map +1 -1
- package/dist/src/symbols/symbol-slot.d.ts.map +1 -1
- package/dist/src/symbols/symbol-slot.js +15 -0
- package/dist/src/symbols/symbol-slot.js.map +1 -1
- package/dist/src/symbols/symbol-slot.test.d.ts +2 -0
- package/dist/src/symbols/symbol-slot.test.d.ts.map +1 -0
- package/dist/src/symbols/symbol-slot.test.js +35 -0
- package/dist/src/symbols/symbol-slot.test.js.map +1 -0
- package/dist/src/symbols/symbol-table.d.ts.map +1 -1
- package/dist/src/symbols/symbol-table.js +6 -5
- package/dist/src/symbols/symbol-table.js.map +1 -1
- package/dist/src/trace.d.ts +2 -0
- package/dist/src/trace.d.ts.map +1 -0
- package/dist/src/trace.js +2 -0
- package/dist/src/trace.js.map +1 -0
- package/dist/src/tracer.d.ts +2 -228
- package/dist/src/tracer.d.ts.map +1 -1
- package/dist/src/tracer.js +5 -298
- package/dist/src/tracer.js.map +1 -1
- package/dist/src/utils.d.ts.map +1 -1
- package/dist/src/utils.js +5 -0
- package/dist/src/utils.js.map +1 -1
- package/dist/test/components/append-file.test.d.ts.map +1 -1
- package/dist/test/components/append-file.test.js +18 -10
- package/dist/test/components/append-file.test.js.map +1 -1
- package/dist/test/components/template-file.test.d.ts.map +1 -1
- package/dist/test/components/template-file.test.js +6 -4
- package/dist/test/components/template-file.test.js.map +1 -1
- package/dist/test/rendering/basic.test.js +3 -0
- package/dist/test/rendering/basic.test.js.map +1 -1
- package/dist/test/rendering/print-render-stack.test.d.ts +2 -0
- package/dist/test/rendering/print-render-stack.test.d.ts.map +1 -0
- package/dist/test/rendering/print-render-stack.test.js +207 -0
- package/dist/test/rendering/print-render-stack.test.js.map +1 -0
- package/dist/testing/create-test-wrapper.d.ts +1 -1
- package/dist/testing/create-test-wrapper.d.ts.map +1 -1
- package/dist/testing/create-test-wrapper.js +1 -1
- package/dist/testing/create-test-wrapper.js.map +1 -1
- package/dist/testing/devtools-utils.d.ts +26 -0
- package/dist/testing/devtools-utils.d.ts.map +1 -0
- package/dist/testing/devtools-utils.js +140 -0
- package/dist/testing/devtools-utils.js.map +1 -0
- package/dist/testing/extend-expect.d.ts.map +1 -1
- package/dist/testing/extend-expect.js +63 -1
- package/dist/testing/extend-expect.js.map +1 -1
- package/dist/testing/render.d.ts +2 -2
- package/dist/testing/render.d.ts.map +1 -1
- package/dist/testing/render.js +2 -2
- package/dist/testing/render.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +21 -7
- package/scripts/copy-devtools-ui.mjs +26 -0
- package/src/binder.ts +71 -16
- package/src/components/AppendFile.tsx +14 -9
- package/src/components/Block.tsx +1 -1
- package/src/components/Declaration.tsx +2 -1
- package/src/components/Scope.tsx +4 -1
- package/src/components/TemplateFile.tsx +18 -9
- package/src/content-slot.tsx +6 -6
- package/src/context.ts +15 -4
- package/src/{debug.ts → debug/cli.ts} +112 -127
- package/src/debug/diagnostics.test.tsx +55 -0
- package/src/debug/effects.test.tsx +96 -0
- package/src/debug/effects.ts +313 -0
- package/src/debug/files.test.tsx +96 -0
- package/src/debug/files.ts +40 -0
- package/src/debug/index.ts +126 -0
- package/src/debug/render.test.tsx +379 -0
- package/src/debug/render.ts +639 -0
- package/src/debug/serialize.ts +85 -0
- package/src/debug/symbols.test.tsx +106 -0
- package/src/debug/symbols.ts +230 -0
- package/src/debug/trace.ts +312 -0
- package/src/devtools/devtools-protocol.ts +312 -0
- package/src/devtools/devtools-server.browser.ts +71 -0
- package/src/devtools/devtools-server.ts +290 -0
- package/src/devtools/devtools-transport.ts +154 -0
- package/src/devtools-entry.browser.ts +52 -0
- package/src/devtools-entry.ts +54 -0
- package/src/diagnostics.ts +141 -0
- package/src/index.ts +2 -6
- package/src/print-hook.ts +22 -0
- package/src/reactive-union-set.ts +71 -41
- package/src/reactivity.ts +206 -23
- package/src/render-stack.ts +289 -0
- package/src/render.ts +464 -212
- package/src/resource.ts +28 -19
- package/src/runtime/component.ts +11 -0
- package/src/scheduler.ts +55 -3
- package/src/symbols/basic-symbol.ts +6 -1
- package/src/symbols/decl.ts +5 -1
- package/src/symbols/output-scope.ts +21 -12
- package/src/symbols/output-symbol.ts +33 -12
- package/src/symbols/symbol-flow.ts +68 -37
- package/src/symbols/symbol-slot.test.tsx +41 -0
- package/src/symbols/symbol-slot.tsx +47 -20
- package/src/symbols/symbol-table.ts +6 -10
- package/src/trace.ts +1 -0
- package/src/tracer.ts +13 -242
- package/src/utils.tsx +22 -13
- package/temp/api.json +1811 -277
- package/test/components/append-file.test.tsx +36 -29
- package/test/components/template-file.test.tsx +11 -11
- package/test/rendering/basic.test.tsx +4 -0
- package/test/rendering/print-render-stack.test.tsx +244 -0
- package/testing/create-test-wrapper.tsx +1 -1
- package/testing/devtools-utils.ts +203 -0
- package/testing/extend-expect.ts +89 -0
- package/testing/render.ts +2 -2
- package/testing/vitest.d.ts +9 -0
- package/dist/src/debug.d.ts +0 -15
- package/dist/src/debug.d.ts.map +0 -1
- package/dist/src/debug.js.map +0 -1
|
@@ -3,7 +3,6 @@ import { tmpdir } from "os";
|
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
5
|
import { AppendFile, AppendRegion } from "../../src/components/AppendFile.jsx";
|
|
6
|
-
import { render, renderAsync } from "../../src/render.js";
|
|
7
6
|
import "../../testing/extend-expect.js";
|
|
8
7
|
import { d } from "../../testing/render.js";
|
|
9
8
|
|
|
@@ -34,6 +33,7 @@ describe("AppendFile", () => {
|
|
|
34
33
|
|
|
35
34
|
await expect(result).toRenderToAsync("Initial content\nNew content");
|
|
36
35
|
});
|
|
36
|
+
|
|
37
37
|
it("should append content to end of file when no sigils present with no explicit append region", async () => {
|
|
38
38
|
// Create initial file content
|
|
39
39
|
writeFileSync(testFilePath, "Initial content", "utf-8");
|
|
@@ -148,30 +148,35 @@ describe("AppendFile", () => {
|
|
|
148
148
|
await expect(result).toRenderToAsync("Content\ndefault region");
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
-
it("should
|
|
151
|
+
it("should emit diagnostic when region is missing corresponding AppendRegion", async () => {
|
|
152
152
|
writeFileSync(testFilePath, "content", "utf-8");
|
|
153
153
|
|
|
154
|
-
expect(
|
|
155
|
-
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
154
|
+
await expect(
|
|
155
|
+
<AppendFile path={testFilePath} regions={["missing"]}>
|
|
156
|
+
<AppendRegion id="append">content</AppendRegion>
|
|
157
|
+
</AppendFile>,
|
|
158
|
+
).toHaveDiagnosticsAsync([
|
|
159
|
+
{
|
|
160
|
+
message:
|
|
161
|
+
'Region "missing" specified but no corresponding AppendRegion child found',
|
|
162
|
+
severity: "error",
|
|
163
|
+
},
|
|
164
|
+
]);
|
|
163
165
|
});
|
|
164
166
|
|
|
165
|
-
it("should
|
|
167
|
+
it("should emit diagnostic when AppendRegion has neither children nor content", async () => {
|
|
166
168
|
writeFileSync(testFilePath, "content", "utf-8");
|
|
167
169
|
|
|
168
|
-
expect(
|
|
169
|
-
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
170
|
+
await expect(
|
|
171
|
+
<AppendFile path={testFilePath}>
|
|
172
|
+
<AppendRegion id="append" />
|
|
173
|
+
</AppendFile>,
|
|
174
|
+
).toHaveDiagnosticsAsync([
|
|
175
|
+
{
|
|
176
|
+
message: 'AppendRegion "append" must have either children or content',
|
|
177
|
+
severity: "error",
|
|
178
|
+
},
|
|
179
|
+
]);
|
|
175
180
|
});
|
|
176
181
|
|
|
177
182
|
it("should throw error when region has missing start sigil", async () => {
|
|
@@ -196,7 +201,7 @@ describe("AppendFile", () => {
|
|
|
196
201
|
`);
|
|
197
202
|
});
|
|
198
203
|
|
|
199
|
-
it("should
|
|
204
|
+
it("should emit diagnostic when region has missing end sigil", async () => {
|
|
200
205
|
const contentWithOnlyStart = d`
|
|
201
206
|
Content
|
|
202
207
|
<!-- alloy-incomplete-start -->
|
|
@@ -204,15 +209,17 @@ describe("AppendFile", () => {
|
|
|
204
209
|
|
|
205
210
|
writeFileSync(testFilePath, contentWithOnlyStart, "utf-8");
|
|
206
211
|
|
|
207
|
-
await expect(
|
|
208
|
-
|
|
209
|
-
<
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
212
|
+
await expect(
|
|
213
|
+
<AppendFile path={testFilePath} regions={["incomplete"]}>
|
|
214
|
+
<AppendRegion id="incomplete">content</AppendRegion>
|
|
215
|
+
</AppendFile>,
|
|
216
|
+
).toHaveDiagnosticsAsync([
|
|
217
|
+
{
|
|
218
|
+
message:
|
|
219
|
+
'Region "incomplete" has start sigil but no corresponding end sigil',
|
|
220
|
+
severity: "error",
|
|
221
|
+
},
|
|
222
|
+
]);
|
|
216
223
|
});
|
|
217
224
|
|
|
218
225
|
it("should handle complex nested content", async () => {
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
TemplateFile,
|
|
7
7
|
TemplateVariable,
|
|
8
8
|
} from "../../src/components/TemplateFile.jsx";
|
|
9
|
-
import { renderAsync } from "../../src/render.js";
|
|
10
9
|
import "../../testing/extend-expect.js";
|
|
11
10
|
import { d } from "../../testing/render.js";
|
|
12
11
|
|
|
@@ -82,21 +81,22 @@ describe("TemplateFile", () => {
|
|
|
82
81
|
await expect(result).toRenderToAsync("Hello Bob!");
|
|
83
82
|
});
|
|
84
83
|
|
|
85
|
-
it("should
|
|
84
|
+
it("should emit diagnostic for missing template variables", async () => {
|
|
86
85
|
const templatePath = join(tmpdir(), "test-missing-var-template.txt");
|
|
87
86
|
const templateContent = "Hello {{ name }}! Your age is {{ age }}.";
|
|
88
87
|
writeFileSync(templatePath, templateContent);
|
|
89
88
|
|
|
90
89
|
await expect(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
90
|
+
<TemplateFile src={templatePath} path="output.txt">
|
|
91
|
+
<TemplateVariable name="name" value="Charlie" />
|
|
92
|
+
</TemplateFile>,
|
|
93
|
+
).toHaveDiagnosticsAsync([
|
|
94
|
+
{
|
|
95
|
+
message:
|
|
96
|
+
'Template variable "age" not found in TemplateVariable children',
|
|
97
|
+
severity: "error",
|
|
98
|
+
},
|
|
99
|
+
]);
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
it("should handle template with no variables", async () => {
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Output,
|
|
3
|
+
SourceDirectory,
|
|
4
|
+
SourceFile,
|
|
5
|
+
renderAsync,
|
|
6
|
+
} from "@alloy-js/core";
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
8
|
+
import WebSocket from "ws";
|
|
9
|
+
import { createNamedContext } from "../../src/context.js";
|
|
10
|
+
import {
|
|
11
|
+
enableDevtools,
|
|
12
|
+
resetDevtoolsServerForTests,
|
|
13
|
+
} from "../../src/devtools/devtools-server.js";
|
|
14
|
+
import { clearRenderStack } from "../../src/render-stack.js";
|
|
15
|
+
import "../../testing/extend-expect.js";
|
|
16
|
+
|
|
17
|
+
// Strip ANSI escape codes from a string for consistent testing across environments
|
|
18
|
+
function stripAnsi(str: string): string {
|
|
19
|
+
// eslint-disable-next-line no-control-regex
|
|
20
|
+
return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Helper to check if any console.error call contains a string (after stripping ANSI codes)
|
|
24
|
+
function expectErrorContaining(
|
|
25
|
+
spy: ReturnType<typeof vi.spyOn>,
|
|
26
|
+
substring: string,
|
|
27
|
+
) {
|
|
28
|
+
const calls = spy.mock.calls.map((call) => stripAnsi(String(call[0])));
|
|
29
|
+
expect(calls.some((msg) => msg.includes(substring))).toBe(true);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("printRenderStack", () => {
|
|
33
|
+
let socket: WebSocket | undefined;
|
|
34
|
+
|
|
35
|
+
beforeEach(async () => {
|
|
36
|
+
const server = await enableDevtools({ port: 0 });
|
|
37
|
+
socket = new WebSocket(`ws://127.0.0.1:${server.port}`);
|
|
38
|
+
await new Promise<void>((resolve, reject) => {
|
|
39
|
+
socket?.once("open", resolve);
|
|
40
|
+
socket?.once("error", reject);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(async () => {
|
|
45
|
+
if (socket) {
|
|
46
|
+
socket.close();
|
|
47
|
+
socket = undefined;
|
|
48
|
+
}
|
|
49
|
+
await resetDevtoolsServerForTests();
|
|
50
|
+
|
|
51
|
+
// Clear render stack to prevent state leakage between tests
|
|
52
|
+
clearRenderStack();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("prints the current file when an error occurs", async () => {
|
|
56
|
+
const consoleErrorSpy = vi.spyOn(console, "error");
|
|
57
|
+
|
|
58
|
+
function ThrowingComponent() {
|
|
59
|
+
throw new Error("Test error");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function ParentComponent() {
|
|
63
|
+
return <ThrowingComponent />;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await expect(
|
|
67
|
+
renderAsync(
|
|
68
|
+
<Output>
|
|
69
|
+
<SourceFile path="test.ts" filetype="typescript">
|
|
70
|
+
<ParentComponent />
|
|
71
|
+
</SourceFile>
|
|
72
|
+
</Output>,
|
|
73
|
+
),
|
|
74
|
+
).rejects.toThrow("Test error");
|
|
75
|
+
|
|
76
|
+
// Check that console.error was called with file path
|
|
77
|
+
expectErrorContaining(consoleErrorSpy, "Error rendering in file test.ts");
|
|
78
|
+
expectErrorContaining(consoleErrorSpy, "ParentComponent");
|
|
79
|
+
expectErrorContaining(consoleErrorSpy, "ThrowingComponent");
|
|
80
|
+
|
|
81
|
+
consoleErrorSpy.mockRestore();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("prints joined path from nested directories", async () => {
|
|
85
|
+
const consoleErrorSpy = vi.spyOn(console, "error");
|
|
86
|
+
|
|
87
|
+
function ThrowingComponent() {
|
|
88
|
+
throw new Error("Nested error");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await expect(
|
|
92
|
+
renderAsync(
|
|
93
|
+
<Output>
|
|
94
|
+
<SourceDirectory path="dir1">
|
|
95
|
+
<SourceDirectory path="dir2">
|
|
96
|
+
<SourceFile path="test.ts" filetype="typescript">
|
|
97
|
+
<ThrowingComponent />
|
|
98
|
+
</SourceFile>
|
|
99
|
+
</SourceDirectory>
|
|
100
|
+
</SourceDirectory>
|
|
101
|
+
</Output>,
|
|
102
|
+
),
|
|
103
|
+
).rejects.toThrow("Nested error");
|
|
104
|
+
|
|
105
|
+
// Should show the joined path of all directories
|
|
106
|
+
expectErrorContaining(
|
|
107
|
+
consoleErrorSpy,
|
|
108
|
+
"Error rendering in file dir1/dir2/test.ts",
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
consoleErrorSpy.mockRestore();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("works when no file context is present", async () => {
|
|
115
|
+
const consoleErrorSpy = vi.spyOn(console, "error");
|
|
116
|
+
|
|
117
|
+
function ThrowingComponent() {
|
|
118
|
+
throw new Error("No file context error");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Track the number of calls before our test
|
|
122
|
+
const callsBefore = consoleErrorSpy.mock.calls.length;
|
|
123
|
+
|
|
124
|
+
await expect(
|
|
125
|
+
renderAsync(
|
|
126
|
+
<Output>
|
|
127
|
+
<ThrowingComponent />
|
|
128
|
+
</Output>,
|
|
129
|
+
),
|
|
130
|
+
).rejects.toThrow("No file context error");
|
|
131
|
+
|
|
132
|
+
// Get only the calls from THIS test (after callsBefore)
|
|
133
|
+
const callsFromThisTest = consoleErrorSpy.mock.calls.slice(callsBefore);
|
|
134
|
+
const messagesFromThisTest = callsFromThisTest.map((call: any) =>
|
|
135
|
+
stripAnsi(String(call[0])),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// Output component creates a SourceDirectory with path "./"
|
|
139
|
+
// The error message should be "Error rendering in file ./"
|
|
140
|
+
expect(
|
|
141
|
+
messagesFromThisTest.some(
|
|
142
|
+
(msg: string) => msg && msg.includes("Error rendering in file ./"),
|
|
143
|
+
),
|
|
144
|
+
).toBe(true);
|
|
145
|
+
|
|
146
|
+
consoleErrorSpy.mockRestore();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("includes component stack with props", async () => {
|
|
150
|
+
const consoleErrorSpy = vi.spyOn(console, "error");
|
|
151
|
+
|
|
152
|
+
function ThrowingComponent(props: { message: string; count: number }) {
|
|
153
|
+
throw new Error("Component error");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function WrapperComponent(props: { value: string }) {
|
|
157
|
+
return <ThrowingComponent message={props.value} count={42} />;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
await expect(
|
|
161
|
+
renderAsync(
|
|
162
|
+
<Output>
|
|
163
|
+
<SourceFile path="props-test.ts" filetype="typescript">
|
|
164
|
+
<WrapperComponent value="test" />
|
|
165
|
+
</SourceFile>
|
|
166
|
+
</Output>,
|
|
167
|
+
),
|
|
168
|
+
).rejects.toThrow("Component error");
|
|
169
|
+
|
|
170
|
+
expectErrorContaining(
|
|
171
|
+
consoleErrorSpy,
|
|
172
|
+
"Error rendering in file props-test.ts",
|
|
173
|
+
);
|
|
174
|
+
expectErrorContaining(consoleErrorSpy, "WrapperComponent");
|
|
175
|
+
expectErrorContaining(consoleErrorSpy, 'value: "test"');
|
|
176
|
+
expectErrorContaining(consoleErrorSpy, "ThrowingComponent");
|
|
177
|
+
expectErrorContaining(consoleErrorSpy, 'message: "test", count: 42');
|
|
178
|
+
|
|
179
|
+
consoleErrorSpy.mockRestore();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("prints 'Error rendering:' when no file or directory context is present", async () => {
|
|
183
|
+
const consoleErrorSpy = vi.spyOn(console, "error");
|
|
184
|
+
|
|
185
|
+
function ThrowingComponent() {
|
|
186
|
+
throw new Error("No context error");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Track the number of calls before our test
|
|
190
|
+
const callsBefore = consoleErrorSpy.mock.calls.length;
|
|
191
|
+
|
|
192
|
+
// Don't use Output wrapper to avoid SourceDirectory context
|
|
193
|
+
await expect(renderAsync(<ThrowingComponent />)).rejects.toThrow();
|
|
194
|
+
|
|
195
|
+
// Get only the calls from THIS test (after callsBefore)
|
|
196
|
+
const callsFromThisTest = consoleErrorSpy.mock.calls.slice(callsBefore);
|
|
197
|
+
const messagesFromThisTest = callsFromThisTest.map((call: any) =>
|
|
198
|
+
stripAnsi(String(call[0])),
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// Should have "Error rendering:" without file path
|
|
202
|
+
expect(
|
|
203
|
+
messagesFromThisTest.some(
|
|
204
|
+
(msg: string) => msg && msg.includes("Error rendering:"),
|
|
205
|
+
),
|
|
206
|
+
).toBe(true);
|
|
207
|
+
// Should NOT have any message with "in file"
|
|
208
|
+
expect(
|
|
209
|
+
messagesFromThisTest.some(
|
|
210
|
+
(msg: string) => msg && msg.includes("in file"),
|
|
211
|
+
),
|
|
212
|
+
).toBe(false);
|
|
213
|
+
|
|
214
|
+
consoleErrorSpy.mockRestore();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("shows context name for named context providers", async () => {
|
|
218
|
+
const consoleErrorSpy = vi.spyOn(console, "error");
|
|
219
|
+
|
|
220
|
+
const MyContext = createNamedContext<string>("MyContext");
|
|
221
|
+
|
|
222
|
+
function ThrowingComponent() {
|
|
223
|
+
throw new Error("Context error");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
await expect(
|
|
227
|
+
renderAsync(
|
|
228
|
+
<Output>
|
|
229
|
+
<SourceFile path="context-test.ts" filetype="typescript">
|
|
230
|
+
<MyContext.Provider value="test-value">
|
|
231
|
+
<ThrowingComponent />
|
|
232
|
+
</MyContext.Provider>
|
|
233
|
+
</SourceFile>
|
|
234
|
+
</Output>,
|
|
235
|
+
),
|
|
236
|
+
).rejects.toThrow("Context error");
|
|
237
|
+
|
|
238
|
+
// Check that the named context provider is shown as a separate component
|
|
239
|
+
expectErrorContaining(consoleErrorSpy, "at MyContext");
|
|
240
|
+
expectErrorContaining(consoleErrorSpy, 'value: "test-value"');
|
|
241
|
+
|
|
242
|
+
consoleErrorSpy.mockRestore();
|
|
243
|
+
});
|
|
244
|
+
});
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
|
|
3
|
+
export interface DevtoolsMessage {
|
|
4
|
+
type: string;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates a message collector that accumulates messages and provides utilities
|
|
10
|
+
* for waiting on conditions. Useful for tests with reactive updates.
|
|
11
|
+
*/
|
|
12
|
+
export function createMessageCollector(socket: WebSocket) {
|
|
13
|
+
let renderBuffer: DevtoolsMessage[] = [];
|
|
14
|
+
let flushBuffer: DevtoolsMessage[] = [];
|
|
15
|
+
const completedRenderBatches: DevtoolsMessage[][] = [];
|
|
16
|
+
const completedFlushBatches: DevtoolsMessage[][] = [];
|
|
17
|
+
const renderWaiters: Array<{
|
|
18
|
+
resolve: (messages: DevtoolsMessage[]) => void;
|
|
19
|
+
reject: (error: Error) => void;
|
|
20
|
+
timeout: NodeJS.Timeout;
|
|
21
|
+
}> = [];
|
|
22
|
+
const flushWaiters: Array<{
|
|
23
|
+
resolve: (messages: DevtoolsMessage[]) => void;
|
|
24
|
+
reject: (error: Error) => void;
|
|
25
|
+
timeout: NodeJS.Timeout;
|
|
26
|
+
}> = [];
|
|
27
|
+
|
|
28
|
+
const resolveBatch = (
|
|
29
|
+
batch: DevtoolsMessage[],
|
|
30
|
+
waiters: typeof renderWaiters,
|
|
31
|
+
completed: DevtoolsMessage[][],
|
|
32
|
+
) => {
|
|
33
|
+
if (waiters.length > 0) {
|
|
34
|
+
for (const waiter of waiters.splice(0, waiters.length)) {
|
|
35
|
+
clearTimeout(waiter.timeout);
|
|
36
|
+
waiter.resolve(batch);
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
completed.push(batch);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const purgeMessages = (
|
|
44
|
+
targetBuffer: DevtoolsMessage[],
|
|
45
|
+
targetCompleted: DevtoolsMessage[][],
|
|
46
|
+
batch: DevtoolsMessage[],
|
|
47
|
+
) => {
|
|
48
|
+
if (batch.length === 0) {
|
|
49
|
+
return {
|
|
50
|
+
buffer: targetBuffer,
|
|
51
|
+
completed: targetCompleted,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const toRemove = new Set(batch);
|
|
56
|
+
const buffer = targetBuffer.filter((msg) => !toRemove.has(msg));
|
|
57
|
+
const completed = targetCompleted
|
|
58
|
+
.map((messages) => messages.filter((msg) => !toRemove.has(msg)))
|
|
59
|
+
.filter((messages) => messages.length > 0);
|
|
60
|
+
|
|
61
|
+
return { buffer, completed };
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const onMessage = (data: WebSocket.RawData) => {
|
|
65
|
+
try {
|
|
66
|
+
const message = JSON.parse(String(data)) as DevtoolsMessage;
|
|
67
|
+
renderBuffer.push(message);
|
|
68
|
+
flushBuffer.push(message);
|
|
69
|
+
|
|
70
|
+
if (
|
|
71
|
+
message.type === "render:complete" ||
|
|
72
|
+
message.type === "render:error"
|
|
73
|
+
) {
|
|
74
|
+
const batch = renderBuffer;
|
|
75
|
+
renderBuffer = [];
|
|
76
|
+
resolveBatch(batch, renderWaiters, completedRenderBatches);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (message.type === "flushJobs:complete") {
|
|
80
|
+
const batch = flushBuffer;
|
|
81
|
+
flushBuffer = [];
|
|
82
|
+
resolveBatch(batch, flushWaiters, completedFlushBatches);
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// ignore invalid messages
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
socket.on("message", onMessage);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
waitForRender(): Promise<DevtoolsMessage[]> {
|
|
93
|
+
if (completedRenderBatches.length > 0) {
|
|
94
|
+
const batch = completedRenderBatches.shift()!;
|
|
95
|
+
const purged = purgeMessages(flushBuffer, completedFlushBatches, batch);
|
|
96
|
+
flushBuffer = purged.buffer;
|
|
97
|
+
completedFlushBatches.length = 0;
|
|
98
|
+
return Promise.resolve(batch);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
const timeout = setTimeout(() => {
|
|
103
|
+
const types = renderBuffer.map((m) => m.type).join(", ");
|
|
104
|
+
reject(
|
|
105
|
+
new Error(
|
|
106
|
+
`Timed out waiting for render completion. Received ${renderBuffer.length} messages${types ? `: ${types}` : "."}`,
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
}, 2000);
|
|
110
|
+
|
|
111
|
+
renderWaiters.push({
|
|
112
|
+
resolve: (messages) => {
|
|
113
|
+
const purged = purgeMessages(
|
|
114
|
+
flushBuffer,
|
|
115
|
+
completedFlushBatches,
|
|
116
|
+
messages,
|
|
117
|
+
);
|
|
118
|
+
flushBuffer = purged.buffer;
|
|
119
|
+
completedFlushBatches.length = 0;
|
|
120
|
+
resolve(messages);
|
|
121
|
+
},
|
|
122
|
+
reject,
|
|
123
|
+
timeout,
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
waitForFlush(): Promise<DevtoolsMessage[]> {
|
|
128
|
+
if (completedFlushBatches.length > 0) {
|
|
129
|
+
const batch = completedFlushBatches.shift()!;
|
|
130
|
+
const purged = purgeMessages(
|
|
131
|
+
renderBuffer,
|
|
132
|
+
completedRenderBatches,
|
|
133
|
+
batch,
|
|
134
|
+
);
|
|
135
|
+
renderBuffer = purged.buffer;
|
|
136
|
+
completedRenderBatches.length = 0;
|
|
137
|
+
completedRenderBatches.push(...purged.completed);
|
|
138
|
+
return Promise.resolve(batch);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
const timeout = setTimeout(() => {
|
|
143
|
+
const types = flushBuffer.map((m) => m.type).join(", ");
|
|
144
|
+
reject(
|
|
145
|
+
new Error(
|
|
146
|
+
`Timed out waiting for flushJobs:complete. Received ${flushBuffer.length} messages${types ? `: ${types}` : "."}`,
|
|
147
|
+
),
|
|
148
|
+
);
|
|
149
|
+
}, 2000);
|
|
150
|
+
|
|
151
|
+
flushWaiters.push({
|
|
152
|
+
resolve: (messages) => {
|
|
153
|
+
const purged = purgeMessages(
|
|
154
|
+
renderBuffer,
|
|
155
|
+
completedRenderBatches,
|
|
156
|
+
messages,
|
|
157
|
+
);
|
|
158
|
+
renderBuffer = purged.buffer;
|
|
159
|
+
completedRenderBatches.length = 0;
|
|
160
|
+
completedRenderBatches.push(...purged.completed);
|
|
161
|
+
resolve(messages);
|
|
162
|
+
},
|
|
163
|
+
reject,
|
|
164
|
+
timeout,
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
/**
|
|
169
|
+
* Stop collecting messages
|
|
170
|
+
*/
|
|
171
|
+
stop() {
|
|
172
|
+
socket.off("message", onMessage);
|
|
173
|
+
for (const waiter of renderWaiters.splice(0, renderWaiters.length)) {
|
|
174
|
+
clearTimeout(waiter.timeout);
|
|
175
|
+
waiter.reject(new Error("Collector stopped before render completion."));
|
|
176
|
+
}
|
|
177
|
+
for (const waiter of flushWaiters.splice(0, flushWaiters.length)) {
|
|
178
|
+
clearTimeout(waiter.timeout);
|
|
179
|
+
waiter.reject(
|
|
180
|
+
new Error("Collector stopped before flushJobs:complete."),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Filter messages to only include render tree messages (those starting with "render:")
|
|
189
|
+
*/
|
|
190
|
+
export function filterRenderTreeMessages(
|
|
191
|
+
messages: DevtoolsMessage[],
|
|
192
|
+
): DevtoolsMessage[] {
|
|
193
|
+
return messages.filter((m) => m.type.startsWith("render"));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Filter messages to only include effect debug messages (those starting with "effect:")
|
|
198
|
+
*/
|
|
199
|
+
export function filterEffectsMessages(
|
|
200
|
+
messages: DevtoolsMessage[],
|
|
201
|
+
): DevtoolsMessage[] {
|
|
202
|
+
return messages.filter((m) => m.type.startsWith("effect:"));
|
|
203
|
+
}
|