effect-start 0.12.0 → 0.13.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/README.md CHANGED
@@ -52,27 +52,24 @@ src/routes
52
52
 
53
53
  ### Tailwind CSS Support
54
54
 
55
- Install Tailwind plugin:
55
+ Effect Start comes with native Tailwind support that is lightweight and
56
+ works with minimal setup.
57
+
58
+ First, install Tailwind package:
56
59
 
57
60
  ```sh
58
- bun add tailwindcss bun-plugin-tailwind
61
+ bun add tailwindcss
59
62
  ```
60
63
 
61
- In `bunfig.toml`:
64
+ Then, register a plugin in `bunfig.toml`:
62
65
 
63
66
  ```toml
64
67
  [serve.static]
65
- plugins = ["bun-plugin-tailwind"]
68
+ plugins = ["effect-start/x/tailwind/plugin"]
66
69
  ```
67
70
 
68
- Finally, include it in your `src/app.html`:
71
+ Finally, include it in your `src/app.css`:
69
72
 
70
73
  ```html
71
- <!doctype html>
72
- <html>
73
- <head>
74
- <link rel="stylesheet" href="tailwindcss" />
75
- </head>
76
- <!-- the rest of your HTML... -->
77
- </html>
74
+ @import "tailwindcss";
78
75
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-start",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "exports": {
@@ -16,6 +16,7 @@
16
16
  "./jsx-dev-runtime": "./src/jsx-runtime.ts",
17
17
  "./hyper": "./src/hyper/index.ts",
18
18
  "./x/*": "./src/x/*/index.ts",
19
+ "./x/tailwind/plugin": "./src/x/tailwind/plugin.ts",
19
20
  "./middlewares/BasicAuthMiddleware": "./src/middlewares/BasicAuthMiddleware.ts",
20
21
  "./assets.d.ts": "./src/assets.d.ts"
21
22
  },
@@ -0,0 +1,23 @@
1
+ import * as NFS from "node:fs/promises"
2
+ import * as NPath from "node:path"
3
+
4
+ export const findClosestPackageJson = async (
5
+ path: string,
6
+ ): Promise<string | undefined> => {
7
+ const resolved = NPath.resolve(path)
8
+ const stat = await NFS.stat(resolved).catch(() => undefined)
9
+ let dir = stat?.isDirectory() ? resolved : NPath.dirname(resolved)
10
+ const root = NPath.parse(dir).root
11
+
12
+ while (dir !== root) {
13
+ const candidate = NPath.join(dir, "package.json")
14
+ try {
15
+ await NFS.access(candidate)
16
+ return candidate
17
+ } catch {
18
+ dir = NPath.dirname(dir)
19
+ }
20
+ }
21
+
22
+ return undefined
23
+ }
@@ -1,6 +1,6 @@
1
1
  import type * as Tailwind from "@tailwindcss/node"
2
2
  import type { BunPlugin } from "bun"
3
- import * as NodePath from "node:path"
3
+ import * as NPath from "node:path"
4
4
 
5
5
  type Compiler = Awaited<ReturnType<typeof Tailwind.compile>>
6
6
 
@@ -30,6 +30,7 @@ export const make = (opts: {
30
30
  *
31
31
  * This option scans the provided path and ensures that class names found under this path
32
32
  * are includedd, even if they are not part of the import graph.
33
+ * Useful when we want to scan clientside code which is not imported directly on serverside.
33
34
  */
34
35
  scanPath?: string
35
36
  } = {}): BunPlugin => {
@@ -65,44 +66,51 @@ export const make = (opts: {
65
66
  }
66
67
 
67
68
  /**
68
- * Track import relationships.
69
- * We do this to scope all class name candidates to tailwind entrypoints
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
70
75
  */
71
- builder.onResolve({
72
- filter: /.*/,
73
- }, (args) => {
74
- const fullPath = Bun.resolveSync(args.path, args.resolveDir)
75
-
76
- if (fullPath.includes("/node_modules/")) {
77
- return undefined
78
- }
79
-
80
- /**
81
- * Register every visited module.
82
- */
83
- {
84
- if (!importAncestors.has(fullPath)) {
85
- importAncestors.set(fullPath, new Set())
76
+ if (!opts.scanPath) {
77
+ builder.onResolve({
78
+ filter: /.*/,
79
+ }, (args) => {
80
+ const fullPath = Bun.resolveSync(args.path, args.resolveDir)
81
+ const importer = args.importer
82
+
83
+ if (fullPath.includes("/node_modules/")) {
84
+ return undefined
86
85
  }
87
86
 
88
- if (!importDescendants.has(fullPath)) {
89
- importDescendants.set(fullPath, new Set())
90
- }
87
+ /**
88
+ * Register every visited module.
89
+ */
90
+ {
91
+ if (!importAncestors.has(fullPath)) {
92
+ importAncestors.set(fullPath, new Set())
93
+ }
91
94
 
92
- if (!importAncestors.has(args.importer)) {
93
- importAncestors.set(args.importer, new Set())
94
- }
95
+ if (!importDescendants.has(fullPath)) {
96
+ importDescendants.set(fullPath, new Set())
97
+ }
95
98
 
96
- if (!importDescendants.has(args.importer)) {
97
- importDescendants.set(args.importer, new Set())
99
+ if (!importAncestors.has(importer)) {
100
+ importAncestors.set(args.importer, new Set())
101
+ }
102
+
103
+ if (!importDescendants.has(importer)) {
104
+ importDescendants.set(importer, new Set())
105
+ }
98
106
  }
99
- }
100
107
 
101
- importAncestors.get(fullPath)!.add(args.importer)
102
- importDescendants.get(args.importer)!.add(fullPath)
108
+ importAncestors.get(fullPath)!.add(importer)
109
+ importDescendants.get(importer)!.add(fullPath)
103
110
 
104
- return undefined
105
- })
111
+ return undefined
112
+ })
113
+ }
106
114
 
107
115
  /**
108
116
  * Scan for class name candidates in component files.
@@ -133,14 +141,12 @@ export const make = (opts: {
133
141
  }
134
142
 
135
143
  const compiler = await Tailwind.compile(source, {
136
- base: NodePath.dirname(args.path),
144
+ base: NPath.dirname(args.path),
137
145
  shouldRewriteUrls: true,
138
146
  onDependency: (path) => {},
139
147
  })
140
148
 
141
149
  // wait for other files to be loaded so we can collect class name candidates
142
- // NOTE: at currently processed css won't be in import graph because
143
- // we haven't returned its contents yet.
144
150
  await args.defer()
145
151
 
146
152
  const candidates = new Set<string>()
@@ -189,81 +195,78 @@ export const make = (opts: {
189
195
  }
190
196
 
191
197
  const CSS_IMPORT_REGEX = /@import\s+(?:url\()?["']?([^"')]+)["']?\)?\s*[^;]*;/
198
+ const HTML_COMMENT_REGEX = /<!--[\s\S]*?-->/g
199
+ const TEMPLATE_EXPRESSION_REGEX = /\$\{[^}]*\}/g
200
+ const TAILWIND_CLASS_REGEX = /^[a-zA-Z0-9_:-]+(\[[^\]]*\])?$/
201
+ const CLASS_NAME_PATTERNS = [
202
+ // HTML class attributes with double quotes: <div class="bg-blue-500 text-white">
203
+ "<[^>]*?\\sclass\\s*=\\s*\"([^\"]+)\"",
192
204
 
193
- function hasCssImport(css: string, specifier?: string): boolean {
194
- const [, importPath] = css.match(CSS_IMPORT_REGEX) ?? []
195
-
196
- if (!importPath) return false
197
-
198
- return specifier === undefined
199
- || importPath.includes(specifier)
200
- }
201
-
202
- export function extractClassNames(source: string): Set<string> {
203
- const candidates = new Set<string>()
205
+ // HTML class attributes with single quotes: <div class='bg-blue-500 text-white'>
206
+ "<[^>]*?\\sclass\\s*=\\s*'([^']+)'",
204
207
 
205
- // Remove HTML comments to avoid false matches
206
- const sourceWithoutComments = source.replace(/<!--[\s\S]*?-->/g, "")
208
+ // JSX className attributes with double quotes: <div className="bg-blue-500 text-white">
209
+ "<[^>]*?\\sclassName\\s*=\\s*\"([^\"]+)\"",
207
210
 
208
- // Array of pattern strings for different class/className attribute formats
209
- const patterns = [
210
- // HTML class attributes with double quotes: <div class="bg-blue-500 text-white">
211
- "<[^>]*?\\sclass\\s*=\\s*\"([^\"]+)\"",
211
+ // JSX className attributes with single quotes: <div className='bg-blue-500 text-white'>
212
+ "<[^>]*?\\sclassName\\s*=\\s*'([^']+)'",
212
213
 
213
- // HTML class attributes with single quotes: <div class='bg-blue-500 text-white'>
214
- "<[^>]*?\\sclass\\s*=\\s*'([^']+)'",
214
+ // JSX className with braces and double quotes: <div className={"bg-blue-500 text-white"}>
215
+ "<[^>]*?\\sclassName\\s*=\\s*\\{\\s*\"([^\"]+)\"\\s*\\}",
215
216
 
216
- // JSX className attributes with double quotes: <div className="bg-blue-500 text-white">
217
- "<[^>]*?\\sclassName\\s*=\\s*\"([^\"]+)\"",
217
+ // JSX className with braces and single quotes: <div className={'bg-blue-500 text-white'}>
218
+ "<[^>]*?\\sclassName\\s*=\\s*\\{\\s*'([^']+)'\\s*\\}",
218
219
 
219
- // JSX className attributes with single quotes: <div className='bg-blue-500 text-white'>
220
- "<[^>]*?\\sclassName\\s*=\\s*'([^']+)'",
220
+ // JSX className with template literals: <div className={`bg-blue-500 ${variable}`}>
221
+ "<[^>]*?\\sclassName\\s*=\\s*\\{\\s*`([^`]*)`\\s*\\}",
221
222
 
222
- // JSX className with braces and double quotes: <div className={"bg-blue-500 text-white"}>
223
- "<[^>]*?\\sclassName\\s*=\\s*\\{\\s*\"([^\"]+)\"\\s*\\}",
223
+ // HTML class with template literals: <div class={`bg-blue-500 ${variable}`}>
224
+ "<[^>]*?\\sclass\\s*=\\s*\\{\\s*`([^`]*)`\\s*\\}",
224
225
 
225
- // JSX className with braces and single quotes: <div className={'bg-blue-500 text-white'}>
226
- "<[^>]*?\\sclassName\\s*=\\s*\\{\\s*'([^']+)'\\s*\\}",
226
+ // HTML class at start of tag with double quotes: <div class="bg-blue-500">
227
+ "<\\w+\\s+class\\s*=\\s*\"([^\"]+)\"",
227
228
 
228
- // JSX className with template literals: <div className={`bg-blue-500 ${variable}`}>
229
- "<[^>]*?\\sclassName\\s*=\\s*\\{\\s*`([^`]*)`\\s*\\}",
229
+ // HTML class at start of tag with single quotes: <div class='bg-blue-500'>
230
+ "<\\w+\\s+class\\s*=\\s*'([^']+)'",
230
231
 
231
- // HTML class with template literals: <div class={`bg-blue-500 ${variable}`}>
232
- "<[^>]*?\\sclass\\s*=\\s*\\{\\s*`([^`]*)`\\s*\\}",
232
+ // JSX className at start of tag with double quotes: <div className="bg-blue-500">
233
+ "<\\w+\\s+className\\s*=\\s*\"([^\"]+)\"",
233
234
 
234
- // HTML class at start of tag with double quotes: <div class="bg-blue-500">
235
- "<\\w+\\s+class\\s*=\\s*\"([^\"]+)\"",
235
+ // JSX className at start of tag with single quotes: <div className='bg-blue-500'>
236
+ "<\\w+\\s+className\\s*=\\s*'([^']+)'",
236
237
 
237
- // HTML class at start of tag with single quotes: <div class='bg-blue-500'>
238
- "<\\w+\\s+class\\s*=\\s*'([^']+)'",
238
+ // JSX className at start with braces and double quotes: <div className={"bg-blue-500"}>
239
+ "<\\w+\\s+className\\s*=\\s*\\{\\s*\"([^\"]+)\"\\s*\\}",
239
240
 
240
- // JSX className at start of tag with double quotes: <div className="bg-blue-500">
241
- "<\\w+\\s+className\\s*=\\s*\"([^\"]+)\"",
241
+ // JSX className at start with braces and single quotes: <div className={'bg-blue-500'}>
242
+ "<\\w+\\s+className\\s*=\\s*\\{\\s*'([^']+)'\\s*\\}",
242
243
 
243
- // JSX className at start of tag with single quotes: <div className='bg-blue-500'>
244
- "<\\w+\\s+className\\s*=\\s*'([^']+)'",
244
+ // JSX className at start with template literals: <div className={`bg-blue-500 ${variable}`}>
245
+ "<\\w+\\s+className\\s*=\\s*\\{\\s*`([^`]*)`\\s*\\}",
245
246
 
246
- // JSX className at start with braces and double quotes: <div className={"bg-blue-500"}>
247
- "<\\w+\\s+className\\s*=\\s*\\{\\s*\"([^\"]+)\"\\s*\\}",
247
+ // HTML class at start with template literals: <div class={`bg-blue-500 ${variable}`}>
248
+ "<\\w+\\s+class\\s*=\\s*\\{\\s*`([^`]*)`\\s*\\}",
249
+ ]
248
250
 
