effect-start 0.9.0 → 0.11.0

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.
Files changed (54) hide show
  1. package/package.json +15 -14
  2. package/src/BundleHttp.test.ts +1 -1
  3. package/src/Commander.test.ts +15 -15
  4. package/src/Commander.ts +58 -88
  5. package/src/EncryptedCookies.test.ts +4 -4
  6. package/src/FileHttpRouter.test.ts +85 -16
  7. package/src/FileHttpRouter.ts +119 -32
  8. package/src/FileRouter.ts +62 -166
  9. package/src/FileRouterCodegen.test.ts +252 -66
  10. package/src/FileRouterCodegen.ts +13 -56
  11. package/src/FileRouterPattern.test.ts +116 -0
  12. package/src/FileRouterPattern.ts +59 -0
  13. package/src/FileRouter_path.test.ts +63 -102
  14. package/src/FileSystemExtra.test.ts +226 -0
  15. package/src/FileSystemExtra.ts +24 -60
  16. package/src/HttpAppExtra.test.ts +84 -0
  17. package/src/HttpAppExtra.ts +399 -47
  18. package/src/HttpUtils.test.ts +68 -0
  19. package/src/HttpUtils.ts +15 -0
  20. package/src/HyperHtml.ts +24 -5
  21. package/src/JsModule.test.ts +1 -1
  22. package/src/NodeFileSystem.ts +764 -0
  23. package/src/Random.ts +59 -0
  24. package/src/Route.test.ts +515 -18
  25. package/src/Route.ts +321 -166
  26. package/src/RouteRender.ts +40 -0
  27. package/src/Router.test.ts +416 -0
  28. package/src/Router.ts +288 -31
  29. package/src/RouterPattern.test.ts +655 -0
  30. package/src/RouterPattern.ts +416 -0
  31. package/src/Start.ts +14 -52
  32. package/src/TestHttpClient.test.ts +29 -0
  33. package/src/TestHttpClient.ts +122 -73
  34. package/src/assets.d.ts +39 -0
  35. package/src/bun/BunBundle.test.ts +0 -3
  36. package/src/bun/BunHttpServer.test.ts +74 -0
  37. package/src/bun/BunHttpServer.ts +259 -0
  38. package/src/bun/BunHttpServer_web.ts +384 -0
  39. package/src/bun/BunRoute.test.ts +514 -0
  40. package/src/bun/BunRoute.ts +427 -0
  41. package/src/bun/BunRoute_bundles.test.ts +218 -0
  42. package/src/bun/BunRuntime.ts +33 -0
  43. package/src/bun/BunTailwindPlugin.test.ts +1 -1
  44. package/src/bun/_empty.html +1 -0
  45. package/src/bun/index.ts +2 -1
  46. package/src/index.ts +14 -14
  47. package/src/middlewares/BasicAuthMiddleware.test.ts +74 -0
  48. package/src/middlewares/BasicAuthMiddleware.ts +36 -0
  49. package/src/testing.ts +12 -3
  50. package/src/Datastar.test.ts +0 -267
  51. package/src/Datastar.ts +0 -68
  52. package/src/bun/BunFullstackServer.ts +0 -45
  53. package/src/bun/BunFullstackServer_httpServer.ts +0 -541
  54. package/src/jsx-datastar.d.ts +0 -63
@@ -1,9 +1,11 @@
1
1
  import {
2
2
  HttpRouter,
3
+ HttpServerRequest,
3
4
  HttpServerResponse,
4
5
  } from "@effect/platform"
5
6
  import * as HttpApp from "@effect/platform/HttpApp"
6
7
  import * as HttpMiddleware from "@effect/platform/HttpMiddleware"
