effect-start 0.9.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/LICENSE +21 -0
- package/README.md +109 -0
- package/package.json +57 -0
- package/src/Bundle.ts +167 -0
- package/src/BundleFiles.ts +174 -0
- package/src/BundleHttp.test.ts +160 -0
- package/src/BundleHttp.ts +259 -0
- package/src/Commander.test.ts +1378 -0
- package/src/Commander.ts +672 -0
- package/src/Datastar.test.ts +267 -0
- package/src/Datastar.ts +68 -0
- package/src/Effect_HttpRouter.test.ts +570 -0
- package/src/EncryptedCookies.test.ts +427 -0
- package/src/EncryptedCookies.ts +451 -0
- package/src/FileHttpRouter.test.ts +207 -0
- package/src/FileHttpRouter.ts +122 -0
- package/src/FileRouter.ts +405 -0
- package/src/FileRouterCodegen.test.ts +598 -0
- package/src/FileRouterCodegen.ts +251 -0
- package/src/FileRouter_files.test.ts +64 -0
- package/src/FileRouter_path.test.ts +132 -0
- package/src/FileRouter_tree.test.ts +126 -0
- package/src/FileSystemExtra.ts +102 -0
- package/src/HttpAppExtra.ts +127 -0
- package/src/Hyper.ts +194 -0
- package/src/HyperHtml.test.ts +90 -0
- package/src/HyperHtml.ts +139 -0
- package/src/HyperNode.ts +37 -0
- package/src/JsModule.test.ts +14 -0
- package/src/JsModule.ts +116 -0
- package/src/PublicDirectory.test.ts +280 -0
- package/src/PublicDirectory.ts +108 -0
- package/src/Route.test.ts +873 -0
- package/src/Route.ts +992 -0
- package/src/Router.ts +80 -0
- package/src/SseHttpResponse.ts +55 -0
- package/src/Start.ts +133 -0
- package/src/StartApp.ts +43 -0
- package/src/StartHttp.ts +42 -0
- package/src/StreamExtra.ts +146 -0
- package/src/TestHttpClient.test.ts +54 -0
- package/src/TestHttpClient.ts +100 -0
- package/src/bun/BunBundle.test.ts +277 -0
- package/src/bun/BunBundle.ts +309 -0
- package/src/bun/BunBundle_imports.test.ts +50 -0
- package/src/bun/BunFullstackServer.ts +45 -0
- package/src/bun/BunFullstackServer_httpServer.ts +541 -0
- package/src/bun/BunImportTrackerPlugin.test.ts +77 -0
- package/src/bun/BunImportTrackerPlugin.ts +97 -0
- package/src/bun/BunTailwindPlugin.test.ts +335 -0
- package/src/bun/BunTailwindPlugin.ts +322 -0
- package/src/bun/BunVirtualFilesPlugin.ts +59 -0
- package/src/bun/index.ts +4 -0
- package/src/client/Overlay.ts +34 -0
- package/src/client/ScrollState.ts +120 -0
- package/src/client/index.ts +101 -0
- package/src/index.ts +24 -0
- package/src/jsx-datastar.d.ts +63 -0
- package/src/jsx-runtime.ts +23 -0
- package/src/jsx.d.ts +4402 -0
- package/src/testing.ts +55 -0
- package/src/x/cloudflare/CloudflareTunnel.ts +110 -0
- package/src/x/cloudflare/index.ts +1 -0
- package/src/x/datastar/Datastar.test.ts +267 -0
- package/src/x/datastar/Datastar.ts +68 -0
- package/src/x/datastar/index.ts +4 -0
- package/src/x/datastar/jsx-datastar.d.ts +63 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import * as BunContext from "@effect/platform-bun/BunContext"
|
|
2
|
+
import * as BunHttpServer from "@effect/platform-bun/BunHttpServer"
|
|
3
|
+
import * as HttpRouter from "@effect/platform/HttpRouter"
|
|
4
|
+
import * as HttpServer from "@effect/platform/HttpServer"
|
|
5
|
+
import * as t from "bun:test"
|
|
6
|
+
import * as Effect from "effect/Effect"
|
|
7
|
+
import * as Layer from "effect/Layer"
|
|
8
|
+
import * as NFS from "node:fs/promises"
|
|
9
|
+
import * as NOS from "node:os"
|
|
10
|
+
import * as NPath from "node:path"
|
|
11
|
+
import * as Bundle from "../Bundle.ts"
|
|
12
|
+
import * as BundleHttp from "../BundleHttp.ts"
|
|
13
|
+
import * as TestHttpClient from "../TestHttpClient.ts"
|
|
14
|
+
import * as BunBundle from "./BunBundle.ts"
|
|
15
|
+
|
|
16
|
+
t.describe("BunBundle manifest structure", () => {
|
|
17
|
+
t.it("should generate manifest with inputs and outputs arrays", async () => {
|
|
18
|
+
const tmpDir = await NFS.mkdtemp(
|
|
19
|
+
NPath.join(NOS.tmpdir(), "effect-start-test-"),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const htmlContent = `<!DOCTYPE html>
|
|
24
|
+
<html>
|
|
25
|
+
<head><title>Test</title></head>
|
|
26
|
+
<body>
|
|
27
|
+
<div id="app"></div>
|
|
28
|
+
<script src="./index.ts" type="module"></script>
|
|
29
|
+
</body>
|
|
30
|
+
</html>`
|
|
31
|
+
|
|
32
|
+
const jsContent = `console.log("Hello from test bundle");
|
|
33
|
+
export const greeting = "Hello World";`
|
|
34
|
+
|
|
35
|
+
const htmlPath = NPath.join(tmpDir, "index.html")
|
|
36
|
+
const jsPath = NPath.join(tmpDir, "index.ts")
|
|
37
|
+
|
|
38
|
+
await NFS.writeFile(htmlPath, htmlContent)
|
|
39
|
+
await NFS.writeFile(jsPath, jsContent)
|
|
40
|
+
|
|
41
|
+
const bundle = await Effect.runPromise(
|
|
42
|
+
BunBundle.buildClient({
|
|
43
|
+
entrypoints: [htmlPath],
|
|
44
|
+
}),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
t
|
|
48
|
+
.expect(bundle.entrypoints)
|
|
49
|
+
.toBeObject()
|
|
50
|
+
t
|
|
51
|
+
.expect(bundle.artifacts)
|
|
52
|
+
.toBeArray()
|
|
53
|
+
|
|
54
|
+
t
|
|
55
|
+
.expect(Object.keys(bundle.entrypoints).length)
|
|
56
|
+
.toBe(1)
|
|
57
|
+
t
|
|
58
|
+
.expect(bundle.artifacts.length)
|
|
59
|
+
.toBe(3)
|
|
60
|
+
|
|
61
|
+
const entrypointKeys = Object.keys(bundle.entrypoints)
|
|
62
|
+
const firstEntrypoint = entrypointKeys[0]
|
|
63
|
+
|
|
64
|
+
t
|
|
65
|
+
.expect(firstEntrypoint)
|
|
66
|
+
.toBeString()
|
|
67
|
+
t
|
|
68
|
+
.expect(bundle.entrypoints[firstEntrypoint])
|
|
69
|
+
.toBeString()
|
|
70
|
+
|
|
71
|
+
const firstArtifact = bundle.artifacts[0]
|
|
72
|
+
|
|
73
|
+
t
|
|
74
|
+
.expect(firstArtifact)
|
|
75
|
+
.toHaveProperty("path")
|
|
76
|
+
t
|
|
77
|
+
.expect(firstArtifact)
|
|
78
|
+
.toHaveProperty("type")
|
|
79
|
+
t
|
|
80
|
+
.expect(firstArtifact)
|
|
81
|
+
.toHaveProperty("size")
|
|
82
|
+
|
|
83
|
+
t
|
|
84
|
+
.expect(firstArtifact.size)
|
|
85
|
+
.toBeGreaterThan(0)
|
|
86
|
+
} finally {
|
|
87
|
+
await NFS.rm(tmpDir, {
|
|
88
|
+
recursive: true,
|
|
89
|
+
force: true,
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
t.it("should serve manifest via HTTP with correct structure", async () => {
|
|
95
|
+
const tmpDir = await NFS.mkdtemp(
|
|
96
|
+
NPath.join(NOS.tmpdir(), "effect-start-test-"),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const htmlContent = `<!DOCTYPE html>
|
|
101
|
+
<html>
|
|
102
|
+
<head><title>Test App</title></head>
|
|
103
|
+
<body><h1>Test</h1></body>
|
|
104
|
+
</html>`
|
|
105
|
+
|
|
106
|
+
const htmlPath = NPath.join(tmpDir, "app.html")
|
|
107
|
+
|
|
108
|
+
await NFS.writeFile(htmlPath, htmlContent)
|
|
109
|
+
|
|
110
|
+
const testLayer = Layer.effect(
|
|
111
|
+
Bundle.ClientBundle,
|
|
112
|
+
BunBundle.buildClient({
|
|
113
|
+
entrypoints: [htmlPath],
|
|
114
|
+
}),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
const result = await Effect.runPromise(
|
|
118
|
+
Effect
|
|
119
|
+
.scoped(
|
|
120
|
+
Effect.gen(function*() {
|
|
121
|
+
const App = HttpRouter.empty.pipe(
|
|
122
|
+
HttpRouter.mountApp(
|
|
123
|
+
"/_bundle",
|
|
124
|
+
BundleHttp.httpApp(),
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
const Client = TestHttpClient.make(App)
|
|
129
|
+
|
|
130
|
+
const response = yield* Client.get("/_bundle/manifest.json")
|
|
131
|
+
|
|
132
|
+
const manifestText = yield* response.text
|
|
133
|
+
|
|
134
|
+
return JSON.parse(manifestText)
|
|
135
|
+
}),
|
|
136
|
+
)
|
|
137
|
+
.pipe(
|
|
138
|
+
Effect.provide(testLayer),
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
t
|
|
143
|
+
.expect(result)
|
|
144
|
+
.toHaveProperty("entrypoints")
|
|
145
|
+
t
|
|
146
|
+
.expect(result)
|
|
147
|
+
.toHaveProperty("artifacts")
|
|
148
|
+
|
|
149
|
+
t
|
|
150
|
+
.expect(result.entrypoints)
|
|
151
|
+
.toBeObject()
|
|
152
|
+
t
|
|
153
|
+
.expect(result.artifacts)
|
|
154
|
+
.toBeArray()
|
|
155
|
+
|
|
156
|
+
t
|
|
157
|
+
.expect(Object.keys(result.entrypoints).length)
|
|
158
|
+
.toBe(1)
|
|
159
|
+
t
|
|
160
|
+
.expect(result.artifacts.length)
|
|
161
|
+
.toBe(3)
|
|
162
|
+
|
|
163
|
+
const entrypointKeys = Object.keys(result.entrypoints)
|
|
164
|
+
const firstKey = entrypointKeys[0]
|
|
165
|
+
|
|
166
|
+
t
|
|
167
|
+
.expect(firstKey)
|
|
168
|
+
.toBeString()
|
|
169
|
+
t
|
|
170
|
+
.expect(result.entrypoints[firstKey])
|
|
171
|
+
.toBeString()
|
|
172
|
+
|
|
173
|
+
const artifact = result.artifacts[0]
|
|
174
|
+
|
|
175
|
+
t
|
|
176
|
+
.expect(artifact)
|
|
177
|
+
.toHaveProperty("path")
|
|
178
|
+
t
|
|
179
|
+
.expect(artifact)
|
|
180
|
+
.toHaveProperty("type")
|
|
181
|
+
t
|
|
182
|
+
.expect(artifact)
|
|
183
|
+
.toHaveProperty("size")
|
|
184
|
+
} finally {
|
|
185
|
+
await NFS.rm(tmpDir, {
|
|
186
|
+
recursive: true,
|
|
187
|
+
force: true,
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
t.it("should resolve entrypoints to artifacts correctly", async () => {
|
|
193
|
+
const tmpDir = await NFS.mkdtemp(
|
|
194
|
+
NPath.join(NOS.tmpdir(), "effect-start-test-"),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const htmlContent = `<!DOCTYPE html>
|
|
199
|
+
<html><body>Test</body></html>`
|
|
200
|
+
|
|
201
|
+
const htmlPath = NPath.join(tmpDir, "test.html")
|
|
202
|
+
|
|
203
|
+
await NFS.writeFile(htmlPath, htmlContent)
|
|
204
|
+
|
|
205
|
+
const bundle = await Effect.runPromise(
|
|
206
|
+
BunBundle.buildClient({
|
|
207
|
+
entrypoints: [htmlPath],
|
|
208
|
+
}),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
const entrypointKeys = Object.keys(bundle.entrypoints)
|
|
212
|
+
const firstEntrypoint = entrypointKeys[0]
|
|
213
|
+
const expectedOutput = bundle.entrypoints[firstEntrypoint]
|
|
214
|
+
const resolvedOutput = bundle.resolve(firstEntrypoint)
|
|
215
|
+
|
|
216
|
+
t
|
|
217
|
+
.expect(resolvedOutput)
|
|
218
|
+
.toBe(expectedOutput)
|
|
219
|
+
|
|
220
|
+
const artifact = bundle.getArtifact(resolvedOutput!)
|
|
221
|
+
|
|
222
|
+
t
|
|
223
|
+
.expect(artifact)
|
|
224
|
+
.not
|
|
225
|
+
.toBeNull()
|
|
226
|
+
|
|
227
|
+
t
|
|
228
|
+
.expect(artifact)
|
|
229
|
+
.toBeTruthy()
|
|
230
|
+
} finally {
|
|
231
|
+
await NFS.rm(tmpDir, {
|
|
232
|
+
recursive: true,
|
|
233
|
+
force: true,
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
t.it("should include all artifact metadata", async () => {
|
|
239
|
+
const tmpDir = await NFS.mkdtemp(
|
|
240
|
+
NPath.join(NOS.tmpdir(), "effect-start-test-"),
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
const jsContent = `export const value = 42;`
|
|
245
|
+
const jsPath = NPath.join(tmpDir, "module.ts")
|
|
246
|
+
|
|
247
|
+
await NFS.writeFile(jsPath, jsContent)
|
|
248
|
+
|
|
249
|
+
const bundle = await Effect.runPromise(
|
|
250
|
+
BunBundle.buildClient({
|
|
251
|
+
entrypoints: [jsPath],
|
|
252
|
+
}),
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
const artifact = bundle.artifacts[0]
|
|
256
|
+
|
|
257
|
+
t
|
|
258
|
+
.expect(artifact.path)
|
|
259
|
+
.toBeString()
|
|
260
|
+
t
|
|
261
|
+
.expect(artifact.type)
|
|
262
|
+
.toBeString()
|
|
263
|
+
t
|
|
264
|
+
.expect(artifact.size)
|
|
265
|
+
.toBeNumber()
|
|
266
|
+
|
|
267
|
+
t
|
|
268
|
+
.expect(artifact.type)
|
|
269
|
+
.toContain("javascript")
|
|
270
|
+
} finally {
|
|
271
|
+
await NFS.rm(tmpDir, {
|
|
272
|
+
recursive: true,
|
|
273
|
+
force: true,
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
})
|
|
277
|
+
})
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BuildConfig,
|
|
3
|
+
BuildOutput,
|
|
4
|
+
} from "bun"
|
|
5
|
+
import {
|
|
6
|
+
Array,
|
|
7
|
+
Context,
|
|
8
|
+
Effect,
|
|
9
|
+
Iterable,
|
|
10
|
+
Layer,
|
|
11
|
+
pipe,
|
|
12
|
+
PubSub,
|
|
13
|
+
Record,
|
|
14
|
+
Stream,
|
|
15
|
+
SynchronizedRef,
|
|
16
|
+
} from "effect"
|
|
17
|
+
import * as NPath from "node:path"
|
|
18
|
+
import type {
|
|
19
|
+
BundleContext,
|
|
20
|
+
BundleManifest,
|
|
21
|
+
} from "../Bundle.ts"
|
|
22
|
+
import * as Bundle from "../Bundle.ts"
|
|
23
|
+
import * as FileSystemExtra from "../FileSystemExtra.ts"
|
|
24
|
+
import { BunImportTrackerPlugin } from "./index.ts"
|
|
25
|
+
|
|
26
|
+
export type BuildOptions = Omit<
|
|
27
|
+
BuildConfig,
|
|
28
|
+
"outdir"
|
|
29
|
+
>
|
|
30
|
+
|
|
31
|
+
export const buildClient = (
|
|
32
|
+
config: BuildOptions | string,
|
|
33
|
+
) => {
|
|
34
|
+
if (typeof config === "string") {
|
|
35
|
+
config = {
|
|
36
|
+
entrypoints: [config],
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const baseConfig: Partial<BuildOptions> = {
|
|
41
|
+
sourcemap: "linked",
|
|
42
|
+
naming: {
|
|
43
|
+
entry: "[name]-[hash].[ext]",
|
|
44
|
+
chunk: "[name]-[hash].[ext]",
|
|
45
|
+
asset: "[name]-[hash].[ext]",
|
|
46
|
+
},
|
|
47
|
+
packages: "bundle",
|
|
48
|
+
publicPath: "/_bundle/",
|
|
49
|
+
} as const
|
|
50
|
+
const resolvedConfig = {
|
|
51
|
+
...baseConfig,
|
|
52
|
+
target: "browser" as const,
|
|
53
|
+
...config,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return build(resolvedConfig)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const buildServer = (
|
|
60
|
+
config: BuildOptions | string,
|
|
61
|
+
) => {
|
|
62
|
+
if (typeof config === "string") {
|
|
63
|
+
config = {
|
|
64
|
+
entrypoints: [config],
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const baseConfig: Partial<BuildOptions> = {
|
|
69
|
+
sourcemap: "linked",
|
|
70
|
+
naming: {
|
|
71
|
+
entry: "[dir]/[name]-[hash].[ext]",
|
|
72
|
+
chunk: "[name]-[hash].[ext]",
|
|
73
|
+
asset: "[name]-[hash].[ext]",
|
|
74
|
+
},
|
|
75
|
+
packages: "bundle",
|
|
76
|
+
} as const
|
|
77
|
+
const resolvedConfig = {
|
|
78
|
+
...baseConfig,
|
|
79
|
+
target: "bun" as const,
|
|
80
|
+
...config,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return build(resolvedConfig)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Given a config, build a bundle and returns every time when effect is executed.
|
|
88
|
+
*/
|
|
89
|
+
export function build(
|
|
90
|
+
config: BuildOptions,
|
|
91
|
+
): Effect.Effect<BundleContext, Bundle.BundleError> {
|
|
92
|
+
return Effect.gen(function*() {
|
|
93
|
+
const output = yield* buildBun(config)
|
|
94
|
+
const manifest = generateManifestfromBunBundle(
|
|
95
|
+
config,
|
|
96
|
+
output,
|
|
97
|
+
)
|
|
98
|
+
const artifactsMap = Record.fromIterableBy(
|
|
99
|
+
output.outputs,
|
|
100
|
+
(v) => v.path.replace(/^\.\//, ""),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const resolve = (path: string) => {
|
|
104
|
+
return manifest.entrypoints[path] ?? null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const getArtifact = (path: string): Blob | null => {
|
|
108
|
+
return artifactsMap[resolve(path)]
|
|
109
|
+
?? artifactsMap[path]
|
|
110
|
+
?? null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
...manifest,
|
|
115
|
+
resolve,
|
|
116
|
+
getArtifact,
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function layer<T>(
|
|
122
|
+
tag: Context.Tag<T, BundleContext>,
|
|
123
|
+
config: BuildOptions,
|
|
124
|
+
) {
|
|
125
|
+
return Layer.effect(tag, build(config))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function layerDev<T>(
|
|
129
|
+
tag: Context.Tag<T, BundleContext>,
|
|
130
|
+
config: BuildOptions,
|
|
131
|
+
) {
|
|
132
|
+
return Layer.scoped(
|
|
133
|
+
tag,
|
|
134
|
+
Effect.gen(function*() {
|
|
135
|
+
const loadRefKey = "_loadRef"
|
|
136
|
+
const sharedBundle = yield* build(config)
|
|
137
|
+
|
|
138
|
+
const loadRef = yield* SynchronizedRef.make(null)
|
|
139
|
+
sharedBundle[loadRefKey] = loadRef
|
|
140
|
+
sharedBundle.events = yield* PubSub.unbounded<Bundle.BundleEvent>()
|
|
141
|
+
|
|
142
|
+
yield* Effect.fork(
|
|
143
|
+
pipe(
|
|
144
|
+
FileSystemExtra.watchSource({
|
|
145
|
+
filter: FileSystemExtra.filterSourceFiles,
|
|
146
|
+
}),
|
|
147
|
+
Stream.map(v =>
|
|
148
|
+
({
|
|
149
|
+
_tag: "Change",
|
|
150
|
+
path: v.path,
|
|
151
|
+
}) as Bundle.BundleEvent
|
|
152
|
+
),
|
|
153
|
+
Stream.onError(err =>
|
|
154
|
+
Effect.logError("Error while watching files", err)
|
|
155
|
+
),
|
|
156
|
+
Stream.runForEach((v) =>
|
|
157
|
+
pipe(
|
|
158
|
+
Effect.gen(function*() {
|
|
159
|
+
yield* Effect.logDebug("Updating bundle: " + tag.key)
|
|
160
|
+
|
|
161
|
+
const newBundle = yield* build(config)
|
|
162
|
+
|
|
163
|
+
Object.assign(sharedBundle, newBundle)
|
|
164
|
+
|
|
165
|
+
// Clean old loaded bundle
|
|
166
|
+
yield* SynchronizedRef.update(loadRef, () => null)
|
|
167
|
+
|
|
168
|
+
// publish event after the built
|
|
169
|
+
if (sharedBundle.events) {
|
|
170
|
+
yield* PubSub.publish(sharedBundle.events, v)
|
|
171
|
+
}
|
|
172
|
+
}),
|
|
173
|
+
Effect.catchAll(err =>
|
|
174
|
+
Effect.gen(function*() {
|
|
175
|
+
yield* Effect.logError(
|
|
176
|
+
"Error while updating bundle",
|
|
177
|
+
err,
|
|
178
|
+
)
|
|
179
|
+
if (sharedBundle.events) {
|
|
180
|
+
yield* PubSub.publish(sharedBundle.events, {
|
|
181
|
+
_tag: "BuildError",
|
|
182
|
+
error: String(err),
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
),
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return sharedBundle
|
|
193
|
+
}),
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Finds common path prefix across provided paths.
|
|
199
|
+
*/
|
|
200
|
+
function getBaseDir(paths: string[]) {
|
|
201
|
+
if (paths.length === 0) return ""
|
|
202
|
+
if (paths.length === 1) return NPath.dirname(paths[0])
|
|
203
|
+
|
|
204
|
+
const segmentsList = paths.map((path) =>
|
|
205
|
+
NPath.dirname(path).split("/").filter(Boolean)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return segmentsList[0]
|
|
209
|
+
.filter((segment, i) => segmentsList.every((segs) => segs[i] === segment))
|
|
210
|
+
.reduce((path, seg) => `${path}/${seg}`, "") ?? ""
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Maps entrypoints to their respective build artifacts.
|
|
215
|
+
* Entrypoint key is trimmed to remove common path prefix.
|
|
216
|
+
*/
|
|
217
|
+
function joinBuildEntrypoints(
|
|
218
|
+
options: BuildOptions,
|
|
219
|
+
output: BuildOutput,
|
|
220
|
+
) {
|
|
221
|
+
const commonPathPrefix = getBaseDir(options.entrypoints) + "/"
|
|
222
|
+
|
|
223
|
+
return pipe(
|
|
224
|
+
Iterable.zip(
|
|
225
|
+
options.entrypoints,
|
|
226
|
+
pipe(
|
|
227
|
+
output.outputs,
|
|
228
|
+
// Filter out source maps to properly map artifacts to entrypoints.
|
|
229
|
+
Iterable.filter((v) =>
|
|
230
|
+
v.kind !== "sourcemap"
|
|
231
|
+
&& !(v.loader === "html" && v.path.endsWith(".js"))
|
|
232
|
+
),
|
|
233
|
+
),
|
|
234
|
+
),
|
|
235
|
+
Iterable.map(([entrypoint, artifact]) => {
|
|
236
|
+
return {
|
|
237
|
+
shortPath: entrypoint.replace(commonPathPrefix, ""),
|
|
238
|
+
fullPath: entrypoint,
|
|
239
|
+
artifact,
|
|
240
|
+
} as const
|
|
241
|
+
}),
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Generate manifest from a build.
|
|
247
|
+
* Useful for SSR and providing source->artifact path mapping.
|
|
248
|
+
*/
|
|
249
|
+
function generateManifestfromBunBundle(
|
|
250
|
+
options: BuildOptions,
|
|
251
|
+
output: BuildOutput,
|
|
252
|
+
imports?: BunImportTrackerPlugin.ImportMap,
|
|
253
|
+
): BundleManifest {
|
|
254
|
+
const entrypointArtifacts = joinBuildEntrypoints(options, output)
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
entrypoints: pipe(
|
|
258
|
+
entrypointArtifacts,
|
|
259
|
+
Iterable.map((v) =>
|
|
260
|
+
[
|
|
261
|
+
v.shortPath,
|
|
262
|
+
v.artifact.path.replace(/^\.\//, ""),
|
|
263
|
+
] as const
|
|
264
|
+
),
|
|
265
|
+
Record.fromEntries,
|
|
266
|
+
),
|
|
267
|
+
|
|
268
|
+
artifacts: pipe(
|
|
269
|
+
output.outputs,
|
|
270
|
+
Iterable.map((v) => {
|
|
271
|
+
// strip './' prefix
|
|
272
|
+
const shortPath = v.path.replace(/^\.\//, "")
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
path: shortPath,
|
|
276
|
+
type: v.type,
|
|
277
|
+
size: v.size,
|
|
278
|
+
hash: v.hash ?? undefined,
|
|
279
|
+
imports: imports?.get(v.path),
|
|
280
|
+
}
|
|
281
|
+
}),
|
|
282
|
+
Array.fromIterable,
|
|
283
|
+
),
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function buildBun(
|
|
288
|
+
config: BuildOptions,
|
|
289
|
+
): Effect.Effect<BuildOutput, Bundle.BundleError, never> {
|
|
290
|
+
return Object.assign(
|
|
291
|
+
Effect.gen(function*() {
|
|
292
|
+
const buildOutput: BuildOutput = yield* Effect.tryPromise({
|
|
293
|
+
try: () => Bun.build(config),
|
|
294
|
+
catch: (err: AggregateError | unknown) => {
|
|
295
|
+
const cause = err instanceof AggregateError
|
|
296
|
+
? err.errors?.[0] ?? err
|
|
297
|
+
: err
|
|
298
|
+
|
|
299
|
+
return new Bundle.BundleError({
|
|
300
|
+
message: "Failed to Bun.build: " + cause,
|
|
301
|
+
cause: cause,
|
|
302
|
+
})
|
|
303
|
+
},
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
return buildOutput
|
|
307
|
+
}),
|
|
308
|
+
)
|
|
309
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as t from "bun:test"
|
|
2
|
+
import { effectFn } from "../testing.ts"
|
|
3
|
+
import * as BunBundle from "./BunBundle.ts"
|
|
4
|
+
import * as BunImportTrackerPlugin from "./BunImportTrackerPlugin.ts"
|
|
5
|
+
|
|
6
|
+
const effect = effectFn()
|
|
7
|
+
|
|
8
|
+
t.it("imports", () =>
|
|
9
|
+
effect(function*() {
|
|
10
|
+
const importTracker = BunImportTrackerPlugin.make()
|
|
11
|
+
yield* BunBundle.build({
|
|
12
|
+
target: "bun",
|
|
13
|
+
plugins: [
|
|
14
|
+
importTracker,
|
|
15
|
+
],
|
|
16
|
+
entrypoints: [
|
|
17
|
+
Bun.fileURLToPath(import.meta.resolve("./BunBundle_imports.test.ts")),
|
|
18
|
+
],
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const [
|
|
22
|
+
e0,
|
|
23
|
+
] = importTracker.state.entries()
|
|
24
|
+
|
|
25
|
+
t
|
|
26
|
+
.expect(
|
|
27
|
+
e0,
|
|
28
|
+
)
|
|
29
|
+
.toEqual([
|
|
30
|
+
"src/bun/BunBundle_imports.test.ts",
|
|
31
|
+
[
|
|
32
|
+
{
|
|
33
|
+
kind: "import-statement",
|
|
34
|
+
path: "bun:test",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
kind: "import-statement",
|
|
38
|
+
path: "src/testing.ts",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
kind: "import-statement",
|
|
42
|
+
path: "src/bun/BunBundle.ts",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
kind: "import-statement",
|
|
46
|
+
path: "src/bun/BunImportTrackerPlugin.ts",
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
])
|
|
50
|
+
}))
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { BunHttpServer } from "@effect/platform-bun"
|
|
2
|
+
import * as Config from "effect/Config"
|
|
3
|
+
import * as Effect from "effect/Effect"
|
|
4
|
+
import * as Fiber from "effect/Fiber"
|
|
5
|
+
import * as Layer from "effect/Layer"
|
|
6
|
+
import * as Option from "effect/Option"
|
|
7
|
+
import * as httpServer from "./BunFullstackServer_httpServer.ts"
|
|
8
|
+
|
|
9
|
+
// As of Bun v1.2.13, these types are not publicy exported.
|
|
10
|
+
type BunServeFuntionOptions = Parameters<
|
|
11
|
+
typeof Bun.serve<any, {}>
|
|
12
|
+
>[0]
|
|
13
|
+
|
|
14
|
+
type DefaultOptions = Parameters<typeof BunHttpServer.make>[0]
|
|
15
|
+
|
|
16
|
+
type Options =
|
|
17
|
+
& DefaultOptions
|
|
18
|
+
& Omit<BunServeFuntionOptions, "fetch" | "error">
|
|
19
|
+
|
|
20
|
+
export const make = (opts: Options) => {
|
|
21
|
+
return Effect.gen(function*() {
|
|
22
|
+
const env = yield* Config
|
|
23
|
+
.string("NODE_ENV")
|
|
24
|
+
.pipe(Config.option)
|
|
25
|
+
|
|
26
|
+
return httpServer.make({
|
|
27
|
+
development: Option.getOrNull(env) === "development",
|
|
28
|
+
...opts,
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const layer = (opts: Options) => {
|
|
34
|
+
return Layer.unwrapEffect(
|
|
35
|
+
Effect.gen(function*() {
|
|
36
|
+
const env = yield* Config.string("NODE_ENV").pipe(Config.option)
|
|
37
|
+
const development = Option.getOrNull(env) !== "development"
|
|
38
|
+
|
|
39
|
+
return httpServer.layer({
|
|
40
|
+
development,
|
|
41
|
+
...opts,
|
|
42
|
+
})
|
|
43
|
+
}),
|
|
44
|
+
)
|
|
45
|
+
}
|