effect-start 0.13.0 → 0.13.1

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/README.md CHANGED
@@ -52,13 +52,13 @@ src/routes
52
52
 
53
53
  ### Tailwind CSS Support
54
54
 
55
- Effect Start comes with native Tailwind support that is lightweight and
56
- works with minimal setup.
55
+ Effect Start comes with Tailwind plugin that is lightweight and
56
+ works with minimal configuration.
57
57
 
58
- First, install Tailwind package:
58
+ First, install official Tailwind package:
59
59
 
60
60
  ```sh
61
- bun add tailwindcss
61
+ bun add -D tailwindcss
62
62
  ```
63
63
 
64
64
  Then, register a plugin in `bunfig.toml`:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-start",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "exports": {
@@ -32,23 +32,18 @@
32
32
  "peerDependencies": {
33
33
  "typescript": "^5.9.3"
34
34
  },
35
- "peerDependenciesMeta": {
36
- "@tailwindcss/node": {
37
- "optional": true
38
- }
39
- },
40
35
  "devDependencies": {
36
+ "dprint-cli": "^0.4.1",
37
+ "dprint-markup": "nounder/dprint-markup",
41
38
  "@dprint/json": "^0.21.0",
42
39
  "@dprint/markdown": "^0.20.0",
43
40
  "@dprint/typescript": "^0.95.13",
44
41
  "@effect/language-service": "^0.61.0",
45
- "@tailwindcss/node": "^4.1.17",
46
42
  "@types/bun": "^1.3.4",
47
43
  "@types/react": "^19.2.7",
48
44
  "@types/react-dom": "^19.2.3",
49
- "dprint-cli": "^0.4.1",
50
- "dprint-markup": "nounder/dprint-markup",
51
45
  "effect-memfs": "^0.8.0",
46
+ "tailwindcss": "^4.1.18",
52
47
  "ts-namespace-import": "nounder/ts-namespace-import#140c405"
53
48
  },