8
+
7
9
  import {
8
10
  RequestError,
9
11
  RouteNotFound,
@@ -27,10 +29,242 @@ type GraciousError =
27
29
  | RequestError
28
30
  | ParseResult.ParseError
29
31
 
32
+ type StackFrame = {
33
+ function: string
34
+ file: string
35
+ type: "application" | "framework" | "node_modules"
36
+ }
37
+
38
+ const ERROR_PAGE_CSS = `
39
+ :root {
40
+ --error-red: #c00;
41
+ --error-red-dark: #a00;
42
+ --bg-error: #fee;
43
+ --bg-light: #f5f5f5;
44
+ --bg-white: #fff;
45
+ --border-color: #ddd;
46
+ --text-dark: #333;
47
+ --text-gray: #666;
48
+ --text-mono: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
49
+ }
50
+
51
+ * { box-sizing: border-box; }
52
+
53
+ body {
54
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
55
+ margin: 0;
56
+ padding: 0;
57
+ color: var(--text-dark);
58
+ line-height: 1.6;
59
+ min-height: 100dvh;
60
+ }
61
+
62
+ .error-page { width: 100%; margin: 0; }
63
+
64
+ .error-header {
65
+ background: var(--error-red);
66
+ color: white;
67
+ padding: 2rem 2.5rem;
68
+ margin: 0;
69
+ font-family: var(--text-mono);
70
+ }
71
+
72
+ .error-header h1 {
73
+ margin: 0 0 0.5rem 0;
74
+ font-size: 2rem;
75
+ font-weight: 600;
76
+ font-family: var(--text-mono);
77
+ }
78
+
79
+ .error-message {
80
+ margin: 0;
81
+ font-size: 1.1rem;
82
+ opacity: 0.95;
83
+ font-family: var(--text-mono);
84
+ }
85
+
86
+ .error-content {
87
+ background: var(--bg-white);
88
+ padding: 2rem 2.5rem;
89
+ }
90
+
91
+ .stack-trace {
92
+ margin: 1.5rem 0;
93
+ border: 1px solid var(--border-color);
94
+ border-radius: 4px;
95
+ overflow: hidden;
96
+ }
97
+
98
+ .stack-trace-header {
99
+ font-weight: 600;
100
+ padding: 0.75rem 1rem;
101
+ background: var(--bg-light);
102
+ border-bottom: 1px solid var(--border-color);
103
+ }
104
+
105
+ .stack-list {
106
+ list-style: none;
107
+ padding: 0;
108
+ margin: 0;
109
+ max-height: 400px;
110
+ overflow-y: auto;
111
+ }
112
+
113
+ .stack-list li {
114
+ padding: 0.5rem 1rem;
115
+ font-family: var(--text-mono);
116
+ font-size: 0.875rem;
117
+ border-bottom: 1px solid var(--border-color);
118
+ background: var(--bg-white);
119
+ }
120
+
121
+ .stack-list li:last-child { border-bottom: none; }
122
+
123
+ .stack-list li:hover { background: #fafafa; }
124
+
125
+ .stack-list code {
126
+ background: transparent;
127
+ padding: 0;
128
+ font-weight: 600;
129
+ color: var(--error-red-dark);
130
+ }
131
+
132
+ .stack-list .path { color: var(--text-gray); margin-left: 0.5rem; }
133
+
134
+ .request-info {
135
+ margin: 1.5rem 0;
136
+ border: 1px solid var(--border-color);
137
+ border-radius: 4px;
138
+ overflow: hidden;
139
+ }
140
+
141
+ .request-info-header {
142
+ font-weight: 700;
143
+ padding: 0.75rem 1rem;
144
+ background: var(--bg-light);
145
+ border-bottom: 1px solid var(--border-color);
146
+ }
147
+
148
+ .request-info-content {
149
+ padding: 1rem;
150
+ font-family: var(--text-mono);
151
+ font-size: 0.875rem;
152
+ white-space: pre-wrap;
153
+ word-break: break-all;
154
+ }
155
+
156
+ @media (max-width: 768px) {
157
+ .error-header, .error-content { padding: 1.5rem 1rem; }
158
+ .error-header h1 { font-size: 1.5rem; }
159
+ }
160
+ `
161
+
162
+ type ErrorHtmlData = {
163
+ status: number
164
+ tag: string
165
+ message?: string
166
+ details?: object
167
+ requestContext?: RequestContext
168
+ errorName?: string
169
+ }
170
+
171
+ function errorHtml(data: ErrorHtmlData): HttpServerResponse.HttpServerResponse {
172
+ let detailsHtml = ""
173
+
174
+ if (data.details) {
175
+ const detailsObj = data.details as Record<string, unknown>
176
+
177
+ if ("stack" in detailsObj && Array.isArray(detailsObj.stack)) {
178
+ const stackFrames = detailsObj.stack as StackFrame[]
179
+ detailsHtml = renderStackTrace(stackFrames)
180
+ }
181
+ }
182
+
183
+ const requestHtml = data.requestContext
184
+ ? renderRequestContext(data.requestContext)
185
+ : ""
186
+
187
+ const messageHtml = data.message
188
+ ? `<p class="error-message">${escapeHtml(data.message)}</p>`
189
+ : ""
190
+
191
+ const headerTitle = data.errorName ?? "UnexpectedError"
192
+
193
+ const html = `<!DOCTYPE html>
194
+ <html lang="en">
195
+ <head>
196
+ <meta charset="UTF-8">
197
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
198
+ <title>${headerTitle} - Error ${data.status}</title>
199
+ <style>${ERROR_PAGE_CSS}</style>
200
+ </head>
201
+ <body>
202
+ <div class="error-header">
203
+ <h1>${escapeHtml(headerTitle)}</h1>
204
+ ${messageHtml}
205
+ </div>
206
+ <div class="error-content">
207
+ ${detailsHtml}
208
+ ${requestHtml}
209
+ </div>
210
+ </body>
211
+ </html>`
212
+ return HttpServerResponse.text(html, {
213
+ status: data.status,
214
+ contentType: "text/html",
215
+ })
216
+ }
217
+
218
+ function errorText(
219
+ status: number,
220
+ tag: string,
221
+ details?: object,
222
+ ): HttpServerResponse.HttpServerResponse {
223
+ const text = details ? `${tag}\n${JSON.stringify(details, null, 2)}` : tag
224
+ return HttpServerResponse.text(text, { status })
225
+ }
226
+
227
+ function respondWithError(
228
+ accept: string,
229
+ status: number,
230
+ tag: string,
231
+ message?: string,
232
+ details?: object,
233
+ requestContext?: RequestContext,
234
+ errorName?: string,
235
+ ): HttpServerResponse.HttpServerResponse {
236
+ if (accept.includes("text/html")) {
237
+ return errorHtml({
238
+ status,
239
+ tag,
240
+ message,
241
+ details,
242
+ requestContext,
243
+ errorName,
244
+ })
245
+ }
246
+ if (accept.includes("text/plain")) {
247
+ return errorText(status, tag, details)
248
+ }
249
+ return HttpServerResponse.unsafeJson(
250
+ { error: { _tag: tag, ...details } },
251
+ { status },
252
+ )
253
+ }
254
+
30
255
  export const renderError = (
31
256
  error: unknown,
257
+ accept: string = "",
32
258
  ) =>
33
259
  Effect.gen(function*() {
260
+ const request = yield* HttpServerRequest.HttpServerRequest
261
+
262
+ const requestContext: RequestContext = {
263
+ url: request.url,
264
+ method: request.method,
265
+ headers: filterSensitiveHeaders(request.headers),
266
+ }
267
+
34
268
  let unwrappedError: GraciousError | undefined
35
269
 
36
270
  if (Cause.isCause(error)) {
@@ -45,68 +279,175 @@ export const renderError = (
45
279
 
46
280
  switch (unwrappedError?._tag) {
47
281
  case "RouteNotFound":
48
- return yield* HttpServerResponse.unsafeJson({
49
- error: {
50
- _tag: unwrappedError._tag,
51
- },
52
- }, {
53
- status: 404,
54
- })
282
+ return respondWithError(
283
+ accept,
284
+ 404,
285
+ "RouteNotFound",
286
+ "The page you were looking for doesn't exist",
287
+ undefined,
288
+ requestContext,
289
+ )
55
290
  case "RequestError": {
56
291
  const message = unwrappedError.reason === "Decode"
57
292
  ? "Request body is invalid"
58
- : undefined
293
+ : "Request could not be processed"
59
294
 
60
- return yield* HttpServerResponse.unsafeJson({
61
- error: {
62
- _tag: unwrappedError._tag,
295
+ return respondWithError(
296
+ accept,
297
+ 400,
298
+ "RequestError",
299
+ message,
300
+ {
63
301
  reason: unwrappedError.reason,
64
- message,
65
302
  },
66
- }, {
67
- status: 400,
68
- })
303
+ requestContext,
304
+ )
69
305
  }
70
306
  case "ParseError": {
71
307
  const issues = yield* ParseResult.ArrayFormatter.formatIssue(
72
308
  unwrappedError.issue,
73
309
  )
74
- const cleanIssues = issues.map(v => Record.remove(v, "_tag"))
310
+ const cleanIssues = issues.map((v) => Record.remove(v, "_tag"))
75
311
 
76
- return yield* HttpServerResponse.unsafeJson({
77
- error: {
78
- _tag: unwrappedError._tag,
312
+ return respondWithError(
313
+ accept,
314
+ 400,
315
+ "ParseError",
316
+ "Validation failed",
317
+ {
79
318
  issues: cleanIssues,
80
319
  },
81
- }, {
82
- status: 400,
83
- })
320
+ requestContext,
321
+ )
84
322
  }
85
323
  }
86
324
 
87
- return yield* HttpServerResponse.unsafeJson({
88
- error: {
89
- _tag: "UnexpectedError",
90
- },
91
- }, {
92
- status: 500,
93
- })
325
+ if (Cause.isCause(error)) {
326
+ const defects = [...Cause.defects(error)]
327
+ const defect = defects[0]
328
+ if (defect instanceof Error) {
329
+ const stackFrames = extractPrettyStack(defect.stack ?? "")
330
+ return respondWithError(
331
+ accept,
332
+ 500,
333
+ "UnexpectedError",
334
+ defect.message,
335
+ {
336
+ name: defect.name,
337
+ stack: stackFrames,
338
+ },
339
+ requestContext,
340
+ defect.name,
341
+ )
342
+ }
343
+ }
344
+
345
+ return respondWithError(
346
+ accept,
347
+ 500,
348
+ "UnexpectedError",
349
+ "An unexpected error occurred",
350
+ undefined,
351
+ requestContext,
352
+ "UnexpectedError",
353
+ )
94
354
  })
95
355
 
96
- function extractPrettyStack(stack: string) {
356
+ function parseStackFrame(line: string): StackFrame | null {
357
+ const match = line.trim().match(StackLinePattern)
358
+ if (!match) return null
359
+
360
+ const [_, fn, fullPath] = match
361
+ const relativePath = fullPath.replace(process.cwd(), ".")
362
+
363
+ let type: "application" | "framework" | "node_modules"
364
+ if (relativePath.includes("node_modules")) {
365
+ type = "node_modules"
366
+ } else if (
367
+ relativePath.startsWith("./src")
368
+ || relativePath.startsWith("./examples")
369
+ ) {
370
+ type = "application"
371
+ } else {
372
+ type = "framework"
373
+ }
374
+
375
+ return {
376
+ function: fn,
377
+ file: relativePath,
378
+ type,
379
+ }
380
+ }
381
+
382
+ function extractPrettyStack(stack: string): StackFrame[] {
97
383
  return stack
98
384
  .split("\n")
99
385
  .slice(1)
100
- .map((line) => {
101
- const match = line.trim().match(StackLinePattern)
386
+ .map(parseStackFrame)
387
+ .filter((frame): frame is StackFrame => frame !== null)
388
+ }
102
389
 
103
- if (!match) return line
390
+ function renderStackFrames(frames: StackFrame[]): string {
391
+ if (frames.length === 0) {
392
+ return "<li>No stack frames</li>"
393
+ }
394
+ return frames
395
+ .map(
396
+ (f) =>
397
+ `<li><code>${f.function}</code> at <span class="path">${f.file}</span></li>`,
398
+ )
399
+ .join("")
400
+ }
104
401
 
105
- const [_, fn, path] = match
106
- const relativePath = path.replace(process.cwd(), ".")
107
- return [fn, relativePath]
108
- })
109
- .filter(Boolean)
402
+ function renderStackTrace(frames: StackFrame[]): string {
403
+ return `
404
+ <div class="stack-trace">
405
+ <div class="stack-trace-header">Stack Trace (${frames.length})</div>
406
+ <ul class="stack-list">${renderStackFrames(frames)}</ul>
407
+ </div>
408
+ `
409
+ }
410
+
411
+ function escapeHtml(unsafe: string): string {
412
+ return unsafe
413
+ .replace(/&/g, "&amp;")
414
+ .replace(/</g, "&lt;")
415
+ .replace(/>/g, "&gt;")
416
+ .replace(/"/g, "&quot;")
417
+ .replace(/'/g, "&#039;")
418
+ }
419
+
420
+ function filterSensitiveHeaders(
421
+ headers: Record<string, string>,
422
+ ): Record<string, string> {
423
+ const sensitive = ["authorization", "cookie", "x-api-key"]
424
+ return Object.fromEntries(
425
+ Object.entries(headers).filter(
426
+ ([key]) => !sensitive.includes(key.toLowerCase()),
427
+ ),
428
+ )
429
+ }
430
+
431
+ type RequestContext = {
432
+ url: string
433
+ method: string
434
+ headers: Record<string, string>
435
+ }
436
+
437
+ function renderRequestContext(context: RequestContext): string {
438
+ const headersText = Object
439
+ .entries(context.headers)
440
+ .map(([key, value]) => `${key}: ${value}`)
441
+ .join("\n")
442
+
443
+ const requestText = `${context.method} ${context.url}\n${headersText}`
444
+
445
+ return `
446
+ <div class="request-info">
447
+ <div class="request-info-header">Request</div>
448
+ <div class="request-info-content">${escapeHtml(requestText)}</div>
449
+ </div>
450
+ `
110
451
  }
111
452
 
112
453
  export function handleErrors<
@@ -114,14 +455,25 @@ export function handleErrors<
114
455
  R,
115
456
  >(
116
457
  app: HttpApp.Default<E, R>,
117
- ): HttpApp.Default<Exclude<E, RouteNotFound>, R> {
118
- return app.pipe(
119
- Effect.catchAllCause(renderError),
120
- )
458
+ ): HttpApp.Default<
459
+ Exclude<E, RouteNotFound>,
460
+ R | HttpServerRequest.HttpServerRequest
461
+ > {
462
+ return Effect.gen(function*() {
463
+ const request = yield* HttpServerRequest.HttpServerRequest
464
+ const accept = request.headers.accept ?? ""
465
+ return yield* app.pipe(
466
+ Effect.catchAllCause((cause) => renderError(cause, accept)),
467
+ )
468
+ })
121
469
  }
122
470
 
123
- export const withErrorHandled = HttpMiddleware.make(app => {
124
- return app.pipe(
125
- Effect.catchAllCause(renderError),
126
- )
127
- })
471
+ export const withErrorHandled = HttpMiddleware.make(app =>
472
+ Effect.gen(function*() {
473
+ const request = yield* HttpServerRequest.HttpServerRequest
474
+ const accept = request.headers.accept ?? ""
475
+ return yield* app.pipe(
476
+ Effect.catchAllCause((cause) => renderError(cause, accept)),
477
+ )
478
+ })
479
+ )
@@ -0,0 +1,68 @@
1
+ import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
2
+ import * as t from "bun:test"
3
+
4
+ import * as HttpUtils from "./HttpUtils.ts"
5
+
6
+ const makeRequest = (url: string, headers: Record<string, string> = {}) =>
7
+ HttpServerRequest.fromWeb(
8
+ new Request(`http://test${url}`, { headers }),
9
+ )
10
+
11
+ t.describe("makeUrlFromRequest", () => {
12
+ t.it("uses Host header for relative URL", () => {
13
+ const request = makeRequest("/api/users", {
14
+ host: "example.com",
15
+ })
16
+ const url = HttpUtils.makeUrlFromRequest(request)
17
+
18
+ t.expect(url.href).toBe("http://example.com/api/users")
19
+ })
20
+
21
+ t.it("uses Origin header when present (takes precedence over Host)", () => {
22
+ const request = makeRequest("/api/users", {
23
+ origin: "https://app.example.com",
24
+ host: "example.com",
25
+ })
26
+ const url = HttpUtils.makeUrlFromRequest(request)
27
+
28
+ t.expect(url.href).toBe("https://app.example.com/api/users")
29
+ })
30
+
31
+ t.it("uses X-Forwarded-Proto for protocol behind reverse proxy", () => {
32
+ const request = makeRequest("/api/users", {
33
+ host: "example.com",
34
+ "x-forwarded-proto": "https",
35
+ })
36
+ const url = HttpUtils.makeUrlFromRequest(request)
37
+
38
+ t.expect(url.href).toBe("https://example.com/api/users")
39
+ })
40
+
41
+ t.it("falls back to http://localhost when no headers", () => {
42
+ const request = makeRequest("/api/users", {})
43
+ const url = HttpUtils.makeUrlFromRequest(request)
44
+
45
+ t.expect(url.href).toBe("http://localhost/api/users")
46
+ })
47
+
48
+ t.it("handles URL with query parameters", () => {
49
+ const request = makeRequest("/search?q=test&page=1", {
50
+ host: "example.com",
51
+ })
52
+ const url = HttpUtils.makeUrlFromRequest(request)
53
+
54
+ t.expect(url.href).toBe("http://example.com/search?q=test&page=1")
55
+ t.expect(url.searchParams.get("q")).toBe("test")
56
+ t.expect(url.searchParams.get("page")).toBe("1")
57
+ })
58
+
59
+ t.it("handles root path", () => {
60
+ const request = makeRequest("/", {
61
+ host: "example.com",
62
+ })
63
+ const url = HttpUtils.makeUrlFromRequest(request)
64
+
65
+ t.expect(url.href).toBe("http://example.com/")
66
+ t.expect(url.pathname).toBe("/")
67
+ })
68
+ })
@@ -0,0 +1,15 @@
1
+ import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
2
+
3
+ export function makeUrlFromRequest(
4
+ request: HttpServerRequest.HttpServerRequest,
5
+ ): URL {
6
+ const origin = request.headers.origin
7
+ ?? request.headers.host
8
+ ?? "http://localhost"
9
+ const protocol = request.headers["x-forwarded-proto"] ?? "http"
10
+ const host = request.headers.host ?? "localhost"
11
+ const base = origin.startsWith("http")
12
+ ? origin
13
+ : `${protocol}://${host}`
14
+ return new URL(request.url, base)
15
+ }
package/src/HyperHtml.ts CHANGED
@@ -1,9 +1,27 @@
1
+ /**
2
+ * Renders Hyper JSX nodes to HTML.
3
+ *
4
+ * Effect Start comes with {@link Hyper} and {@link JsxRuntime} to enable
5
+ * JSX support. The advantage of using JSX over HTML strings or templates
6
+ * is type safety and better editor support.
7
+ *
8
+ * JSX nodes are compatible with React's and Solid's.
9
+
10
+ * You can enable JSX support by updating `tsconfig.json`:
11
+ *
12
+ * {
13
+ * compilerOptions: {
14
+ * jsx: "react-jsx",
15
+ * jsxImportSource: "effect-start" | "react" | "praect" // etc.
16
+ * }
17
+ * }
18
+ */
19
+
20
+ import type * as Hyper from "./Hyper.tsx"
1
21
  import * as HyperNode from "./HyperNode.ts"
2
22
  import { JSX } from "./jsx"
23
+ import type * as JsxRuntime from "./jsx-runtime.ts"
3
24
 
4
- /**
5
- * From: https://github.com/developit/vhtml
6
- */
7
25
  const EMPTY_TAGS = [
8
26
  "area",
9
27
  "base",
@@ -93,8 +111,8 @@ export function renderToString(
93
111
  for (const key in props) {
94
112
  if (
95
113
  key !== "children"
96
- && key !== "innerHTML"
97
- && key !== "dangerouslySetInnerHTML"
114
+ && key !== "innerHTML" // Solid-specific
115
+ && key !== "dangerouslySetInnerHTML" // React-specific
98
116
  && props[key] !== false
99
117
  && props[key] != null
100
118
  ) {
@@ -113,6 +131,7 @@ export function renderToString(
113
131
  if (!EMPTY_TAGS.includes(type)) {
114
132
  stack.push(`</${type}>`)
115
133
 
134
+ // React-specific
116
135
  const html = props.dangerouslySetInnerHTML?.__html
117
136
  ?? props.innerHTML
118
137
 
@@ -1,7 +1,7 @@
1
1
  import * as t from "bun:test"
2
2
  import * as JsModule from "./JsModule.ts"
3
3
 
4
- t.describe("importSource", () => {
4
+ t.describe(`${JsModule.importSource.name}`, () => {
5
5
  t.it("imports a string", async () => {
6
6
  const mod = await JsModule.importSource<any>(`
7
7
  export const b = "B"