249
- // JSX className at start with braces and single quotes: <div className={'bg-blue-500'}>
250
- "<\\w+\\s+className\\s*=\\s*\\{\\s*'([^']+)'\\s*\\}",
251
+ const CLASS_NAME_REGEX = new RegExp(
252
+ CLASS_NAME_PATTERNS.map(pattern => `(?:${pattern})`).join("|"),
253
+ "g",
254
+ )
251
255
 
252
- // JSX className at start with template literals: <div className={`bg-blue-500 ${variable}`}>
253
- "<\\w+\\s+className\\s*=\\s*\\{\\s*`([^`]*)`\\s*\\}",
256
+ function hasCssImport(css: string, specifier?: string): boolean {
257
+ const [, importPath] = css.match(CSS_IMPORT_REGEX) ?? []
254
258
 
255
- // HTML class at start with template literals: <div class={`bg-blue-500 ${variable}`}>
256
- "<\\w+\\s+class\\s*=\\s*\\{\\s*`([^`]*)`\\s*\\}",
257
- ]
259
+ if (!importPath) return false
258
260
 
259
- // Combine all patterns into one regex using alternation
260
- const combinedPattern = patterns
261
- .map(pattern => `(?:${pattern})`)
262
- .join("|")
261
+ return specifier === undefined
262
+ || importPath.includes(specifier)
263
+ }
263
264
 
