@superblocksteam/sdk 2.0.146 → 2.0.147
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/.turbo/turbo-build.log +1 -1
- package/dist/dev-utils/dev-server-metrics.d.mts +9 -0
- package/dist/dev-utils/dev-server-metrics.d.mts.map +1 -1
- package/dist/dev-utils/dev-server-metrics.mjs +14 -0
- package/dist/dev-utils/dev-server-metrics.mjs.map +1 -1
- package/dist/dev-utils/dev-server.d.mts +28 -0
- package/dist/dev-utils/dev-server.d.mts.map +1 -1
- package/dist/dev-utils/dev-server.mjs +55 -20
- package/dist/dev-utils/dev-server.mjs.map +1 -1
- package/dist/dev-utils/dev-server.process-errors.test.d.mts +2 -0
- package/dist/dev-utils/dev-server.process-errors.test.d.mts.map +1 -0
- package/dist/dev-utils/dev-server.process-errors.test.mjs +68 -0
- package/dist/dev-utils/dev-server.process-errors.test.mjs.map +1 -0
- package/dist/dev-utils/fatal-exit.d.mts +13 -0
- package/dist/dev-utils/fatal-exit.d.mts.map +1 -1
- package/dist/dev-utils/fatal-exit.mjs +103 -0
- package/dist/dev-utils/fatal-exit.mjs.map +1 -1
- package/dist/dev-utils/fatal-exit.test.mjs +138 -1
- package/dist/dev-utils/fatal-exit.test.mjs.map +1 -1
- package/dist/dev-utils/recoverable-build-error.d.mts +31 -0
- package/dist/dev-utils/recoverable-build-error.d.mts.map +1 -0
- package/dist/dev-utils/recoverable-build-error.mjs +92 -0
- package/dist/dev-utils/recoverable-build-error.mjs.map +1 -0
- package/dist/dev-utils/recoverable-build-error.test.d.mts +2 -0
- package/dist/dev-utils/recoverable-build-error.test.d.mts.map +1 -0
- package/dist/dev-utils/recoverable-build-error.test.mjs +176 -0
- package/dist/dev-utils/recoverable-build-error.test.mjs.map +1 -0
- package/package.json +6 -6
- package/src/dev-utils/dev-server-metrics.mts +19 -0
- package/src/dev-utils/dev-server.mts +85 -31
- package/src/dev-utils/dev-server.process-errors.test.mts +103 -0
- package/src/dev-utils/fatal-exit.mts +128 -0
- package/src/dev-utils/fatal-exit.test.mts +202 -0
- package/src/dev-utils/recoverable-build-error.mts +151 -0
- package/src/dev-utils/recoverable-build-error.test.mts +236 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DevServerBuildError,
|
|
3
|
+
ServerError,
|
|
4
|
+
} from "@superblocksteam/library-shared/types";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
extractViteBuildErrorMessage,
|
|
8
|
+
isViteBuildError,
|
|
9
|
+
} from "./fatal-exit.mjs";
|
|
10
|
+
import {
|
|
11
|
+
diagnoseViteBuildLogMessage,
|
|
12
|
+
truncateViteRawLog,
|
|
13
|
+
} from "./vite-dev-server-diagnostics.mjs";
|
|
14
|
+
|
|
15
|
+
/** Maximum build errors retained on `devServerStatus.serverErrors`. */
|
|
16
|
+
export const MAX_DEV_SERVER_BUILD_ERRORS = 5;
|
|
17
|
+
|
|
18
|
+
type DevServerStatus = { serverErrors: ServerError[] };
|
|
19
|
+
|
|
20
|
+
export function buildDevServerBuildErrorDedupeKey(
|
|
21
|
+
entry: Pick<DevServerBuildError, "category" | "unresolvedSpecifier">,
|
|
22
|
+
): string {
|
|
23
|
+
return `${entry.category}:${entry.unresolvedSpecifier ?? ""}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildDevServerBuildError(reason: unknown): DevServerBuildError {
|
|
27
|
+
const fullMessage = extractViteBuildErrorMessage(reason);
|
|
28
|
+
// Classify against the full text so a specifier past the cap is still found,
|
|
29
|
+
// but store the capped copy: this string is persisted on `serverErrors`,
|
|
30
|
+
// logged, sent over `/_sb_connect`, and rendered in the editor overlay.
|
|
31
|
+
const diagnostic = diagnoseViteBuildLogMessage(fullMessage);
|
|
32
|
+
const rawError = truncateViteRawLog(fullMessage);
|
|
33
|
+
|
|
34
|
+
if (diagnostic.category === "module_resolve") {
|
|
35
|
+
return {
|
|
36
|
+
type: "dev-server/build",
|
|
37
|
+
timestamp: new Date().toISOString(),
|
|
38
|
+
category: diagnostic.category,
|
|
39
|
+
unresolvedSpecifier: diagnostic.unresolvedSpecifier,
|
|
40
|
+
rawError,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
type: "dev-server/build",
|
|
46
|
+
timestamp: new Date().toISOString(),
|
|
47
|
+
category: diagnostic.category,
|
|
48
|
+
rawError,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function upsertDevServerBuildError(
|
|
53
|
+
devServerStatus: DevServerStatus,
|
|
54
|
+
entry: DevServerBuildError,
|
|
55
|
+
): void {
|
|
56
|
+
const dedupeKey = buildDevServerBuildErrorDedupeKey(entry);
|
|
57
|
+
devServerStatus.serverErrors = devServerStatus.serverErrors.filter(
|
|
58
|
+
(existing) =>
|
|
59
|
+
existing.type !== "dev-server/build" ||
|
|
60
|
+
buildDevServerBuildErrorDedupeKey(existing) !== dedupeKey,
|
|
61
|
+
);
|
|
62
|
+
devServerStatus.serverErrors.push(entry);
|
|
63
|
+
|
|
64
|
+
// Cap build errors only. `serverErrors` is shared with the startup path,
|
|
65
|
+
// which pushes a dependency-install/upgrade entry once per pod before Vite
|
|
66
|
+
// serves anything; those sit at the front and can never be re-reported, so a
|
|
67
|
+
// whole-array cap keeping the newest entries would evict them first.
|
|
68
|
+
let excess =
|
|
69
|
+
devServerStatus.serverErrors.filter(
|
|
70
|
+
(existing) => existing.type === "dev-server/build",
|
|
71
|
+
).length - MAX_DEV_SERVER_BUILD_ERRORS;
|
|
72
|
+
if (excess > 0) {
|
|
73
|
+
devServerStatus.serverErrors = devServerStatus.serverErrors.filter(
|
|
74
|
+
(existing) => {
|
|
75
|
+
if (excess > 0 && existing.type === "dev-server/build") {
|
|
76
|
+
excess -= 1;
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type RecoverableBuildErrorHandler = "uncaughtException" | "unhandledRejection";
|
|
86
|
+
|
|
87
|
+
type RecoverableBuildErrorMetrics = {
|
|
88
|
+
recordRecoverableBuildError: (labels: {
|
|
89
|
+
category: string;
|
|
90
|
+
handler: RecoverableBuildErrorHandler;
|
|
91
|
+
}) => void;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* When a Vite/esbuild build error reaches the process-level handler, keep the
|
|
96
|
+
* pod alive and record it for the editor's `/_sb_connect` readiness path.
|
|
97
|
+
*/
|
|
98
|
+
export function handleRecoverableBuildError(params: {
|
|
99
|
+
devServerStatus?: DevServerStatus;
|
|
100
|
+
devServerMetrics: RecoverableBuildErrorMetrics;
|
|
101
|
+
handler: RecoverableBuildErrorHandler;
|
|
102
|
+
logger: {
|
|
103
|
+
warn: (message: string, meta?: unknown) => void;
|
|
104
|
+
};
|
|
105
|
+
reason: unknown;
|
|
106
|
+
}): boolean {
|
|
107
|
+
// Never throw. This runs inside the process-level `uncaughtException` /
|
|
108
|
+
// `unhandledRejection` listeners, ahead of the caller's `fatalExitHandled`
|
|
109
|
+
// latch and its fatal-exit log line. Node does not degrade a throw from those
|
|
110
|
+
// listeners to the normal fatal path — it tears the process down immediately,
|
|
111
|
+
// losing the real error, the metric, and `gracefulShutdown`, and leaving a
|
|
112
|
+
// misleading TypeError in their place. Returning `false` instead routes the
|
|
113
|
+
// caller to its fatal path, which logs the original reason.
|
|
114
|
+
//
|
|
115
|
+
// `isViteBuildError` classifies on a message marker without re-validating
|
|
116
|
+
// `errors`, so a malformed array reaches the extraction below; that specific
|
|
117
|
+
// hazard is handled in `extractViteBuildErrorMessage`, and this guard covers
|
|
118
|
+
// whatever else the diagnostic, upsert, and metric steps can raise.
|
|
119
|
+
try {
|
|
120
|
+
if (!isViteBuildError(params.reason)) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const entry = buildDevServerBuildError(params.reason);
|
|
125
|
+
if (params.devServerStatus) {
|
|
126
|
+
upsertDevServerBuildError(params.devServerStatus, entry);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
params.devServerMetrics.recordRecoverableBuildError({
|
|
130
|
+
category: entry.category,
|
|
131
|
+
handler: params.handler,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
params.logger.warn(
|
|
135
|
+
`Ignoring recoverable Vite build error (${params.handler})`,
|
|
136
|
+
{ error: entry },
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return true;
|
|
140
|
+
} catch (recordError) {
|
|
141
|
+
// Falling through to the fatal path is the safe direction: the pod exits
|
|
142
|
+
// and the caller logs the original reason. Note the bookkeeping failure
|
|
143
|
+
// separately so a malformed-reason bug stays diagnosable instead of looking
|
|
144
|
+
// like a plain crash.
|
|
145
|
+
params.logger.warn(
|
|
146
|
+
`Failed to record recoverable Vite build error (${params.handler}); treating as fatal`,
|
|
147
|
+
{ recordError },
|
|
148
|
+
);
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
DevServerBuildError,
|
|
5
|
+
ServerError,
|
|
6
|
+
} from "@superblocksteam/library-shared/types";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
buildDevServerBuildError,
|
|
10
|
+
buildDevServerBuildErrorDedupeKey,
|
|
11
|
+
handleRecoverableBuildError,
|
|
12
|
+
MAX_DEV_SERVER_BUILD_ERRORS,
|
|
13
|
+
upsertDevServerBuildError,
|
|
14
|
+
} from "./recoverable-build-error.mjs";
|
|
15
|
+
import { VITE_ERROR_RAW_LOG_MAX_CHARS } from "./vite-dev-server-diagnostics.mjs";
|
|
16
|
+
|
|
17
|
+
describe("buildDevServerBuildError", () => {
|
|
18
|
+
it("classifies module resolve failures with the unresolved specifier", () => {
|
|
19
|
+
const entry = buildDevServerBuildError({
|
|
20
|
+
errors: [{ text: 'Could not resolve "buffer/"' }],
|
|
21
|
+
});
|
|
22
|
+
expect(entry.type).toBe("dev-server/build");
|
|
23
|
+
expect(entry.category).toBe("module_resolve");
|
|
24
|
+
expect(entry.unresolvedSpecifier).toBe("buffer/");
|
|
25
|
+
expect(entry.rawError).toContain("buffer/");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("truncates rawError to the same cap the logging pipeline uses", () => {
|
|
29
|
+
// rawError is persisted on serverErrors, logged, shipped over
|
|
30
|
+
// /_sb_connect, and rendered in the editor overlay. A pathological stack or
|
|
31
|
+
// a failure listing hundreds of specifiers must not reach any of those
|
|
32
|
+
// unbounded.
|
|
33
|
+
const entry = buildDevServerBuildError({
|
|
34
|
+
errors: [
|
|
35
|
+
{
|
|
36
|
+
text: `Could not resolve "buffer/"\n${"x".repeat(
|
|
37
|
+
VITE_ERROR_RAW_LOG_MAX_CHARS * 2,
|
|
38
|
+
)}`,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(entry.rawError.length).toBeLessThan(
|
|
44
|
+
VITE_ERROR_RAW_LOG_MAX_CHARS + 100,
|
|
45
|
+
);
|
|
46
|
+
expect(entry.rawError).toContain("(truncated)");
|
|
47
|
+
// Classification still reads the full text, so the specifier survives even
|
|
48
|
+
// when it sits past the cap.
|
|
49
|
+
expect(entry.category).toBe("module_resolve");
|
|
50
|
+
expect(entry.unresolvedSpecifier).toBe("buffer/");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("upsertDevServerBuildError", () => {
|
|
55
|
+
it("dedupes by category and unresolved specifier and caps the list", () => {
|
|
56
|
+
const devServerStatus = { serverErrors: [] as ServerError[] };
|
|
57
|
+
const first = buildDevServerBuildError({
|
|
58
|
+
errors: [{ text: 'Could not resolve "buffer/"' }],
|
|
59
|
+
});
|
|
60
|
+
const second = buildDevServerBuildError({
|
|
61
|
+
errors: [{ text: 'Could not resolve "buffer/"' }],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
upsertDevServerBuildError(devServerStatus, first);
|
|
65
|
+
upsertDevServerBuildError(devServerStatus, second);
|
|
66
|
+
expect(devServerStatus.serverErrors).toHaveLength(1);
|
|
67
|
+
expect(devServerStatus.serverErrors[0]?.timestamp).toBe(second.timestamp);
|
|
68
|
+
|
|
69
|
+
for (let index = 0; index < MAX_DEV_SERVER_BUILD_ERRORS + 2; index++) {
|
|
70
|
+
upsertDevServerBuildError(
|
|
71
|
+
devServerStatus,
|
|
72
|
+
buildDevServerBuildError({
|
|
73
|
+
errors: [{ text: `Could not resolve "pkg-${index}/"` }],
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
expect(devServerStatus.serverErrors.length).toBeLessThanOrEqual(
|
|
78
|
+
MAX_DEV_SERVER_BUILD_ERRORS,
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("uses a stable dedupe key", () => {
|
|
83
|
+
expect(
|
|
84
|
+
buildDevServerBuildErrorDedupeKey({
|
|
85
|
+
category: "module_resolve",
|
|
86
|
+
unresolvedSpecifier: "buffer/",
|
|
87
|
+
}),
|
|
88
|
+
).toBe("module_resolve:buffer/");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("keeps startup errors when a burst of build errors exceeds the cap", () => {
|
|
92
|
+
// `serverErrors` is shared with the startup path, which pushes a
|
|
93
|
+
// `dependency-install` / `dependency-upgrade` entry once per pod before
|
|
94
|
+
// Vite ever serves a request. Those sit at the front of the array, so a
|
|
95
|
+
// cap that counted every entry and kept only the newest would evict the
|
|
96
|
+
// install failure first — the one entry the user most needs to act on, and
|
|
97
|
+
// one that can never be re-reported.
|
|
98
|
+
const installError: ServerError = {
|
|
99
|
+
type: "dev-server/dependency-install",
|
|
100
|
+
timestamp: new Date("2026-01-01T00:00:00.000Z").toISOString(),
|
|
101
|
+
category: "not_in_registry",
|
|
102
|
+
message: "npm ERR! 404 Not Found - GET https://registry/plotly.js",
|
|
103
|
+
} as ServerError;
|
|
104
|
+
const devServerStatus = { serverErrors: [installError] };
|
|
105
|
+
|
|
106
|
+
for (let index = 0; index < MAX_DEV_SERVER_BUILD_ERRORS + 3; index++) {
|
|
107
|
+
upsertDevServerBuildError(
|
|
108
|
+
devServerStatus,
|
|
109
|
+
buildDevServerBuildError({
|
|
110
|
+
errors: [{ text: `Could not resolve "pkg-${index}/"` }],
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
expect(devServerStatus.serverErrors).toContain(installError);
|
|
116
|
+
expect(
|
|
117
|
+
devServerStatus.serverErrors.filter(
|
|
118
|
+
(entry) => entry.type === "dev-server/build",
|
|
119
|
+
),
|
|
120
|
+
).toHaveLength(MAX_DEV_SERVER_BUILD_ERRORS);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("evicts the oldest build error rather than an arbitrary entry", () => {
|
|
124
|
+
const devServerStatus = { serverErrors: [] as ServerError[] };
|
|
125
|
+
for (let index = 0; index < MAX_DEV_SERVER_BUILD_ERRORS + 1; index++) {
|
|
126
|
+
upsertDevServerBuildError(
|
|
127
|
+
devServerStatus,
|
|
128
|
+
buildDevServerBuildError({
|
|
129
|
+
errors: [{ text: `Could not resolve "pkg-${index}/"` }],
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const specifiers = devServerStatus.serverErrors.map(
|
|
135
|
+
(entry) => (entry as DevServerBuildError).unresolvedSpecifier,
|
|
136
|
+
);
|
|
137
|
+
expect(specifiers).not.toContain("pkg-0/");
|
|
138
|
+
expect(specifiers).toContain(`pkg-${MAX_DEV_SERVER_BUILD_ERRORS}/`);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("handleRecoverableBuildError", () => {
|
|
143
|
+
it("records a build error and returns true for esbuild failures", () => {
|
|
144
|
+
const devServerStatus = { serverErrors: [] as ServerError[] };
|
|
145
|
+
const recordRecoverableBuildError = vi.fn();
|
|
146
|
+
const metrics = {
|
|
147
|
+
recordRecoverableBuildError,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const handled = handleRecoverableBuildError({
|
|
151
|
+
devServerStatus,
|
|
152
|
+
devServerMetrics: metrics,
|
|
153
|
+
handler: "unhandledRejection",
|
|
154
|
+
logger: { warn: vi.fn() },
|
|
155
|
+
reason: {
|
|
156
|
+
errors: [{ text: 'Could not resolve "buffer/"' }],
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(handled).toBe(true);
|
|
161
|
+
expect(devServerStatus.serverErrors).toHaveLength(1);
|
|
162
|
+
expect(recordRecoverableBuildError).toHaveBeenCalledWith({
|
|
163
|
+
category: "module_resolve",
|
|
164
|
+
handler: "unhandledRejection",
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// This runs inside the `uncaughtException` / `unhandledRejection` listeners
|
|
169
|
+
// before the caller writes its fatal-exit line. An escaping throw there does
|
|
170
|
+
// not degrade to the fatal path: Node tears the process down immediately, so
|
|
171
|
+
// the real error never reaches the logs, no metric is emitted, and
|
|
172
|
+
// `gracefulShutdown` never runs. Returning false instead routes the caller to
|
|
173
|
+
// its normal fatal path, which logs the original reason.
|
|
174
|
+
it("falls back to the fatal path instead of throwing when recording fails", () => {
|
|
175
|
+
const logger = { warn: vi.fn() };
|
|
176
|
+
const handled = handleRecoverableBuildError({
|
|
177
|
+
devServerStatus: { serverErrors: [] as ServerError[] },
|
|
178
|
+
devServerMetrics: {
|
|
179
|
+
recordRecoverableBuildError: () => {
|
|
180
|
+
throw new Error("meter unavailable");
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
handler: "uncaughtException",
|
|
184
|
+
logger,
|
|
185
|
+
reason: { errors: [{ text: 'Could not resolve "buffer/"' }] },
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(handled).toBe(false);
|
|
189
|
+
// The bookkeeping failure is noted separately so a malformed reason is
|
|
190
|
+
// diagnosable rather than looking like a plain crash.
|
|
191
|
+
expect(logger.warn).toHaveBeenCalled();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("keeps the recoverable path for a reason whose errors array is malformed", () => {
|
|
195
|
+
// `isViteBuildError` accepts this via the message marker without
|
|
196
|
+
// re-validating `errors`, which is how a malformed array reaches the
|
|
197
|
+
// extraction step.
|
|
198
|
+
const devServerStatus = { serverErrors: [] as ServerError[] };
|
|
199
|
+
const logger = { warn: vi.fn() };
|
|
200
|
+
|
|
201
|
+
const handled = handleRecoverableBuildError({
|
|
202
|
+
devServerStatus,
|
|
203
|
+
devServerMetrics: { recordRecoverableBuildError: vi.fn() },
|
|
204
|
+
handler: "unhandledRejection",
|
|
205
|
+
logger,
|
|
206
|
+
reason: {
|
|
207
|
+
errors: [{ text: 42 }],
|
|
208
|
+
message: "Transform failed with 1 error",
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
expect(handled).toBe(true);
|
|
213
|
+
expect(devServerStatus.serverErrors).toHaveLength(1);
|
|
214
|
+
expect(
|
|
215
|
+
(devServerStatus.serverErrors[0] as { rawError: string }).rawError,
|
|
216
|
+
).toContain("Transform failed");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("returns false for non-build errors", () => {
|
|
220
|
+
const devServerStatus = { serverErrors: [] as ServerError[] };
|
|
221
|
+
const metrics = {
|
|
222
|
+
recordRecoverableBuildError: vi.fn(),
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
expect(
|
|
226
|
+
handleRecoverableBuildError({
|
|
227
|
+
devServerStatus,
|
|
228
|
+
devServerMetrics: metrics,
|
|
229
|
+
handler: "uncaughtException",
|
|
230
|
+
logger: { warn: vi.fn() },
|
|
231
|
+
reason: new TypeError("boom"),
|
|
232
|
+
}),
|
|
233
|
+
).toBe(false);
|
|
234
|
+
expect(devServerStatus.serverErrors).toHaveLength(0);
|
|
235
|
+
});
|
|
236
|
+
});
|