effect-start 0.13.0 → 0.14.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 (38) hide show
  1. package/README.md +4 -4
  2. package/package.json +12 -17
  3. package/src/Bundle.ts +0 -35
  4. package/src/BundleHttp.test.ts +4 -6
  5. package/src/BundleHttp.ts +2 -1
  6. package/src/Effect_HttpRouter.test.ts +2 -3
  7. package/src/FileHttpRouter.test.ts +2 -2
  8. package/src/FileRouterCodegen.test.ts +1 -1
  9. package/src/FileRouter_files.test.ts +1 -1
  10. package/src/HttpAppExtra.test.ts +1 -1
  11. package/src/Start.ts +0 -34
  12. package/src/StartApp.ts +20 -16
  13. package/src/bun/BunBundle.test.ts +1 -1
  14. package/src/bun/BunBundle_imports.test.ts +2 -2
  15. package/src/bun/BunRoute_bundles.test.ts +1 -1
  16. package/src/bun/_BunEnhancedResolve.ts +201 -0
  17. package/src/bun/index.ts +0 -1
  18. package/src/{SseHttpResponse.ts → experimental/SseHttpResponse.ts} +2 -1
  19. package/src/experimental/index.ts +2 -0
  20. package/src/index.ts +2 -20
  21. package/src/middlewares/index.ts +1 -0
  22. package/src/{TestHttpClient.test.ts → testing/TestHttpClient.test.ts} +1 -1
  23. package/src/{TestHttpClient.ts → testing/TestHttpClient.ts} +0 -1
  24. package/src/testing/index.ts +3 -0
  25. package/src/{bun/BunTailwindPlugin.test.ts → x/tailwind/TailwindPlugin.test.ts} +1 -1
  26. package/src/{bun/BunTailwindPlugin.ts → x/tailwind/TailwindPlugin.ts} +32 -42
  27. package/src/x/tailwind/compile.ts +243 -0
  28. package/src/x/tailwind/plugin.ts +2 -2
  29. package/src/JsModule.test.ts +0 -14
  30. package/src/JsModule.ts +0 -116
  31. package/src/PublicDirectory.test.ts +0 -280
  32. package/src/PublicDirectory.ts +0 -108
  33. package/src/StartHttp.ts +0 -42
  34. /package/src/{EncryptedCookies.test.ts → experimental/EncryptedCookies.test.ts} +0 -0
  35. /package/src/{EncryptedCookies.ts → experimental/EncryptedCookies.ts} +0 -0
  36. /package/src/{TestLogger.test.ts → testing/TestLogger.test.ts} +0 -0
  37. /package/src/{TestLogger.ts → testing/TestLogger.ts} +0 -0
  38. /package/src/{testing.ts → testing/utils.ts} +0 -0
@@ -1,18 +1,8 @@
1
- import type * as Tailwind from "@tailwindcss/node"
2
1
  import type { BunPlugin } from "bun"
3
2
  import * as NPath from "node:path"
3
+ import * as Tailwind from "./compile.ts"
4
4
 
