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
@@ -0,0 +1,127 @@
1
+ import {
2
+ HttpRouter,
3
+ HttpServerResponse,
4
+ } from "@effect/platform"
5
+ import * as HttpApp from "@effect/platform/HttpApp"
6
+ import * as HttpMiddleware from "@effect/platform/HttpMiddleware"
7
+ import {
8
+ RequestError,
9
+ RouteNotFound,
10
+ } from "@effect/platform/HttpServerError"
11
+ import {
12
+ Cause,
13
+ Effect,
14
+ Option,
15
+ ParseResult,
16
+ Record,
17
+ } from "effect"
18
+
19
+ /**
20
+ * Groups: function, path
21
+ */
22
+ const StackLinePattern = /^at (.*?) \((.*?)\)/
23
+
24
+ type GraciousError =
25
+ | RouteNotFound
26
+ | ParseResult.ParseError
27
+ | RequestError
28
+ | ParseResult.ParseError
29
+
30
+ export const renderError = (
31
+ error: unknown,
32
+ ) =>
33
+ Effect.gen(function*() {
34
+ let unwrappedError: GraciousError | undefined
35
+
36
+ if (Cause.isCause(error)) {
37
+ const failure = Cause.failureOption(error).pipe(Option.getOrUndefined)
38
+
39
+ if (failure?.["_tag"]) {
40
+ unwrappedError = failure as GraciousError
41
+ }
42
+
43
+ yield* Effect.logError(error)
44
+ }
45
+
46
+ switch (unwrappedError?._tag) {
47
+ case "RouteNotFound":
48
+ return yield* HttpServerResponse.unsafeJson({
49
+ error: {
50
+ _tag: unwrappedError._tag,
51
+ },
52
+ }, {
53
+ status: 404,
54
+ })
55
+ case "RequestError": {
56
+ const message = unwrappedError.reason === "Decode"
57
+ ? "Request body is invalid"
58
+ : undefined
59
+
60
+ return yield* HttpServerResponse.unsafeJson({
61
+ error: {
62
+ _tag: unwrappedError._tag,
63
+ reason: unwrappedError.reason,
64
+ message,
65
+ },
66
+ }, {
67
+ status: 400,
68
+ })
69
+ }
70
+ case "ParseError": {
71
+ const issues = yield* ParseResult.ArrayFormatter.formatIssue(
72
+ unwrappedError.issue,
73
+ )
74
+ const cleanIssues = issues.map(v => Record.remove(v, "_tag"))
75
+
76
+ return yield* HttpServerResponse.unsafeJson({
77
+ error: {
78
+ _tag: unwrappedError._tag,
79
+ issues: cleanIssues,
80
+ },
81
+ }, {
82
+ status: 400,
83
+ })
84
+ }
85
+ }
86
+
87
+ return yield* HttpServerResponse.unsafeJson({
88
+ error: {
89
+ _tag: "UnexpectedError",
90
+ },
91
+ }, {
92
+ status: 500,
93
+ })
94
+ })
95
+
96
+ function extractPrettyStack(stack: string) {
97
+ return stack
98
+ .split("\n")
99
+ .slice(1)
100
+ .map((line) => {
101
+ const match = line.trim().match(StackLinePattern)
102
+
103
+ if (!match) return line
104
+
105
+ const [_, fn, path] = match
106
+ const relativePath = path.replace(process.cwd(), ".")
107
+ return [fn, relativePath]
108
+ })
109
+ .filter(Boolean)
110
+ }
111
+
112
+ export function handleErrors<
113
+ E,
114
+ R,
115
+ >(
116
+ app: HttpApp.Default<E, R>,
117
+ ): HttpApp.Default<Exclude<E, RouteNotFound>, R> {
118
+ return app.pipe(
119
+ Effect.catchAllCause(renderError),
120
+ )
121
+ }
122
+
123
+ export const withErrorHandled = HttpMiddleware.make(app => {
124
+ return app.pipe(
125
+ Effect.catchAllCause(renderError),
126
+ )
127
+ })
package/src/Hyper.ts ADDED
@@ -0,0 +1,194 @@
1
+ import * as HttpApp from "@effect/platform/HttpApp"
2
+ import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
3
+ import * as Context from "effect/Context"
4
+ import * as Effect from "effect/Effect"
5
+ import * as Effectable from "effect/Effectable"
6
+ import * as Fiber from "effect/Fiber"
7
+ import * as Function from "effect/Function"
8
+ import * as Layer from "effect/Layer"
9
+ import * as Option from "effect/Option"
10
+ import * as Pipeable from "effect/Pipeable"
11
+ import { YieldWrap } from "effect/Utils"
12
+ import * as HyperHtml from "./HyperHtml.ts"
13
+ import type { JSX } from "./jsx.d.ts"
14
+ import { HyperHooks } from "./x/datastar/index.ts"
15
+
16
+ const TypeId = Symbol.for("~hyper/TypeId")
17
+ const LayoutTypeId = Symbol.for("~hyper/LayoutTypeId")
18
+
19
+ type Elements = JSX.IntrinsicElements
20
+
21
+ type Children = JSX.Children
22
+
23
+ export type {
24
+ Children,
25
+ Elements,
26
+ JSX,
27
+ }
28
+
29
+ export class Hyper extends Context.Tag("Hyper")<Hyper, {
30
+ hooks: typeof HyperHooks | undefined
31
+ }>() {}
32
+
33
+ export function layer(opts: {
34
+ hooks: typeof HyperHooks
35
+ }) {
36
+ return Layer.sync(Hyper, () => {
37
+ return {
38
+ hooks: opts.hooks,
39
+ }
40
+ })
41
+ }
42
+
43
+ /**
44
+ * Accepts Effect that returns a HyperNode
45
+ * to a HttpApp.
46
+ * TODO: Implement Hyper.page that returns Hyper.Element
47
+ */
48
+ export function handle<E, R>(
49
+ handler: Effect.Effect<
50
+ JSX.Children | HttpServerResponse.HttpServerResponse,
51
+ E,
52
+ R
53
+ >,
54
+ ): HttpApp.Default<E, R>
55
+ export function handle(
56
+ handler: () => Generator<
57
+ never,
58
+ JSX.Children | HttpServerResponse.HttpServerResponse,
59
+ any
60
+ >,
61
+ ): HttpApp.Default<never, never>
62
+ export function handle<Eff extends YieldWrap<Effect.Effect<any, any, any>>>(
63
+ handler: () => Generator<
64
+ Eff,
65
+ JSX.Children | HttpServerResponse.HttpServerResponse,
66
+ any
67
+ >,
68
+ ): HttpApp.Default<
69
+ [Eff] extends [YieldWrap<Effect.Effect<infer _A, infer E, infer _R>>] ? E
70
+ : never,
71
+ [Eff] extends [YieldWrap<Effect.Effect<infer _A, infer _E, infer R>>] ? R
72
+ : never
73
+ >
74
+ export function handle(
75
+ handler:
76
+ | Effect.Effect<
77
+ JSX.Children | HttpServerResponse.HttpServerResponse,
78
+ any,
79
+ any
80
+ >
81
+ | (() => Generator<
82
+ YieldWrap<Effect.Effect<any, any, any>>,
83
+ JSX.Children | HttpServerResponse.HttpServerResponse,
84
+ any
85
+ >),
86
+ ): HttpApp.Default<any, any> {
87
+ return Effect.gen(function*() {
88
+ const hyper = yield* Effect.serviceOption(Hyper).pipe(
89
+ Effect.andThen(Option.getOrNull),
90
+ )
91
+ const effect = isGenerator(handler) ? Effect.gen(handler) : handler
92
+ const value = yield* effect
93
+
94
+ if (HttpServerResponse.isServerResponse(value)) {
95
+ return value
96
+ }
97
+
98
+ const html = HyperHtml.renderToString(value, hyper?.hooks)
99
+
100
+ return yield* HttpServerResponse.html`${html}`
101
+ })
102
+ }
103
+
104
+ function isGenerator<A, E, R>(
105
+ handler: any,
106
+ ): handler is () => Generator<YieldWrap<Effect.Effect<A, E, R>>, any, any> {
107
+ return typeof handler === "function"
108
+ && handler.constructor?.name === "GeneratorFunction"
109
+ }
110
+
111
+ const NoChildren: ReadonlyArray<never> = Object.freeze([])
112
+
113
+ type Primitive = string | number | boolean | null | undefined
114
+
115
+ export type HyperType = string | HyperComponent
116
+
117
+ export type HyperProps = {
118
+ [key: string]:
119
+ | Primitive
120
+ | ReadonlyArray<Primitive>
121
+ | HyperNode
122
+ | HyperNode[]
123
+ | null
124
+ | undefined
125
+ }
126
+
127
+ export type HyperComponent = (
128
+ props: HyperProps,
129
+ ) => HyperNode | Primitive
130
+
131
+ export interface HyperNode {
132
+ type: HyperType
133
+ props: HyperProps
134
+ }
135
+
136
+ export function h(
137
+ type: HyperType,
138
+ props: HyperProps,
139
+ ): HyperNode {
140
+ return {
141
+ type,
142
+ props: {
143
+ ...props,
144
+ children: props.children ?? NoChildren,
145
+ },
146
+ }
147
+ }
148
+
149
+ export function unsafeUse<Value>(tag: Context.Tag<any, Value>) {
150
+ const currentFiber = Option.getOrThrow(
151
+ Fiber.getCurrentFiber(),
152
+ )
153
+ const context = currentFiber.currentContext
154
+
155
+ return Context.unsafeGet(context, tag)
156
+ }
157
+
158
+ export interface Layout<in out Provides, in out Requires>
159
+ extends Layer.Layer<Provides, never, Requires>
160
+ {
161
+ readonly [TypeId]: typeof LayoutTypeId
162
+ }
163
+
164
+ export function layout<Provides, Requires>(
165
+ handler:
166
+ | Effect.Effect<
167
+ JSX.Children | HttpServerResponse.HttpServerResponse,
168
+ any,
169
+ any
170
+ >
171
+ | (() => Generator<
172
+ YieldWrap<Effect.Effect<any, any, any>>,
173
+ JSX.Children | HttpServerResponse.HttpServerResponse,
174
+ any
175
+ >),
176
+ ):
177
+ & Layout<Provides, Requires>
178
+ & {
179
+ handler: any
180
+ }
181
+ {
182
+ return {
183
+ [TypeId]: LayoutTypeId,
184
+ [Layer.LayerTypeId]: {
185
+ _ROut: Function.identity,
186
+ _E: Function.identity,
187
+ _RIn: Function.identity,
188
+ },
189
+ handler,
190
+ pipe() {
191
+ return Pipeable.pipeArguments(this, arguments)
192
+ },
193
+ }
194
+ }
@@ -0,0 +1,90 @@
1
+ import * as t from "bun:test"
2
+ import * as HyperHtml from "./HyperHtml.ts"
3
+ import * as HyperNode from "./HyperNode.ts"
4
+
5
+ t.it("boolean true attributes render without value (React-like)", () => {
6
+ const node = HyperNode.make("div", {
7
+ hidden: true,
8
+ disabled: true,
9
+ "data-active": true,
10
+ })
11
+
12
+ const html = HyperHtml.renderToString(node)
13
+
14
+ t
15
+ .expect(html)
16
+ .toBe("<div hidden disabled data-active></div>")
17
+ })
18
+
19
+ t.it("boolean false attributes are omitted", () => {
20
+ const node = HyperNode.make("div", {
21
+ hidden: false,
22
+ disabled: false,
23
+ "data-active": false,
24
+ })
25
+
26
+ const html = HyperHtml.renderToString(node)
27
+
28
+ t
29
+ .expect(html)
30
+ .toBe("<div></div>")
31
+ })
32
+
33
+ t.it("string attributes render with values", () => {
34
+ const node = HyperNode.make("div", {
35
+ id: "test",
36
+ class: "my-class",
37
+ "data-value": "hello",
38
+ })
39
+
40
+ const html = HyperHtml.renderToString(node)
41
+
42
+ t
43
+ .expect(html)
44
+ .toBe("<div id=\"test\" class=\"my-class\" data-value=\"hello\"></div>")
45
+ })
46
+
47
+ t.it("number attributes render with values", () => {
48
+ const node = HyperNode.make("input", {
49
+ type: "number",
50
+ min: 0,
51
+ max: 100,
52
+ value: 50,
53
+ })
54
+
55
+ const html = HyperHtml.renderToString(node)
56
+
57
+ t
58
+ .expect(html)
59
+ .toBe("<input type=\"number\" min=\"0\" max=\"100\" value=\"50\">")
60
+ })
61
+
62
+ t.it("null and undefined attributes are omitted", () => {
63
+ const node = HyperNode.make("div", {
64
+ id: null,
65
+ class: undefined,
66
+ "data-test": "value",
67
+ })
68
+
69
+ const html = HyperHtml.renderToString(node)
70
+
71
+ t
72
+ .expect(html)
73
+ .toBe("<div data-test=\"value\"></div>")
74
+ })
75
+
76
+ t.it("mixed boolean and string attributes", () => {
77
+ const node = HyperNode.make("input", {
78
+ type: "checkbox",
79
+ checked: true,
80
+ disabled: false,
81
+ name: "test",
82
+ value: "on",
83
+ })
84
+
85
+ const html = HyperHtml.renderToString(node)
86
+
87
+ t
88
+ .expect(html)
89
+ .toBe("<input type=\"checkbox\" checked name=\"test\" value=\"on\">")
90
+ })
@@ -0,0 +1,139 @@
1
+ import * as HyperNode from "./HyperNode.ts"
2
+ import { JSX } from "./jsx"
3
+
4
+ /**
5
+ * From: https://github.com/developit/vhtml
6
+ */
7
+ const EMPTY_TAGS = [
8
+ "area",
9
+ "base",
10
+ "br",
11
+ "col",
12
+ "command",
13
+ "embed",
14
+ "hr",
15
+ "img",
16
+ "input",
17
+ "keygen",
18
+ "link",
19
+ "meta",
20
+ "param",
21
+ "source",
22
+ "track",
23
+ "wbr",
24
+ ]
25
+
26
+ // escape an attribute
27
+ let esc = (str: any) => String(str).replace(/[&<>"']/g, (s) => `&${map[s]};`)
28
+ let map = {
29
+ "&": "amp",
30
+ "<": "lt",
31
+ ">": "gt",
32
+ "\"": "quot",
33
+ "'": "apos",
34
+ }
35
+
36
+ export function renderToString(
37
+ node: JSX.Children,
38
+ hooks?: { onNode?: (node: HyperNode.HyperNode) => void },
39
+ ): string {
40
+ const stack: any[] = [node]
41
+ let result = ""
42
+
43
+ while (stack.length > 0) {
44
+ const current = stack.pop()!
45
+
46
+ if (typeof current === "string") {
47
+ if (current.startsWith("<") && current.endsWith(">")) {
48
+ // This is a closing tag, don't escape it
49
+ result += current
50
+ } else {
51
+ result += esc(current)
52
+ }
53
+ continue
54
+ }
55
+
56
+ if (typeof current === "number") {
57
+ result += esc(current)
58
+ continue
59
+ }
60
+
61
+ if (typeof current === "boolean") {
62
+ // React-like behavior: booleans render nothing
63
+ continue
64
+ }
65
+
66
+ if (current === null || current === undefined) {
67
+ // React-like behavior: null/undefined render nothing
68
+ continue
69
+ }
70
+
71
+ if (Array.isArray(current)) {
72
+ // Handle arrays by pushing all items to stack in reverse order
73
+ for (let i = current.length - 1; i >= 0; i--) {
74
+ stack.push(current[i])
75
+ }
76
+ continue
77
+ }
78
+
79
+ if (current && typeof current === "object" && current.type) {
80
+ hooks?.onNode?.(current)
81
+
82
+ if (typeof current.type === "function") {
83
+ const componentResult = current.type(current.props)
84
+ if (componentResult != null) {
85
+ stack.push(componentResult)
86
+ }
87
+ continue
88
+ }
89
+
90
+ const { type, props } = current
91
+ result += `<${type}`
92
+
93
+ for (const key in props) {
94
+ if (
95
+ key !== "children"
96
+ && key !== "innerHTML"
97
+ && key !== "dangerouslySetInnerHTML"
98
+ && props[key] !== false
99
+ && props[key] != null
100
+ ) {
101
+ if (props[key] === true) {
102
+ result += ` ${esc(key)}`
103
+ } else {
104
+ const resolvedKey = key === "className" ? "class" : key
105
+
106
+ result += ` ${esc(resolvedKey)}="${esc(props[key])}"`
107
+ }
108
+ }
109
+ }
110
+
111
+ result += ">"
112
+
113
+ if (!EMPTY_TAGS.includes(type)) {
114
+ stack.push(`</${type}>`)
115
+
116
+ const html = props.dangerouslySetInnerHTML?.__html
117
+ ?? props.innerHTML
118
+
119
+ if (html) {
120
+ result += html
121
+ } else {
122
+ const children = props.children
123
+ if (Array.isArray(children)) {
124
+ for (let i = children.length - 1; i >= 0; i--) {
125
+ stack.push(children[i])
126
+ }
127
+ } else if (children != null) {
128
+ stack.push(children)
129
+ }
130
+ }
131
+ }
132
+ } else if (current && typeof current === "object") {
133
+ // Handle objects without type property - convert to string or ignore
134
+ // This prevents [object Object] from appearing
135
+ continue
136
+ }
137
+ }
138
+ return result
139
+ }
@@ -0,0 +1,37 @@
1
+ export const TypeId = Symbol.for("effect-start/HyperNode")
2
+ export type TypeId = typeof TypeId
3
+
4
+ const NoChildren: ReadonlyArray<never> = Object.freeze([])
5
+
6
+ type Primitive = string | number | boolean | null | undefined
7
+
8
+ export type Type = string | HyperComponent
9
+
10
+ export type Props = {
11
+ [key: string]:
12
+ | Primitive
13
+ | HyperNode
14
+ | Iterable<Primitive | HyperNode>
15
+ }
16
+
17
+ export type HyperComponent = (
18
+ props: Props,
19
+ ) => HyperNode | Primitive
20
+
21
+ export interface HyperNode {
22
+ type: Type
23
+ props: Props
24
+ }
25
+
26
+ export function make(
27
+ type: Type,
28
+ props: Props,
29
+ ): HyperNode {
30
+ return {
31
+ type,
32
+ props: {
33
+ ...props,
34
+ children: props.children ?? NoChildren,
35
+ },
36
+ }
37
+ }
@@ -0,0 +1,14 @@
1
+ import * as t from "bun:test"
2
+ import * as JsModule from "./JsModule.ts"
3
+
4
+ t.describe("importSource", () => {
5
+ t.it("imports a string", async () => {
6
+ const mod = await JsModule.importSource<any>(`
7
+ export const b = "B"
8
+ `)
9
+
10
+ t
11
+ .expect(mod.b)
12
+ .toBe("B")
13
+ })
14
+ })
@@ -0,0 +1,116 @@
1
+ import * as Array from "effect/Array"
2
+ import * as Function from "effect/Function"
3
+ import * as Iterable from "effect/Iterable"
4
+ import * as Order from "effect/Order"
5
+ import * as Record from "effect/Record"
6
+ import * as NFS from "node:fs"
7
+ import * as NFSP from "node:fs/promises"
8
+ import * as NPath from "node:path"
9
+ import * as process from "node:process"
10
+
11
+ /**
12
+ * Imports a blob as a module.
13
+ * Useful for loading code from build artifacts.
14
+ *
15
+ * Temporary files are wrriten to closest node_modules/ for node resolver
16
+ * to pick up dependencies correctly and to avoid arbitrary file watchers
17
+ * from detecting them.
18
+ */
19
+ export async function importBlob<M = unknown>(
20
+ blob: Blob,
21
+ entrypoint = "index.js",
22
+ ): Promise<M> {
23
+ return await importBundle({
24
+ [entrypoint]: blob,
25
+ }, entrypoint)
26
+ }
27
+
28
+ /**
29
+ * Imports an entrypoint from multiple blobs.
30
+ * Useful for loading code from build artifacts.
31
+ *
32
+ * Temporary files are wrriten to closest node_modules/ for node resolver
33
+ * to pick up dependencies correctly and to avoid arbitrary file watchers
34
+ * from detecting them.
35
+ *
36
+ * WARNING: dynamic imports that happened after this function will fail
37
+ */
38
+ export async function importBundle<M = unknown>(
39
+ blobs: {
40
+ [path: string]: Blob
41
+ },
42
+ entrypoint: string,
43
+ basePath = findNodeModules() + "/.tmp",
44
+ ): Promise<M> {
45
+ const sortedBlobs = Function.pipe(
46
+ blobs,
47
+ Record.toEntries,
48
+ Array.sortWith(v => v[0], Order.string),
49
+ Array.map(v => v[1]),
50
+ )
51
+ const bundleBlob = new Blob(sortedBlobs)
52
+ const hashPrefix = await hashBuffer(await bundleBlob.arrayBuffer())
53
+ .then(v => v.slice(0, 8))
54
+ const dir = `${basePath}/effect-start-${hashPrefix}`
55
+
56
+ await NFSP.mkdir(dir, { recursive: true })
57
+
58
+ await Promise.all(Function.pipe(
59
+ blobs,
60
+ Record.toEntries,
61
+ Array.map(([path, blob]) => {
62
+ const fullPath = `${dir}/${path}`
63
+
64
+ return blob
65
+ .arrayBuffer()
66
+ .then(v => NFSP.writeFile(fullPath, Buffer.from(v)))
67
+ }),
68
+ ))
69
+
70
+ const bundleModule = await import(`${dir}/${entrypoint}`)
71
+
72
+ await NFSP
73
+ .rmdir(dir, { recursive: true })
74
+ // if called concurrently, file sometimes may be deleted
75
+ // safe ignore when this happens
76
+ .catch(() => {})
77
+
78
+ return bundleModule
79
+ }
80
+
81
+ export function findNodeModules(startDir = process.cwd()) {
82
+ let currentDir = NPath.resolve(startDir)
83
+
84
+ while (currentDir !== NPath.parse(currentDir).root) {
85
+ const nodeModulesPath = NPath.join(currentDir, "node_modules")
86
+ if (
87
+ NFS.statSync(nodeModulesPath).isDirectory()
88
+ ) {
89
+ return nodeModulesPath
90
+ }
91
+
92
+ currentDir = NPath.dirname(currentDir)
93
+ }
94
+
95
+ return null
96
+ }
97
+
98
+ /**
99
+ * Loads a JS module from a string using data: import
100
+ */
101
+ export async function importSource<M = unknown>(
102
+ code: string,
103
+ ): Promise<M> {
104
+ const dataUrl = `data:text/javascript,${encodeURIComponent(code)}`
105
+ return await import(dataUrl)
106
+ }
107
+
108
+ async function hashBuffer(buffer: BufferSource) {
109
+ const hashBuffer = await crypto.subtle.digest("SHA-256", buffer)
110
+
111
+ return Function.pipe(
112
+ new Uint8Array(hashBuffer),
113
+ Iterable.map(b => b.toString(16).padStart(2, "0")),
114
+ Iterable.reduce("", (a, b) => a + b),
115
+ )
116
+ }