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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/package.json +57 -0
  4. package/src/Bundle.ts +167 -0
  5. package/src/BundleFiles.ts +174 -0
  6. package/src/BundleHttp.test.ts +160 -0
  7. package/src/BundleHttp.ts +259 -0
  8. package/src/Commander.test.ts +1378 -0
  9. package/src/Commander.ts +672 -0
  10. package/src/Datastar.test.ts +267 -0
  11. package/src/Datastar.ts +68 -0
  12. package/src/Effect_HttpRouter.test.ts +570 -0
  13. package/src/EncryptedCookies.test.ts +427 -0
  14. package/src/EncryptedCookies.ts +451 -0
  15. package/src/FileHttpRouter.test.ts +207 -0
  16. package/src/FileHttpRouter.ts +122 -0
  17. package/src/FileRouter.ts +405 -0
  18. package/src/FileRouterCodegen.test.ts +598 -0
  19. package/src/FileRouterCodegen.ts +251 -0
  20. package/src/FileRouter_files.test.ts +64 -0
  21. package/src/FileRouter_path.test.ts +132 -0
  22. package/src/FileRouter_tree.test.ts +126 -0
  23. package/src/FileSystemExtra.ts +102 -0
  24. package/src/HttpAppExtra.ts +127 -0
  25. package/src/Hyper.ts +194 -0
  26. package/src/HyperHtml.test.ts +90 -0
  27. package/src/HyperHtml.ts +139 -0
  28. package/src/HyperNode.ts +37 -0
  29. package/src/JsModule.test.ts +14 -0
  30. package/src/JsModule.ts +116 -0
  31. package/src/PublicDirectory.test.ts +280 -0
  32. package/src/PublicDirectory.ts +108 -0
  33. package/src/Route.test.ts +873 -0
  34. package/src/Route.ts +992 -0
  35. package/src/Router.ts +80 -0
  36. package/src/SseHttpResponse.ts +55 -0
  37. package/src/Start.ts +133 -0
  38. package/src/StartApp.ts +43 -0
  39. package/src/StartHttp.ts +42 -0
  40. package/src/StreamExtra.ts +146 -0
  41. package/src/TestHttpClient.test.ts +54 -0
  42. package/src/TestHttpClient.ts +100 -0
  43. package/src/bun/BunBundle.test.ts +277 -0
  44. package/src/bun/BunBundle.ts +309 -0
  45. package/src/bun/BunBundle_imports.test.ts +50 -0
  46. package/src/bun/BunFullstackServer.ts +45 -0
  47. package/src/bun/BunFullstackServer_httpServer.ts +541 -0
  48. package/src/bun/BunImportTrackerPlugin.test.ts +77 -0
  49. package/src/bun/BunImportTrackerPlugin.ts +97 -0
  50. package/src/bun/BunTailwindPlugin.test.ts +335 -0
  51. package/src/bun/BunTailwindPlugin.ts +322 -0
  52. package/src/bun/BunVirtualFilesPlugin.ts +59 -0
  53. package/src/bun/index.ts +4 -0
  54. package/src/client/Overlay.ts +34 -0
  55. package/src/client/ScrollState.ts +120 -0
  56. package/src/client/index.ts +101 -0
  57. package/src/index.ts +24 -0
  58. package/src/jsx-datastar.d.ts +63 -0
  59. package/src/jsx-runtime.ts +23 -0
  60. package/src/jsx.d.ts +4402 -0
  61. package/src/testing.ts +55 -0
  62. package/src/x/cloudflare/CloudflareTunnel.ts +110 -0
  63. package/src/x/cloudflare/index.ts +1 -0
  64. package/src/x/datastar/Datastar.test.ts +267 -0
  65. package/src/x/datastar/Datastar.ts +68 -0
  66. package/src/x/datastar/index.ts +4 -0
  67. package/src/x/datastar/jsx-datastar.d.ts +63 -0