5
- type Compiler = Awaited<ReturnType<typeof Tailwind.compile>>
6
-
7
- export const make = (opts: {
8
- /**
9
- * Custom importer function to load Tailwind.
10
- * By default, it imports from '@tailwindcss/node'.
11
- * If you want to use a different version or a custom implementation,
12
- * provide your own importer.
13
- */
14
- importer?: () => Promise<typeof Tailwind>
15
-
5
+ export const make = (opts?: {
16
6
  /**
17
7
  * Pattern to match component and HTML files for class name extraction.
18
8
  */
@@ -33,24 +23,19 @@ export const make = (opts: {
33
23
  * Useful when we want to scan clientside code which is not imported directly on serverside.
34
24
  */
35
25
  scanPath?: string
36
- } = {}): BunPlugin => {
26
+
27
+ target?: "browser" | "bun" | "node"
28
+ }): BunPlugin => {
37
29
  const {
38
30
  filesPattern = /\.(jsx?|tsx?|html|svelte|vue|astro)$/,
39
31
  cssPattern = /\.css$/,
40
- importer = () =>
41
- import("@tailwindcss/node").catch(err => {
42
- throw new Error(
43
- "Tailwind not found: install @tailwindcss/node or provide custom importer option",
44
- )
45
- }),
46
- } = opts
32
+ target = "browser",
33
+ } = opts ?? {}
47
34
 
48
35
  return {
49
- name: "Bun Tailwind.css plugin",
50
- target: "browser",
36
+ name: "Tailwind.css plugin",
37
+ target,
51
38
  async setup(builder) {
52
- const Tailwind = await importer()
53
-
54
39
  const scannedCandidates = new Set<string>()
55
40
  // (file) -> (class names)
56
41
  const classNameCandidates = new Map<string, Set<string>>()
@@ -59,21 +44,22 @@ export const make = (opts: {
59
44
  // (imported path) -> (importer paths)
60
45
  const importDescendants = new Map<string, Set<string>>()
61
46
 
62
- if (opts.scanPath) {
63
- const candidates = await scanFiles(opts.scanPath)
47
+ const prepopulateCandidates = opts?.scanPath
48
+ ? async () => {
49
+ const candidates = await scanFiles(opts.scanPath!)
64
50
 
65
- candidates.forEach(candidate => scannedCandidates.add(candidate))
66
- }
51
+ scannedCandidates.clear()
67
52
 
68
- /**
69
- * Track import relationships when dynamically scanning
70
- * from tailwind entrypoints.
71
- *
72
- * As of Bun 1.3 this pathway break for Bun Full-Stack server.
73
- * Better to pass scanPath explicitly.
74
- * @see https://github.com/oven-sh/bun/issues/20877
75
- */
76
- if (!opts.scanPath) {
53
+ candidates.forEach(candidate => scannedCandidates.add(candidate))
54
+ }
55
+ : null
56
+
57
+ // Track import relationships when dynamically scanning
58
+ // from tailwind entrypoints.
59
+ // As of Bun 1.3 this pathway break for Bun Full-Stack server.
60
+ // Better to pass scanPath explicitly.
61
+ // @see https://github.com/oven-sh/bun/issues/20877
62
+ if (!prepopulateCandidates) {
77
63
  builder.onResolve({
78
64
  filter: /.*/,
79
65
  }, (args) => {
@@ -142,18 +128,18 @@ export const make = (opts: {
142
128
 
143
129
  const compiler = await Tailwind.compile(source, {
144
130
  base: NPath.dirname(args.path),
145
- shouldRewriteUrls: true,
146
131
  onDependency: (path) => {},
147
132
  })
148
133
 
134
+ await prepopulateCandidates?.()
135
+
149
136
  // wait for other files to be loaded so we can collect class name candidates
150
137
  await args.defer()
151
138
 
152
- const candidates = new Set<string>()
153
-
154
- scannedCandidates.forEach(candidate => candidates.add(candidate))
139
+ const candidates = new Set<string>(scannedCandidates)
155
140
 
156
- {
141
+ // when we scan a path, we don't need to track candidate tree
142
+ if (!prepopulateCandidates) {
157
143
  const pendingModules = [
158
144
  // get class name candidates from all modules that import this one
159
145
  ...(importAncestors.get(args.path) ?? []),
@@ -311,6 +297,10 @@ async function scanFiles(dir: string): Promise<Set<string>> {
311
297
  absolute: true,
312
298
  })
313
299
  ) {
300
+ if (filePath.includes("/node_modules/")) {
301
+ continue
302
+ }
303
+
314
304
  const contents = await Bun.file(filePath).text()
315
305
  const classNames = extractClassNames(contents)
316
306
 
@@ -0,0 +1,243 @@
1
+ import fsPromises from "node:fs/promises"
2
+ import path from "node:path"
3
+ import { pathToFileURL } from "node:url"
4
+ import {
5
+ compile as _compile,
6
+ compileAst as _compileAst,
7
+ Features,
8
+ Polyfills,
9
+ } from "tailwindcss"
10
+ import * as BunEnhancedResolve from "../../bun/_BunEnhancedResolve"
11
+
12
+ type AstNode = Parameters<typeof _compileAst>[0][number]
13
+
14
+ export {
15
+ Features,
16
+ Polyfills,
17
+ }
18
+
19
+ export type Resolver = (
20
+ id: string,
21
+ base: string,
22
+ ) => Promise<string | false | undefined>
23
+
24
+ export interface CompileOptions {
25
+ base: string
26
+ from?: string
27
+ onDependency: (path: string) => void
28
+ polyfills?: Polyfills
29
+
30
+ customCssResolver?: Resolver
31
+ customJsResolver?: Resolver
32
+ }
33
+
34
+ function createCompileOptions({
35
+ base,
36
+ from,
37
+ polyfills,
38
+ onDependency,
39
+
40
+ customCssResolver,
41
+ customJsResolver,
42
+ }: CompileOptions) {
43
+ return {
44
+ base,
45
+ polyfills,
46
+ from,
47
+ async loadModule(id: string, base: string) {
48
+ return loadModule(id, base, onDependency, customJsResolver)
49
+ },
50
+ async loadStylesheet(id: string, sheetBase: string) {
51
+ let sheet = await loadStylesheet(
52
+ id,
53
+ sheetBase,
54
+ onDependency,
55
+ customCssResolver,
56
+ )
57
+
58
+ return sheet
59
+ },
60
+ }
61
+ }
62
+
63
+ async function ensureSourceDetectionRootExists(compiler: {
64
+ root: Awaited<ReturnType<typeof compile>>["root"]
65
+ }) {
66
+ // Verify if the `source(…)` path exists (until the glob pattern starts)
67
+ if (compiler.root && compiler.root !== "none") {
68
+ let globSymbols = /[*{]/
69
+ let basePath: string[] = []
70
+ for (let segment of compiler.root.pattern.split("/")) {
71
+ if (globSymbols.test(segment)) {
72
+ break
73
+ }
74
+
75
+ basePath.push(segment)
76
+ }
77
+
78
+ let exists = await fsPromises
79
+ .stat(path.resolve(compiler.root.base, basePath.join("/")))
80
+ .then((stat) => stat.isDirectory())
81
+ .catch(() => false)
82
+
83
+ if (!exists) {
84
+ throw new Error(
85
+ `The \`source(${compiler.root.pattern})\` does not exist or is not a directory.`,
86
+ )
87
+ }
88
+ }
89
+ }
90
+
91
+ export async function compileAst(ast: AstNode[], options: CompileOptions) {
92
+ let compiler = await _compileAst(ast, createCompileOptions(options))
93
+ await ensureSourceDetectionRootExists(compiler)
94
+ return compiler
95
+ }
96
+
97
+ export async function compile(css: string, options: CompileOptions) {
98
+ let compiler = await _compile(css, createCompileOptions(options))
99
+ await ensureSourceDetectionRootExists(compiler)
100
+ return compiler
101
+ }
102
+
103
+ export async function loadModule(
104
+ id: string,
105
+ base: string,
106
+ _onDependency: (path: string) => void,
107
+ customJsResolver?: Resolver,
108
+ ) {
109
+ if (id[0] !== ".") {
110
+ let resolvedPath = await resolveJsId(id, base, customJsResolver)
111
+ if (!resolvedPath) {
112
+ throw new Error(`Could not resolve '${id}' from '${base}'`)
113
+ }
114
+
115
+ let module = await importModule(pathToFileURL(resolvedPath).href)
116
+ return {
117
+ path: resolvedPath,
118
+ base: path.dirname(resolvedPath),
119
+ module: module.default ?? module,
120
+ }
121
+ }
122
+
123
+ let resolvedPath = await resolveJsId(id, base, customJsResolver)
124
+ if (!resolvedPath) {
125
+ throw new Error(`Could not resolve '${id}' from '${base}'`)
126
+ }
127
+
128
+ let [module] = await Promise.all([
129
+ importModule(pathToFileURL(resolvedPath).href + "?id=" + Date.now()),
130
+ ])
131
+
132
+ return {
133
+ path: resolvedPath,
134
+ base: path.dirname(resolvedPath),
135
+ module: module.default ?? module,
136
+ }
137
+ }
138
+
139
+ async function loadStylesheet(
140
+ id: string,
141
+ base: string,
142
+ onDependency: (path: string) => void,
143
+ cssResolver?: Resolver,
144
+ ) {
145
+ let resolvedPath = await resolveCssId(id, base, cssResolver)
146
+ if (!resolvedPath) throw new Error(`Could not resolve '${id}' from '${base}'`)
147
+
148
+ onDependency(resolvedPath)
149
+
150
+ let file = await fsPromises.readFile(resolvedPath, "utf-8")
151
+ return {
152
+ path: resolvedPath,
153
+ base: path.dirname(resolvedPath),
154
+ content: file,
155
+ }
156
+ }
157
+
158
+ async function importModule(path: string): Promise<any> {
159
+ if (typeof globalThis.__tw_load === "function") {
160
+ let module = await globalThis.__tw_load(path)
161
+ if (module) {
162
+ return module
163
+ }
164
+ }
165
+
166
+ return await import(path)
167
+ }
168
+
169
+ const cssResolver = BunEnhancedResolve.ResolverFactory.createResolver({
170
+ extensions: [".css"],
171
+ mainFields: ["style"],
172
+ conditionNames: ["style"],
173
+ })
174
+ async function resolveCssId(
175
+ id: string,
176
+ base: string,
177
+ customCssResolver?: Resolver,
178
+ ): Promise<string | false | undefined> {
179
+ if (typeof globalThis.__tw_resolve === "function") {
180
+ let resolved = globalThis.__tw_resolve(id, base)
181
+ if (resolved) {
182
+ return Promise.resolve(resolved)
183
+ }
184
+ }
185
+
186
+ if (customCssResolver) {
187
+ let customResolution = await customCssResolver(id, base)
188
+ if (customResolution) {
189
+ return customResolution
190
+ }
191
+ }
192
+
193
+ return runResolver(cssResolver, id, base)
194
+ }
195
+
196
+ const esmResolver = BunEnhancedResolve.ResolverFactory.createResolver({
197
+ extensions: [".js", ".json", ".node", ".ts"],
198
+ conditionNames: ["node", "import"],
199
+ mainFields: ["module", "main"],
200
+ })
201
+
202
+ const cjsResolver = BunEnhancedResolve.ResolverFactory.createResolver({
203
+ extensions: [".js", ".json", ".node", ".ts"],
204
+ conditionNames: ["node", "require"],
205
+ mainFields: ["main"],
206
+ })
207
+
208
+ async function resolveJsId(
209
+ id: string,
210
+ base: string,
211
+ customJsResolver?: Resolver,
212
+ ): Promise<string | false | undefined> {
213
+ if (typeof globalThis.__tw_resolve === "function") {
214
+ let resolved = globalThis.__tw_resolve(id, base)
215
+ if (resolved) {
216
+ return Promise.resolve(resolved)
217
+ }
218
+ }
219
+
220
+ if (customJsResolver) {
221
+ let customResolution = await customJsResolver(id, base)
222
+ if (customResolution) {
223
+ return customResolution
224
+ }
225
+ }
226
+
227
+ return runResolver(esmResolver, id, base).catch(() =>
228
+ runResolver(cjsResolver, id, base)
229
+ )
230
+ }
231
+
232
+ function runResolver(
233
+ resolver: BunEnhancedResolve.Resolver,
234
+ id: string,
235
+ base: string,
236
+ ): Promise<string | false | undefined> {
237
+ return new Promise((resolve, reject) =>
238
+ resolver.resolve({}, base, id, {}, (err, result) => {
239
+ if (err) return reject(err)
240
+ resolve(result)
241
+ })
242
+ )
243
+ }
@@ -1,6 +1,6 @@
1
1
  import * as NPath from "node:path"
2
- import * as BunTailwindPlugin from "../../bun/BunTailwindPlugin.ts"
3
2
  import * as NodeUtils from "../../NodeUtils.ts"
3
+ import * as TailwindPlugin from "./TailwindPlugin.ts"
4
4
 
5
5
  // Append `?dir=` to module identifier to pass custom directory to scan
6
6
  const dirParam = URL.parse(import.meta.url)?.searchParams.get("dir")
@@ -12,6 +12,6 @@ const scanPath = dirParam
12
12
  : process.cwd()
13
13
 
14
14
  // Export as default to be used in bunfig.toml
15
- export default BunTailwindPlugin.make({
15
+ export default TailwindPlugin.make({
16
16
  scanPath,
17
17
  })
@@ -1,14 +0,0 @@
1
- import * as t from "bun:test"
2
- import * as JsModule from "./JsModule.ts"
3
-
4
- t.describe(`${JsModule.importSource.name}`, () => {
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
- })
package/src/JsModule.ts DELETED
@@ -1,116 +0,0 @@
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
- }