effect-start 0.15.0 → 0.17.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.
- package/package.json +2 -1
- package/src/ContentNegotiation.test.ts +103 -0
- package/src/ContentNegotiation.ts +10 -3
- package/src/Development.test.ts +119 -0
- package/src/Development.ts +137 -0
- package/src/Entity.test.ts +592 -0
- package/src/Entity.ts +359 -0
- package/src/FileRouter.ts +2 -2
- package/src/Http.test.ts +315 -20
- package/src/Http.ts +153 -11
- package/src/PathPattern.ts +3 -1
- package/src/Route.ts +26 -10
- package/src/RouteBody.test.ts +98 -66
- package/src/RouteBody.ts +125 -35
- package/src/RouteHook.ts +15 -14
- package/src/RouteHttp.test.ts +2549 -83
- package/src/RouteHttp.ts +337 -113
- package/src/RouteHttpTracer.ts +92 -0
- package/src/RouteMount.test.ts +23 -10
- package/src/RouteMount.ts +161 -4
- package/src/RouteSchema.test.ts +346 -0
- package/src/RouteSchema.ts +386 -7
- package/src/RouteSse.test.ts +249 -0
- package/src/RouteSse.ts +195 -0
- package/src/RouteTree.test.ts +233 -85
- package/src/RouteTree.ts +98 -44
- package/src/StreamExtra.ts +21 -1
- package/src/Values.test.ts +263 -0
- package/src/Values.ts +68 -6
- package/src/bun/BunBundle.ts +0 -73
- package/src/bun/BunHttpServer.ts +23 -7
- package/src/bun/BunRoute.test.ts +162 -0
- package/src/bun/BunRoute.ts +144 -105
- package/src/hyper/HyperHtml.test.ts +119 -0
- package/src/hyper/HyperHtml.ts +10 -2
- package/src/hyper/HyperNode.ts +2 -0
- package/src/hyper/HyperRoute.test.tsx +197 -0
- package/src/hyper/HyperRoute.ts +61 -0
- package/src/hyper/index.ts +4 -0
- package/src/hyper/jsx.d.ts +15 -0
- package/src/index.ts +2 -0
- package/src/node/FileSystem.ts +8 -0
- package/src/testing/TestLogger.test.ts +0 -3
- package/src/testing/TestLogger.ts +15 -9
- package/src/FileSystemExtra.test.ts +0 -242
- package/src/FileSystemExtra.ts +0 -66
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import * as test from "bun:test"
|
|
2
|
+
import * as Effect from "effect/Effect"
|
|
3
|
+
import * as Layer from "effect/Layer"
|
|
4
|
+
import * as Option from "effect/Option"
|
|
5
|
+
import * as Route from "../Route.ts"
|
|
6
|
+
import * as BunHttpServer from "./BunHttpServer.ts"
|
|
7
|
+
import * as BunRoute from "./BunRoute.ts"
|
|
8
|
+
|
|
9
|
+
const layerServer = Layer.scoped(
|
|
10
|
+
BunHttpServer.BunHttpServer,
|
|
11
|
+
BunHttpServer.make({ port: 0 }),
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const testLayer = (routes: ReturnType<typeof Route.tree>) =>
|
|
15
|
+
Layer.provideMerge(
|
|
16
|
+
BunHttpServer.layerRoutes(routes),
|
|
17
|
+
layerServer,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
test.describe(BunRoute.htmlBundle, () => {
|
|
21
|
+
test.test("wraps child content with layout", async () => {
|
|
22
|
+
const routes = Route.tree({
|
|
23
|
+
"/": Route.get(
|
|
24
|
+
BunRoute.htmlBundle(() => import("../../static/LayoutSlots.html")),
|
|
25
|
+
Route.html("<p>Hello World</p>"),
|
|
26
|
+
),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const response = await Effect.runPromise(
|
|
30
|
+
Effect.scoped(
|
|
31
|
+
Effect
|
|
32
|
+
.gen(function*() {
|
|
33
|
+
const bunServer = yield* BunHttpServer.BunHttpServer
|
|
34
|
+
return yield* Effect.promise(() =>
|
|
35
|
+
fetch(`http://localhost:${bunServer.server.port}/`)
|
|
36
|
+
)
|
|
37
|
+
})
|
|
38
|
+
.pipe(Effect.provide(testLayer(routes))),
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
test.expect(response.status).toBe(200)
|
|
43
|
+
const html = await response.text()
|
|
44
|
+
test.expect(html).toContain("<p>Hello World</p>")
|
|
45
|
+
test.expect(html).toContain("<body>")
|
|
46
|
+
test.expect(html).toContain("</body>")
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test.test("replaces %yield% with child content", async () => {
|
|
50
|
+
const routes = Route.tree({
|
|
51
|
+
"/page": Route.get(
|
|
52
|
+
BunRoute.htmlBundle(() => import("../../static/LayoutSlots.html")),
|
|
53
|
+
Route.html("<div>Page Content</div>"),
|
|
54
|
+
),
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const response = await Effect.runPromise(
|
|
58
|
+
Effect.scoped(
|
|
59
|
+
Effect
|
|
60
|
+
.gen(function*() {
|
|
61
|
+
const bunServer = yield* BunHttpServer.BunHttpServer
|
|
62
|
+
return yield* Effect.promise(() =>
|
|
63
|
+
fetch(`http://localhost:${bunServer.server.port}/page`)
|
|
64
|
+
)
|
|
65
|
+
})
|
|
66
|
+
.pipe(Effect.provide(testLayer(routes))),
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const html = await response.text()
|
|
71
|
+
test.expect(html).toContain("<div>Page Content</div>")
|
|
72
|
+
test.expect(html).not.toContain("%children%")
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test.test("works with use() for wildcard routes", async () => {
|
|
76
|
+
const routes = Route.tree({
|
|
77
|
+
"*": Route.use(
|
|
78
|
+
BunRoute.htmlBundle(() => import("../../static/LayoutSlots.html")),
|
|
79
|
+
),
|
|
80
|
+
"/:path*": Route.get(Route.html("<section>Catch All</section>")),
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const response = await Effect.runPromise(
|
|
84
|
+
Effect.scoped(
|
|
85
|
+
Effect
|
|
86
|
+
.gen(function*() {
|
|
87
|
+
const bunServer = yield* BunHttpServer.BunHttpServer
|
|
88
|
+
return yield* Effect.promise(() =>
|
|
89
|
+
fetch(`http://localhost:${bunServer.server.port}/any/path`)
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
.pipe(Effect.provide(testLayer(routes))),
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
test.expect(response.status).toBe(200)
|
|
97
|
+
const html = await response.text()
|
|
98
|
+
test.expect(html).toContain("<section>Catch All</section>")
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test.test("has format: html descriptor", async () => {
|
|
102
|
+
const routes = Route.tree({
|
|
103
|
+
"/": Route.get(
|
|
104
|
+
BunRoute.htmlBundle(() => import("../../static/LayoutSlots.html")),
|
|
105
|
+
Route.html("<p>content</p>"),
|
|
106
|
+
),
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const response = await Effect.runPromise(
|
|
110
|
+
Effect.scoped(
|
|
111
|
+
Effect
|
|
112
|
+
.gen(function*() {
|
|
113
|
+
const bunServer = yield* BunHttpServer.BunHttpServer
|
|
114
|
+
return yield* Effect.promise(() =>
|
|
115
|
+
fetch(`http://localhost:${bunServer.server.port}/`)
|
|
116
|
+
)
|
|
117
|
+
})
|
|
118
|
+
.pipe(Effect.provide(testLayer(routes))),
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
test.expect(response.status).toBe(200)
|
|
123
|
+
const contentType = response.headers.get("content-type")
|
|
124
|
+
test.expect(contentType).toContain("text/html")
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test.describe(BunRoute.validateBunPattern, () => {
|
|
129
|
+
test.test("returns none for valid patterns", () => {
|
|
130
|
+
test
|
|
131
|
+
.expect(Option.isNone(BunRoute.validateBunPattern("/users")))
|
|
132
|
+
.toBe(true)
|
|
133
|
+
test
|
|
134
|
+
.expect(Option.isNone(BunRoute.validateBunPattern("/users/[id]")))
|
|
135
|
+
.toBe(true)
|
|
136
|
+
test
|
|
137
|
+
.expect(Option.isNone(BunRoute.validateBunPattern("/[...rest]")))
|
|
138
|
+
.toBe(true)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test.test("returns error for prefixed params", () => {
|
|
142
|
+
const result = BunRoute.validateBunPattern("/pk_[id]")
|
|
143
|
+
test.expect(Option.isSome(result)).toBe(true)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test.test("returns error for suffixed params", () => {
|
|
147
|
+
const result = BunRoute.validateBunPattern("/[id]_suffix")
|
|
148
|
+
test.expect(Option.isSome(result)).toBe(true)
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test.describe(BunRoute.isHtmlBundle, () => {
|
|
153
|
+
test.test("returns false for non-objects", () => {
|
|
154
|
+
test.expect(BunRoute.isHtmlBundle(null)).toBe(false)
|
|
155
|
+
test.expect(BunRoute.isHtmlBundle(undefined)).toBe(false)
|
|
156
|
+
test.expect(BunRoute.isHtmlBundle("string")).toBe(false)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test.test("returns true for object with index property", () => {
|
|
160
|
+
test.expect(BunRoute.isHtmlBundle({ index: "index.html" })).toBe(true)
|
|
161
|
+
})
|
|
162
|
+
})
|
package/src/bun/BunRoute.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
|
|
3
|
-
import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
|
|
4
1
|
import type * as Bun from "bun"
|
|
5
2
|
import * as Array from "effect/Array"
|
|
3
|
+
import * as Data from "effect/Data"
|
|
6
4
|
import * as Effect from "effect/Effect"
|
|
7
5
|
import * as Option from "effect/Option"
|
|
8
|
-
import * as
|
|
6
|
+
import * as Entity from "../Entity.ts"
|
|
9
7
|
import * as Hyper from "../hyper/Hyper.ts"
|
|
10
8
|
import * as HyperHtml from "../hyper/HyperHtml.ts"
|
|
11
9
|
import * as Random from "../Random.ts"
|
|
@@ -13,106 +11,155 @@ import * as Route from "../Route.ts"
|
|
|
13
11
|
import * as RouterPattern from "../RouterPattern.ts"
|
|
14
12
|
import * as BunHttpServer from "./BunHttpServer.ts"
|
|
15
13
|
|
|
16
|
-
const BunHandlerTypeId: unique symbol = Symbol.for("effect-start/BunHandler")
|
|
17
|
-
|
|
18
14
|
const INTERNAL_FETCH_HEADER = "x-effect-start-internal-fetch"
|
|
19
15
|
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
16
|
+
export class BunRouteError extends Data.TaggedError("BunRouteError")<{
|
|
17
|
+
reason: "ProxyError" | "UnsupportedPattern"
|
|
18
|
+
pattern: string
|
|
19
|
+
message: string
|
|
20
|
+
}> {}
|
|
21
|
+
|
|
22
|
+
export type BunDescriptors = {
|
|
23
|
+
bunPrefix: string
|
|
24
|
+
bunLoad: () => Promise<Bun.HTMLBundle>
|
|
25
|
+
}
|
|
27
26
|
|
|
28
|
-
export function
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
export function descriptors(
|
|
28
|
+
route: Route.Route.Route,
|
|
29
|
+
): BunDescriptors | undefined {
|
|
30
|
+
const descriptor = Route.descriptor(route) as Partial<BunDescriptors>
|
|
31
|
+
if (
|
|
32
|
+
typeof descriptor.bunPrefix === "string"
|
|
33
|
+
&& typeof descriptor.bunLoad === "function"
|
|
34
|
+
) {
|
|
35
|
+
return descriptor as BunDescriptors
|
|
36
|
+
}
|
|
37
|
+
return undefined
|
|
31
38
|
}
|
|
32
39
|
|
|
33
|
-
export function
|
|
40
|
+
export function htmlBundle(
|
|
34
41
|
load: () => Promise<Bun.HTMLBundle | { default: Bun.HTMLBundle }>,
|
|
35
|
-
)
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
42
|
+
) {
|
|
43
|
+
const bunPrefix = `/.BunRoute-${Random.token(6)}`
|
|
44
|
+
const bunLoad = () => load().then(mod => "default" in mod ? mod.default : mod)
|
|
45
|
+
const descriptors = { bunPrefix, bunLoad, format: "html" as const }
|
|
46
|
+
|
|
47
|
+
return function<
|
|
48
|
+
D extends Route.RouteDescriptor.Any,
|
|
49
|
+
B extends {},
|
|
50
|
+
I extends Route.Route.Tuple,
|
|
51
|
+
>(
|
|
52
|
+
self: Route.RouteSet.RouteSet<D, B, I>,
|
|
53
|
+
): Route.RouteSet.RouteSet<
|
|
54
|
+
D,
|
|
55
|
+
B,
|
|
56
|
+
[
|
|
57
|
+
...I,
|
|
58
|
+
Route.Route.Route<
|
|
59
|
+
BunDescriptors & { format: "html" },
|
|
60
|
+
{ request: Request },
|
|
61
|
+
string,
|
|
62
|
+
BunRouteError,
|
|
63
|
+
BunHttpServer.BunHttpServer
|
|
64
|
+
>,
|
|
65
|
+
]
|
|
66
|
+
> {
|
|
67
|
+
const handler: Route.Route.Handler<
|
|
68
|
+
BunDescriptors & { format: "html" } & { request: Request },
|
|
69
|
+
string,
|
|
70
|
+
BunRouteError,
|
|
71
|
+
BunHttpServer.BunHttpServer
|
|
72
|
+
> = (context, next) =>
|
|
73
|
+
Effect.gen(function*() {
|
|
74
|
+
const originalRequest = context.request
|
|
75
|
+
|
|
76
|
+
if (
|
|
77
|
+
originalRequest.headers.get(INTERNAL_FETCH_HEADER) === "true"
|
|
78
|
+
) {
|
|
79
|
+
const url = new URL(originalRequest.url)
|
|
80
|
+
return yield* Effect.fail(
|
|
81
|
+
new BunRouteError({
|
|
82
|
+
reason: "ProxyError",
|
|
83
|
+
pattern: url.pathname,
|
|
84
|
+
message:
|
|
85
|
+
"Request to internal Bun server was caught by BunRoute handler. This should not happen. Please report a bug.",
|
|
86
|
+
}),
|
|
87
|
+
)
|
|
88
|
+
}
|
|
68
89
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
const bunServer = yield* BunHttpServer.BunHttpServer
|
|
91
|
+
const url = new URL(originalRequest.url)
|
|
92
|
+
|
|
93
|
+
const internalPath = `${bunPrefix}${url.pathname}`
|
|
94
|
+
const internalUrl = new URL(internalPath, bunServer.server.url)
|
|
95
|
+
|
|
96
|
+
const headers = new Headers(originalRequest.headers)
|
|
97
|
+
headers.set(INTERNAL_FETCH_HEADER, "true")
|
|
98
|
+
|
|
99
|
+
const proxyRequest = new Request(internalUrl, {
|
|
100
|
+
method: originalRequest.method,
|
|
101
|
+
headers,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const response = yield* Effect.tryPromise({
|
|
105
|
+
try: () => fetch(proxyRequest),
|
|
106
|
+
catch: (error) =>
|
|
107
|
+
new BunRouteError({
|
|
108
|
+
reason: "ProxyError",
|
|
109
|
+
pattern: internalPath,
|
|
110
|
+
message: `Failed to fetch internal HTML bundle: ${String(error)}`,
|
|
111
|
+
}),
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
let html = yield* Effect.tryPromise({
|
|
115
|
+
try: () => response.text(),
|
|
116
|
+
catch: (error) =>
|
|
117
|
+
new BunRouteError({
|
|
118
|
+
reason: "ProxyError",
|
|
119
|
+
pattern: internalPath,
|
|
120
|
+
message: String(error),
|
|
121
|
+
}),
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const childEntity = yield* Entity.resolve(next(context))
|
|
125
|
+
const children = childEntity?.body ?? childEntity
|
|
126
|
+
|
|
127
|
+
let childrenHtml = ""
|
|
128
|
+
if (children != null) {
|
|
129
|
+
if ((children as unknown) instanceof Response) {
|
|
130
|
+
childrenHtml = yield* Effect.promise(() =>
|
|
131
|
+
(children as unknown as Response).text()
|
|
132
|
+
)
|
|
133
|
+
} else if (Hyper.isGenericJsxObject(children)) {
|
|
134
|
+
childrenHtml = HyperHtml.renderToString(children)
|
|
135
|
+
} else {
|
|
136
|
+
childrenHtml = String(children)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
html = html.replace(/%children%/g, childrenHtml)
|
|
78
141
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
message: String(error),
|
|
86
|
-
}),
|
|
142
|
+
return Entity.make(html, {
|
|
143
|
+
status: response.status,
|
|
144
|
+
headers: {
|
|
145
|
+
"content-type": response.headers.get("content-type"),
|
|
146
|
+
},
|
|
147
|
+
})
|
|
87
148
|
})
|
|
88
149
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
html = html.replace(/%yield%/g, childrenHtml)
|
|
103
|
-
html = html.replace(
|
|
104
|
-
/%slots\.(\w+)%/g,
|
|
105
|
-
(_, name) => context.slots[name] ?? "",
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
return html
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
return Object.assign(handler, {
|
|
112
|
-
[BunHandlerTypeId]: BunHandlerTypeId,
|
|
113
|
-
internalPathPrefix,
|
|
114
|
-
load: () => load().then(mod => "default" in mod ? mod.default : mod),
|
|
115
|
-
}) as BunHandler
|
|
150
|
+
const route = Route.make<
|
|
151
|
+
BunDescriptors & { format: "html" },
|
|
152
|
+
{ request: Request },
|
|
153
|
+
string,
|
|
154
|
+
BunRouteError,
|
|
155
|
+
BunHttpServer.BunHttpServer
|
|
156
|
+
>(handler, descriptors)
|
|
157
|
+
|
|
158
|
+
return Route.set(
|
|
159
|
+
[...Route.items(self), route] as any,
|
|
160
|
+
Route.descriptor(self),
|
|
161
|
+
)
|
|
162
|
+
}
|
|
116
163
|
}
|
|
117
164
|
|
|
118
165
|
type BunServerFetchHandler = (
|
|
@@ -127,14 +174,6 @@ type BunServerRouteHandler =
|
|
|
127
174
|
|
|
128
175
|
export type BunRoutes = Record<string, BunServerRouteHandler>
|
|
129
176
|
|
|
130
|
-
type MethodHandlers = Partial<
|
|
131
|
-
Record<Bun.Serve.HTTPMethod, BunServerFetchHandler>
|
|
132
|
-
>
|
|
133
|
-
|
|
134
|
-
function isMethodHandlers(value: unknown): value is MethodHandlers {
|
|
135
|
-
return typeof value === "object" && value !== null && !("index" in value)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
177
|
/**
|
|
139
178
|
* Validates that a route pattern can be implemented with Bun.serve routes.
|
|
140
179
|
*
|
|
@@ -156,7 +195,7 @@ function isMethodHandlers(value: unknown): value is MethodHandlers {
|
|
|
156
195
|
|
|
157
196
|
export function validateBunPattern(
|
|
158
197
|
pattern: string,
|
|
159
|
-
): Option.Option<
|
|
198
|
+
): Option.Option<BunRouteError> {
|
|
160
199
|
const segments = RouterPattern.parse(pattern)
|
|
161
200
|
|
|
162
201
|
const unsupported = Array.findFirst(segments, (seg) => {
|
|
@@ -169,7 +208,7 @@ export function validateBunPattern(
|
|
|
169
208
|
|
|
170
209
|
if (Option.isSome(unsupported)) {
|
|
171
210
|
return Option.some(
|
|
172
|
-
new
|
|
211
|
+
new BunRouteError({
|
|
173
212
|
reason: "UnsupportedPattern",
|
|
174
213
|
pattern,
|
|
175
214
|
message:
|
|
@@ -182,7 +221,7 @@ export function validateBunPattern(
|
|
|
182
221
|
return Option.none()
|
|
183
222
|
}
|
|
184
223
|
|
|
185
|
-
export const
|
|
224
|
+
export const isHtmlBundle = (handle: any) => {
|
|
186
225
|
return (
|
|
187
226
|
typeof handle === "object"
|
|
188
227
|
&& handle !== null
|
|
@@ -88,3 +88,122 @@ test.it("mixed boolean and string attributes", () => {
|
|
|
88
88
|
.expect(html)
|
|
89
89
|
.toBe("<input type=\"checkbox\" checked name=\"test\" value=\"on\">")
|
|
90
90
|
})
|
|
91
|
+
|
|
92
|
+
test.it("data-* attributes with object values are JSON stringified", () => {
|
|
93
|
+
const node = HyperNode.make("div", {
|
|
94
|
+
"data-signals": {
|
|
95
|
+
draft: "",
|
|
96
|
+
pendingDraft: "",
|
|
97
|
+
username: "User123",
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const html = HyperHtml.renderToString(node)
|
|
102
|
+
|
|
103
|
+
test
|
|
104
|
+
.expect(html)
|
|
105
|
+
.toBe(
|
|
106
|
+
"<div data-signals=\"{"draft":"","pendingDraft":"","username":"User123"}\"></div>",
|
|
107
|
+
)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test.it("data-* attributes with array values are JSON stringified", () => {
|
|
111
|
+
const node = HyperNode.make("div", {
|
|
112
|
+
"data-items": [1, 2, 3],
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const html = HyperHtml.renderToString(node)
|
|
116
|
+
|
|
117
|
+
test
|
|
118
|
+
.expect(html)
|
|
119
|
+
.toBe("<div data-items=\"[1,2,3]\"></div>")
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test.it("data-* attributes with nested object values", () => {
|
|
123
|
+
const node = HyperNode.make("div", {
|
|
124
|
+
"data-config": {
|
|
125
|
+
user: { name: "John", active: true },
|
|
126
|
+
settings: { theme: "dark" },
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const html = HyperHtml.renderToString(node)
|
|
131
|
+
|
|
132
|
+
test
|
|
133
|
+
.expect(html)
|
|
134
|
+
.toBe(
|
|
135
|
+
"<div data-config=\"{"user":{"name":"John","active":true},"settings":{"theme":"dark"}}\"></div>",
|
|
136
|
+
)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test.it("data-* string values are not JSON stringified", () => {
|
|
140
|
+
const node = HyperNode.make("div", {
|
|
141
|
+
"data-value": "hello world",
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const html = HyperHtml.renderToString(node)
|
|
145
|
+
|
|
146
|
+
test
|
|
147
|
+
.expect(html)
|
|
148
|
+
.toBe("<div data-value=\"hello world\"></div>")
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test.it("non-data attributes with object values are not JSON stringified", () => {
|
|
152
|
+
const node = HyperNode.make("div", {
|
|
153
|
+
style: "color: red",
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const html = HyperHtml.renderToString(node)
|
|
157
|
+
|
|
158
|
+
test
|
|
159
|
+
.expect(html)
|
|
160
|
+
.toBe("<div style=\"color: red\"></div>")
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test.it("script with function child renders as IIFE", () => {
|
|
164
|
+
const handler = (window: Window) => {
|
|
165
|
+
console.log("Hello from", window.document.title)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const node = HyperNode.make("script", {
|
|
169
|
+
children: handler,
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
const html = HyperHtml.renderToString(node)
|
|
173
|
+
|
|
174
|
+
test
|
|
175
|
+
.expect(html)
|
|
176
|
+
.toBe(`<script>(${handler.toString()})(window)</script>`)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test.it("script with arrow function child renders as IIFE", () => {
|
|
180
|
+
const node = HyperNode.make("script", {
|
|
181
|
+
children: (window: Window) => {
|
|
182
|
+
window.alert("test")
|
|
183
|
+
},
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
const html = HyperHtml.renderToString(node)
|
|
187
|
+
|
|
188
|
+
test
|
|
189
|
+
.expect(html)
|
|
190
|
+
.toContain("<script>(")
|
|
191
|
+
test
|
|
192
|
+
.expect(html)
|
|
193
|
+
.toContain(")(window)</script>")
|
|
194
|
+
test
|
|
195
|
+
.expect(html)
|
|
196
|
+
.toContain("window.alert")
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
test.it("script with string child renders normally", () => {
|
|
200
|
+
const node = HyperNode.make("script", {
|
|
201
|
+
children: "console.log('hello')",
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
const html = HyperHtml.renderToString(node)
|
|
205
|
+
|
|
206
|
+
test
|
|
207
|
+
.expect(html)
|
|
208
|
+
.toBe("<script>console.log('hello')</script>")
|
|
209
|
+
})
|
package/src/hyper/HyperHtml.ts
CHANGED
|
@@ -120,8 +120,13 @@ export function renderToString(
|
|
|
120
120
|
result += ` ${esc(key)}`
|
|
121
121
|
} else {
|
|
122
122
|
const resolvedKey = key === "className" ? "class" : key
|
|
123
|
+
const value = props[key]
|
|
123
124
|
|
|
124
|
-
|
|
125
|
+
if (key.startsWith("data-") && typeof value === "object") {
|
|
126
|
+
result += ` ${esc(resolvedKey)}="${esc(JSON.stringify(value))}"`
|
|
127
|
+
} else {
|
|
128
|
+
result += ` ${esc(resolvedKey)}="${esc(value)}"`
|
|
129
|
+
}
|
|
125
130
|
}
|
|
126
131
|
}
|
|
127
132
|
}
|
|
@@ -139,7 +144,10 @@ export function renderToString(
|
|
|
139
144
|
result += html
|
|
140
145
|
} else {
|
|
141
146
|
const children = props.children
|
|
142
|
-
|
|
147
|
+
|
|
148
|
+
if (type === "script" && typeof children === "function") {
|
|
149
|
+
result += `(${children.toString()})(window)`
|
|
150
|
+
} else if (Array.isArray(children)) {
|
|
143
151
|
for (let i = children.length - 1; i >= 0; i--) {
|
|
144
152
|
stack.push(children[i])
|
|
145
153
|
}
|