264
- const combinedRegex = new RegExp(combinedPattern, "g")
265
+ export function extractClassNames(source: string): Set<string> {
266
+ const candidates = new Set<string>()
267
+ const sourceWithoutComments = source.replace(HTML_COMMENT_REGEX, "")
265
268
 
266
- for (const match of sourceWithoutComments.matchAll(combinedRegex)) {
269
+ for (const match of sourceWithoutComments.matchAll(CLASS_NAME_REGEX)) {
267
270
  // Find the first non-undefined capture group (skip match[0] which is full match)
268
271
  let classString = ""
269
272
  for (let i = 1; i < match.length; i++) {
@@ -277,18 +280,14 @@ export function extractClassNames(source: string): Set<string> {
277
280
  continue
278
281
  }
279
282
 
280
- // Only apply complex processing if the string contains characters that require it
281
283
  if (classString.includes("${")) {
282
- // Split by ${...} expressions and process each static part
283
- const staticParts = classString.split(/\$\{[^}]*\}/g)
284
+ const staticParts = classString.split(TEMPLATE_EXPRESSION_REGEX)
284
285
 
285
286
  for (const part of staticParts) {
286
287
  const names = part.trim().split(/\s+/).filter(name => {
287
288
  if (name.length === 0) return false
288
- // Don't extract incomplete classes like "bg-" or "-500"
289
289
  if (name.endsWith("-") || name.startsWith("-")) return false
290
- // Basic Tailwind class pattern validation
291
- return /^[a-zA-Z0-9_:-]+(\[[^\]]*\])?$/.test(name)
290
+ return TAILWIND_CLASS_REGEX.test(name)
292
291
  })
293
292
  names.forEach(name => candidates.add(name))
294
293
  }
@@ -0,0 +1,17 @@
1
+ import * as NPath from "node:path"
2
+ import * as BunTailwindPlugin from "../../bun/BunTailwindPlugin.ts"
3
+ import * as NodeUtils from "../../NodeUtils.ts"
4
+
5
+ // Append `?dir=` to module identifier to pass custom directory to scan
6
+ const dirParam = URL.parse(import.meta.url)?.searchParams.get("dir")
7
+ const packageJson = await NodeUtils.findClosestPackageJson(process.cwd())
8
+ const scanPath = dirParam
9
+ ? NPath.resolve(process.cwd(), dirParam)
10
+ : packageJson
11
+ ? NPath.dirname(packageJson)
12
+ : process.cwd()
13
+
14
+ // Export as default to be used in bunfig.toml
15
+ export default BunTailwindPlugin.make({
16
+ scanPath,
17
+ })