54
49
  "files": [
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Bun adapter for enhanced-resolve
3
+ *
4
+ * This module provides a drop-in replacement for `enhanced-resolve` that uses
5
+ * Bun's built-in resolver. It implements the subset of the enhanced-resolve API
6
+ * used for Tailwind CSS.
7
+ */
8
+ import fs from "node:fs"
9
+ import path from "node:path"
10
+
11
+ type ErrorWithDetail =
12
+ & Error
13
+ & {
14
+ details?: string
15
+ }
16
+
17
+ interface ResolveRequest {
18
+ path: string | false
19
+ context?: object
20
+ descriptionFilePath?: string
21
+ descriptionFileRoot?: string
22
+ descriptionFileData?: Record<string, unknown>
23
+ relativePath?: string
24
+ ignoreSymlinks?: boolean
25
+ fullySpecified?: boolean
26
+ }
27
+
28
+ interface ResolveContext {
29
+ contextDependencies?: { add: (item: string) => void }
30
+ fileDependencies?: { add: (item: string) => void }
31
+ missingDependencies?: { add: (item: string) => void }
32
+ stack?: Set<string>
33
+ log?: (str: string) => void
34
+ yield?: (request: ResolveRequest) => void
35
+ }
36
+
37
+ interface ResolveOptions {
38
+ extensions?: string[]
39
+ mainFields?: (string | string[])[]
40
+ conditionNames?: string[]
41
+ fileSystem?: unknown
42
+ useSyncFileSystemCalls?: boolean
43
+ modules?: string | string[]
44
+ }
45
+
46
+ export interface Resolver {
47
+ resolve(
48
+ context: object,
49
+ path: string,
50
+ request: string,
51
+ resolveContext: ResolveContext,
52
+ callback: (
53
+ err: null | ErrorWithDetail,
54
+ res?: string | false,
55
+ req?: ResolveRequest,
56
+ ) => void,
57
+ ): void
58
+ }
59
+
60
+ export class CachedInputFileSystem {
61
+ constructor(
62
+ _fileSystem: unknown,
63
+ _duration: number,
64
+ ) {}
65
+ }
66
+
67
+ export const ResolverFactory = {
68
+ createResolver(options: ResolveOptions): Resolver {
69
+ const extensions = options.extensions ?? []
70
+ const mainFields = (options.mainFields ?? []).flatMap((f) =>
71
+ Array.isArray(f) ? f : [f]
72
+ )
73
+ const conditionNames = options.conditionNames ?? []
74
+
75
+ return {
76
+ resolve(
77
+ _context: object,
78
+ basePath: string,
79
+ id: string,
80
+ _resolveContext: ResolveContext,
81
+ callback: (
82
+ err: null | ErrorWithDetail,
83
+ res?: string | false,
84
+ req?: ResolveRequest,
85
+ ) => void,
86
+ ): void {
87
+ try {
88
+ const result = resolveSync(id, basePath, {
89
+ extensions,
90
+ mainFields,
91
+ conditionNames,
92
+ })
93
+ callback(null, result)
94
+ } catch (err) {
95
+ callback(err instanceof Error ? err : new Error(String(err)))
96
+ }
97
+ },
98
+ }
99
+ },
100
+ }
101
+
102
+ interface ResolveInternalOptions {
103
+ extensions: string[]
104
+ mainFields: string[]
105
+ conditionNames: string[]
106
+ }
107
+
108
+ function resolveSync(
109
+ id: string,
110
+ base: string,
111
+ options: ResolveInternalOptions,
112
+ ): string | undefined {
113
+ if (id.startsWith(".") || id.startsWith("/")) {
114
+ for (const ext of ["", ...options.extensions]) {
115
+ const fullPath = path.resolve(base, id + ext)
116
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
117
+ return fullPath
118
+ }
119
+ }
120
+ return undefined
121
+ }
122
+
123
+ const packagePath = resolvePackagePath(id, base)
124
+ if (!packagePath) return undefined
125
+
126
+ const packageJsonPath = path.join(packagePath, "package.json")
127
+ if (!fs.existsSync(packageJsonPath)) return undefined
128
+
129
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"))
130
+
131
+ for (const field of options.mainFields) {
132
+ if (typeof packageJson[field] === "string") {
133
+ const resolved = path.resolve(packagePath, packageJson[field])
134
+ if (fs.existsSync(resolved)) return resolved
135
+ }
136
+ }
137
+
138
+ if (packageJson.exports) {
139
+ const resolved = resolveExports(
140
+ packageJson.exports,
141
+ packagePath,
142
+ options.conditionNames,
143
+ )
144
+ if (resolved && fs.existsSync(resolved)) return resolved
145
+ }
146
+
147
+ try {
148
+ return Bun.resolveSync(id, base)
149
+ } catch {
150
+ return undefined
151
+ }
152
+ }
153
+
154
+ function resolvePackagePath(id: string, base: string): string | undefined {
155
+ const parts = id.split("/")
156
+ const packageName = id.startsWith("@")
157
+ ? parts.slice(0, 2).join("/")
158
+ : parts[0]
159
+
160
+ let dir = base
161
+ while (dir !== path.dirname(dir)) {
162
+ const candidate = path.join(dir, "node_modules", packageName)
163
+ if (fs.existsSync(candidate)) return candidate
164
+ dir = path.dirname(dir)
165
+ }
166
+ return undefined
167
+ }
168
+
169
+ function resolveExports(
170
+ exports: unknown,
171
+ packagePath: string,
172
+ conditionNames: string[],
173
+ ): string | undefined {
174
+ if (typeof exports === "string") {
175
+ return path.resolve(packagePath, exports)
176
+ }
177
+
178
+ if (exports && typeof exports === "object" && !Array.isArray(exports)) {
179
+ const exportsObj = exports as Record<string, unknown>
180
+
181
+ if ("." in exportsObj) {
182
+ return resolveExports(exportsObj["."], packagePath, conditionNames)
183
+ }
184
+
185
+ for (const condition of conditionNames) {
186
+ if (condition in exportsObj) {
187
+ return resolveExports(
188
+ exportsObj[condition],
189
+ packagePath,
190
+ conditionNames,
191
+ )
192
+ }
193
+ }
194
+
195
+ if ("default" in exportsObj) {
196
+ return resolveExports(exportsObj["default"], packagePath, conditionNames)
197
+ }
198
+ }
199
+
200
+ return undefined
201
+ }
package/src/bun/index.ts CHANGED
@@ -2,4 +2,3 @@ export * as BunBundle from "./BunBundle.ts"
2
2
  export * as BunHttpServer from "./BunHttpServer.ts"
3
3
  export * as BunImportTrackerPlugin from "./BunImportTrackerPlugin.ts"
4
4
  export * as BunRoute from "./BunRoute.ts"
5
- export * as BunTailwindPlugin from "./BunTailwindPlugin.ts"
@@ -1,6 +1,6 @@
1
1
  import * as t from "bun:test"
2
2
 
3
- import { extractClassNames } from "./BunTailwindPlugin.ts"
3
+ import { extractClassNames } from "./TailwindPlugin.ts"
4
4
 
5
5
  // Keep the old broad implementation for comparison tests
6
6
  function extractClassNamesBroad(source: string): Set<string> {
@@ -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,7 +44,7 @@ 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) {
47
+ if (opts?.scanPath) {
63
48
  const candidates = await scanFiles(opts.scanPath)
64
49
 
65
50
  candidates.forEach(candidate => scannedCandidates.add(candidate))
@@ -73,7 +58,7 @@ export const make = (opts: {
73
58
  * Better to pass scanPath explicitly.
74
59
  * @see https://github.com/oven-sh/bun/issues/20877
75
60
  */
76
- if (!opts.scanPath) {
61
+ if (!opts?.scanPath) {
77
62
  builder.onResolve({
78
63
  filter: /.*/,
79
64
  }, (args) => {
@@ -142,7 +127,6 @@ export const make = (opts: {
142
127
 
143
128
  const compiler = await Tailwind.compile(source, {
144
129
  base: NPath.dirname(args.path),
145
- shouldRewriteUrls: true,
146
130
  onDependency: (path) => {},
147
131
  })
148
132
 
@@ -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
  })