package/src/testing.ts ADDED
@@ -0,0 +1,55 @@
1
+ import * as Array from "effect/Array"
2
+ import * as Effect from "effect/Effect"
3
+ import * as Function from "effect/Function"
4
+ import * as Layer from "effect/Layer"
5
+ import * as Logger from "effect/Logger"
6
+ import * as Scope from "effect/Scope"
7
+ import type { YieldWrap } from "effect/Utils"
8
+
9
+ /**
10
+ * Creates a scoped Effects and runs is asynchronously.
11
+ * Useful for testing.
12
+ */
13
+ export const effectFn = <RL>(layer?: Layer.Layer<RL, any>) =>
14
+ <
15
+ Eff extends YieldWrap<Effect.Effect<any, any, RE>>,
16
+ AEff,
17
+ RE extends RL | Scope.Scope,
18
+ >(
19
+ f: () => Generator<Eff, AEff, never>,
20
+ ): Promise<void> =>
21
+ Function.pipe(
22
+ Effect.gen(f),
23
+ Effect.scoped,
24
+ Effect.provide(Logger.pretty),
25
+ Effect.provide(layer ?? Layer.empty),
26
+ // @ts-expect-error will have to figure out how to clear deps
27
+ Effect.runPromise,
28
+ v => v.then(() => {}, clearStackTraces),
29
+ )
30
+
31
+ /*
32
+ * When effect fails, instead of throwing FiberFailure,
33
+ * throw a plain Error with the strack trace and hides
34
+ * effect internals.
35
+ * Otherwise, at least on Bun, the strack trace is repeated,
36
+ * with some junks in between taking half of the screen.
37
+ *
38
+ * Direct children that starts with a dot are excluded because
39
+ * some tools, like effect-start, use it to generate temporary
40
+ * files that are then loaded into a runtime.
41
+ */
42
+ const clearStackTraces = (err: any | Error) => {
43
+ const ExternalStackTraceLineRegexp = /\(.*\/node_modules\/[^\.]/
44
+
45
+ const newErr = new Error(err.message)
46
+ const stack: string = err.stack ?? ""
47
+
48
+ newErr.stack = Function.pipe(
49
+ stack.split("\n"),
50
+ Array.takeWhile(s => !ExternalStackTraceLineRegexp.test(s)),
51
+ Array.join("\n"),
52
+ )
53
+
54
+ throw newErr
55
+ }
@@ -0,0 +1,110 @@
1
+ import {
2
+ Command,
3
+ HttpServer,
4
+ } from "@effect/platform"
5
+ import {
6
+ Config,
7
+ Effect,
8
+ identity,
9
+ Layer,
10
+ LogLevel,
11
+ Option,
12
+ pipe,
13
+ Stream,
14
+ String,
15
+ } from "effect"
16
+
17
+ /**
18
+ * Starts Cloudflare tunnel using cloudflared cli.
19
+ */
20
+ export const start = (opts: {
21
+ command?: string
22
+ tunnelName: string
23
+ tunnelUrl?: string
24
+ cleanLogs?: false
25
+ logLevel?: LogLevel.LogLevel
26
+ logPrefix?: string
27
+ }) =>
28
+ Effect.gen(function*() {
29
+ const logPrefix = String.isString(opts.logPrefix)
30
+ ? opts.logPrefix
31
+ : "CloudflareTunnel: "
32
+ const args: string[] = [
33
+ "tunnel",
34
+ "run",
35
+ opts.tunnelUrl
36
+ ? [
37
+ "--url",
38
+ opts.tunnelUrl,
39
+ ]
40
+ : [],
41
+ opts.tunnelName,
42
+ ]
43
+ .flatMap(v => v)
44
+
45
+ const process = yield* pipe(
46
+ Command.make(opts.command ?? "cloudflared", ...args),
47
+ Command.start,
48
+ )
49
+
50
+ yield* Effect.logInfo(
51
+ `Cloudflare tunnel started name=${opts.tunnelName} pid=${process.pid} tunnelUrl=${
52
+ opts.tunnelUrl ?? "<empty>"
53
+ }`,
54
+ )
55
+
56
+ yield* pipe(
57
+ Stream.merge(
58
+ process.stdout,
59
+ process.stderr,
60
+ ),
61
+ Stream.decodeText("utf-8"),
62
+ Stream.splitLines,
63
+ opts.cleanLogs ?? true
64
+ ? Stream.map(v =>
65
+ v.replace(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\s\w+\s/, "")
66
+ )
67
+ : identity,
68
+ logPrefix
69
+ ? Stream.map(v => logPrefix + v)
70
+ : identity,
71
+ Stream.runForEach(v =>
72
+ Effect.logWithLevel(opts.logLevel ?? LogLevel.Debug, v)
73
+ ),
74
+ )
75
+ })
76
+
77
+ export const layer = () =>
78
+ Layer.scopedDiscard(Effect.gen(function*() {
79
+ const tunnelName = yield* pipe(
80
+ Config.string("CLOUDFLARE_TUNNEL_NAME"),
81
+ Config.option,
82
+ Effect.andThen(Option.getOrUndefined),
83
+ )
84
+ const tunnelUrl = yield* pipe(
85
+ Config.string("CLOUDFLARE_TUNNEL_URL"),
86
+ Config.option,
87
+ Effect.andThen(Option.getOrUndefined),
88
+ )
89
+
90
+ if (!tunnelName) {
91
+ yield* Effect.logWarning("CLOUDFLARE_TUNNEL_NAME not provided. Skipping.")
92
+
93
+ return
94
+ }
95
+
96
+ yield* Effect
97
+ .forkScoped(
98
+ pipe(
99
+ start({
100
+ tunnelName,
101
+ tunnelUrl,
102
+ }),
103
+ ),
104
+ )
105
+ .pipe(
106
+ Effect.catchAll(err =>
107
+ Effect.logError("Cloudflare tunnel failed", err)
108
+ ),
109
+ )
110
+ }))
@@ -0,0 +1 @@
1
+ export * as CloudflareTunnel from "./CloudflareTunnel.ts"
@@ -0,0 +1,267 @@
1
+ import * as t from "bun:test"
2
+ import * as HyperHtml from "../../HyperHtml.ts"
3
+ import * as HyperNode from "../../HyperNode.ts"
4
+ import { jsx } from "../../jsx-runtime.ts"
5
+ import * as Datastar from "./Datastar.ts"
6
+
7
+ t.it("data-signals object serialization", () => {
8
+ const node = HyperNode.make("div", {
9
+ "data-signals": { foo: 1, bar: { baz: "hello" } } as any,
10
+ })
11
+
12
+ const html = HyperHtml.renderToString(node, Datastar.HyperHooks)
13
+
14
+ t
15
+ .expect(html)
16
+ .toBe(
17
+ "<div data-signals=\"{&quot;foo&quot;:1,&quot;bar&quot;:{&quot;baz&quot;:&quot;hello&quot;}}\"></div>",
18
+ )
19
+ })
20
+
21
+ t.it("data-signals string passthrough", () => {
22
+ const node = HyperNode.make("div", {
23
+ "data-signals": "$mySignal",
24
+ })
25
+
26
+ const html = HyperHtml.renderToString(node, Datastar.HyperHooks)
27
+
28
+ t
29
+ .expect(html)
30
+ .toBe("<div data-signals=\"$mySignal\"></div>")
31
+ })
32
+
33
+ t.it("data-signals-* object serialization", () => {
34
+ const node = HyperNode.make("div", {
35
+ "data-signals-user": { name: "John", age: 30 } as any,
36
+ })
37
+
38
+ const html = HyperHtml.renderToString(node, Datastar.HyperHooks)
39
+
40
+ t
41
+ .expect(html)
42
+ .toBe(
43
+ "<div data-signals-user=\"{&quot;name&quot;:&quot;John&quot;,&quot;age&quot;:30}\"></div>",
44
+ )
45
+ })
46
+
47
+ t.it("non-data attributes unchanged", () => {
48
+ const node = HyperNode.make("div", {
49
+ id: "test",
50
+ class: "my-class",
51
+ "data-text": "$count",
52
+ "data-signals": { count: 0 } as any,
53
+ })
54
+
55
+ const html = HyperHtml.renderToString(node, Datastar.HyperHooks)
56
+
57
+ t
58
+ .expect(html)
59
+ .toContain("id=\"test\"")
60
+ t
61
+ .expect(html)
62
+ .toContain("class=\"my-class\"")
63
+ t
64
+ .expect(html)
65
+ .toContain("data-text=\"$count\"")
66
+ t
67
+ .expect(html)
68
+ .toContain("data-signals=\"{&quot;count&quot;:0}\"")
69
+ })
70
+
71
+ t.it("null and undefined values ignored", () => {
72
+ const node = HyperNode.make("div", {
73
+ "data-signals": null,
74
+ "data-other": undefined,
75
+ })
76
+
77
+ const html = HyperHtml.renderToString(node, Datastar.HyperHooks)
78
+
79
+ t
80
+ .expect(html)
81
+ .toBe("<div></div>")
82
+ })
83
+
84
+ t.it("complex nested objects serialization", () => {
85
+ const complexObject = {
86
+ user: { name: "John Doe", preferences: { theme: "dark" } },
87
+ items: [1, 2, 3],
88
+ }
89
+
90
+ const node = HyperNode.make("div", {
91
+ "data-signals": complexObject as any,
92
+ })
93
+
94
+ const html = HyperHtml.renderToString(node, Datastar.HyperHooks)
95
+
96
+ t
97
+ .expect(html)
98
+ .toContain("data-signals=")
99
+ t
100
+ .expect(html)
101
+ .toContain("John Doe")
102
+ })
103
+
104
+ t.it("non-signals data attributes serialized", () => {
105
+ const node = HyperNode.make("div", {
106
+ "data-class": { hidden: true, visible: false } as any,
107
+ "data-style": { color: "red", display: "none" } as any,
108
+ "data-show": true as any,
109
+ "data-text": "$count",
110
+ })
111
+
112
+ const html = HyperHtml.renderToString(node, Datastar.HyperHooks)
113
+
114
+ t
115
+ .expect(html)
116
+ .toContain(
117
+ "data-class=\"{&quot;hidden&quot;:true,&quot;visible&quot;:false}\"",
118
+ )
119
+ t
120
+ .expect(html)
121
+ .toContain(
122
+ "data-style=\"{&quot;color&quot;:&quot;red&quot;,&quot;display&quot;:&quot;none&quot;}\"",
123
+ )
124
+ t
125
+ .expect(html)
126
+ .toContain("data-show=\"true\"")
127
+ t
128
+ .expect(html)
129
+ .toContain("data-text=\"$count\"")
130
+ })
131
+
132
+ t.it("data-attr object serialization", () => {
133
+ const node = HyperNode.make("div", {
134
+ "data-attr": { disabled: true, tabindex: 0 } as any,
135
+ })
136
+
137
+ const html = HyperHtml.renderToString(node, Datastar.HyperHooks)
138
+
139
+ t
140
+ .expect(html)
141
+ .toBe(
142
+ "<div data-attr=\"{&quot;disabled&quot;:true,&quot;tabindex&quot;:0}\"></div>",
143
+ )
144
+ })
145
+
146
+ t.it("boolean attributes converted to strings", () => {
147
+ const node = HyperNode.make("div", {
148
+ "data-ignore": false as any,
149
+ "data-ignore-morph": true as any,
150
+ })
151
+
152
+ const html = HyperHtml.renderToString(node, Datastar.HyperHooks)
153
+
154
+ t
155
+ .expect(html)
156
+ .not
157
+ .toContain("data-ignore=\"")
158
+ t
159
+ .expect(html)
160
+ .toContain("data-ignore-morph")
161
+ t
162
+ .expect(html)
163
+ .not
164
+ .toContain("data-ignore-morph=")
165
+ })
166
+
167
+ t.it("data-ignore attributes only present when true", () => {
168
+ const nodeTrue = HyperNode.make("div", {
169
+ "data-ignore": true as any,
170
+ })
171
+
172
+ const nodeFalse = HyperNode.make("div", {
173
+ "data-ignore": false as any,
174
+ })
175
+
176
+ const htmlTrue = HyperHtml.renderToString(nodeTrue, Datastar.HyperHooks)
177
+ const htmlFalse = HyperHtml.renderToString(nodeFalse, Datastar.HyperHooks)
178
+
179
+ t
180
+ .expect(htmlTrue)
181
+ .toContain("data-ignore")
182
+ t
183
+ .expect(htmlTrue)
184
+ .not
185
+ .toContain("data-ignore=")
186
+ t
187
+ .expect(htmlFalse)
188
+ .not
189
+ .toContain("data-ignore")
190
+ })
191
+
192
+ t.it("dynamic attributes with suffixes", () => {
193
+ const node = HyperNode.make("div", {
194
+ "data-class-active": "hidden" as any,
195
+ "data-attr-tabindex": "5" as any,
196
+ "data-style-opacity": "0.5" as any,
197
+ })
198
+
199
+ const html = HyperHtml.renderToString(node, Datastar.HyperHooks)
200
+
201
+ t
202
+ .expect(html)
203
+ .toContain("data-class-active=\"hidden\"")
204
+ t
205
+ .expect(html)
206
+ .toContain("data-attr-tabindex=\"5\"")
207
+ t
208
+ .expect(html)
209
+ .toContain("data-style-opacity=\"0.5\"")
210
+ })
211
+
212
+ t.it("JSX with data-signals object", () => {
213
+ const node = jsx("div", {
214
+ "data-signals": { isOpen: false, count: 42 } as any,
215
+ children: "content",
216
+ })
217
+
218
+ const html = HyperHtml.renderToString(node, Datastar.HyperHooks)
219
+
220
+ t
221
+ .expect(html)
222
+ .toBe(
223
+ "<div data-signals=\"{&quot;isOpen&quot;:false,&quot;count&quot;:42}\">content</div>",
224
+ )
225
+ t
226
+ .expect(html)
227
+ .not
228
+ .toContain("[object Object]")
229
+ })
230
+
231
+ t.it("JSX component returning element with data-signals", () => {
232
+ function TestComponent() {
233
+ return jsx("div", {
234
+ "data-signals": { isOpen: false } as any,
235
+ children: jsx("span", { children: "nested content" }),
236
+ })
237
+ }
238
+
239
+ const node = jsx(TestComponent, {})
240
+
241
+ const html = HyperHtml.renderToString(node, Datastar.HyperHooks)
242
+
243
+ t
244
+ .expect(html)
245
+ .toBe(
246
+ "<div data-signals=\"{&quot;isOpen&quot;:false}\"><span>nested content</span></div>",
247
+ )
248
+ t
249
+ .expect(html)
250
+ .not
251
+ .toContain("[object Object]")
252
+ })
253
+
254
+ t.it("debug hook execution", () => {
255
+ const node = jsx("div", {
256
+ "data-signals": { isOpen: false, count: 42 } as any,
257
+ children: "content",
258
+ })
259
+
260
+ const html = HyperHtml.renderToString(node, Datastar.HyperHooks)
261
+ console.log("Final HTML:", html)
262
+
263
+ t
264
+ .expect(html)
265
+ .not
266
+ .toContain("[object Object]")
267
+ })
@@ -0,0 +1,68 @@
1
+ import * as HyperNode from "../../HyperNode.ts"
2
+
3
+ export const HyperHooks = {
4
+ onNode,
5
+ } as const
6
+
7
+ function onNode(node: HyperNode.HyperNode) {
8
+ const {
9
+ "data-signals": dataSignals,
10
+ "data-class": dataClass,
11
+ "data-attr": dataAttr,
12
+ "data-style": dataStyle,
13
+ "data-show": dataShow,
14
+ "data-ignore": dataIgnore,
15
+ "data-ignore-morph": dataIgnoreMorph,
16
+ } = node.props as any
17
+
18
+ if (typeof dataSignals === "object" && dataSignals !== null) {
19
+ node.props["data-signals"] = JSON.stringify(dataSignals)
20
+ }
21
+
22
+ if (typeof dataClass === "function") {
23
+ node.props["data-class"] = `(${dataClass.toString()})()`
24
+ } else if (typeof dataClass === "object" && dataClass !== null) {
25
+ node.props["data-class"] = JSON.stringify(dataClass)
26
+ }
27
+
28
+ if (typeof dataAttr === "object" && dataAttr !== null) {
29
+ node.props["data-attr"] = JSON.stringify(dataAttr)
30
+ }
31
+
32
+ if (typeof dataStyle === "function") {
33
+ node.props["data-style"] = `(${dataStyle.toString()})()`
34
+ } else if (typeof dataStyle === "object" && dataStyle !== null) {
35
+ node.props["data-style"] = JSON.stringify(dataStyle)
36
+ }
37
+
38
+ if (typeof dataShow === "boolean") {
39
+ node.props["data-show"] = dataShow.toString()
40
+ }
41
+
42
+ if (dataIgnore !== true && dataIgnore !== undefined) {
43
+ delete node.props["data-ignore"]
44
+ }
45
+
46
+ if (dataIgnoreMorph !== true && dataIgnoreMorph !== undefined) {
47
+ delete node.props["data-ignore-morph"]
48
+ }
49
+
50
+ // Handle dynamic attributes with suffixes
51
+ for (const [key, value] of Object.entries(node.props)) {
52
+ if (
53
+ key.startsWith("data-signals-")
54
+ && typeof value === "object"
55
+ && value !== null
56
+ ) {
57
+ node.props[key] = JSON.stringify(value)
58
+ }
59
+
60
+ if (
61
+ key.startsWith("data-on-")
62
+ && typeof value === "function"
63
+ ) {
64
+ // @ts-ignore
65
+ node.props[key] = `(${value.toString()})()`
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,4 @@
1
+ export * as Datastar from "./Datastar.ts"
2
+ export {
3
+ HyperHooks,
4
+ } from "./Datastar.ts"
@@ -0,0 +1,63 @@
1
+ // Datastar object types for specific attributes
2
+ type DatastarSignalsObject = Record<string, any>
3
+ type DatastarClassObject = Record<string, boolean | string>
4
+ type DatastarAttrObject = Record<string, string | boolean | number>
5
+ type DatastarStyleObject = Record<
6
+ string,
7
+ string | number | boolean | null | undefined
8
+ >
9
+
10
+ /**
11
+ * Datastar attributes for reactive web applications
12
+ * @see https://data-star.dev/reference/attributes
13
+ */
14
+ export interface DatastarAttributes {
15
+ // Core attributes that can accept objects (but also strings)
16
+ "data-signals"?: string | DatastarSignalsObject | undefined
17
+ "data-class"?: string | DatastarClassObject | undefined
18
+ "data-attr"?: string | DatastarAttrObject | undefined
19
+ "data-style"?: Function | string | DatastarStyleObject | undefined
20
+
21
+ // Boolean/presence attributes (but also strings)
22
+ "data-show"?: string | boolean | undefined
23
+ "data-ignore"?: string | boolean | undefined
24
+ "data-ignore-morph"?: string | boolean | undefined
25
+
26
+ // All other Datastar attributes as strings only
27
+ "data-bind"?: string | undefined
28
+ "data-computed"?: string | undefined
29
+ "data-effect"?: string | undefined
30
+ "data-indicator"?: string | undefined
31
+ "data-json-signals"?: string | undefined
32
+ "data-on"?: string | undefined
33
+ "data-on-intersect"?: string | undefined
34
+ "data-on-interval"?: string | undefined
35
+ "data-on-load"?: string | undefined
36
+ "data-on-signal-patch"?: string | undefined
37
+ "data-on-signal-patch-filter"?: string | undefined
38
+ "data-preserve-attr"?: string | undefined
39
+ "data-ref"?: string | undefined
40
+ "data-text"?: string | undefined
41
+
42
+ // Pro attributes (strings only)
43
+ "data-animate"?: string | undefined
44
+ "data-custom-validity"?: string | undefined
45
+ "data-on-raf"?: string | undefined
46
+ "data-on-resize"?: string | undefined
47
+ "data-persist"?: string | undefined
48
+ "data-query-string"?: string | undefined
49
+ "data-replace-url"?: string | undefined
50
+ "data-scroll-into-view"?: string | undefined
51
+ "data-view-transition"?: string | undefined
52
+
53
+ // Dynamic attributes with suffixes
54
+ [key: `data-signals-${string}`]: string | undefined
55
+ [key: `data-class-${string}`]: string | undefined
56
+ [key: `data-attr-${string}`]: string | undefined
57
+ [key: `data-style-${string}`]: string | undefined
58
+ [key: `data-bind-${string}`]: string | undefined
59
+ [key: `data-computed-${string}`]: string | undefined
60
+ [key: `data-indicator-${string}`]: string | undefined
61
+ [key: `data-ref-${string}`]: string | undefined
62
+ [key: `data-on-${string}`]: Function | string | undefined
63
+ }