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,335 @@
1
+ import * as t from "bun:test"
2
+
3
+ import { extractClassNames } from "./BunTailwindPlugin.ts"
4
+
5
+ // Keep the old broad implementation for comparison tests
6
+ function extractClassNamesBroad(source: string): Set<string> {
7
+ // Old broad implementation
8
+ const CLASS_NAME_REGEX = /^[^"'`\s]+$/
9
+ const classTokenRegex =
10
+ /\b[a-zA-Z][a-zA-Z0-9_:-]*(?:\[[^\]]*\])?(?:\/[0-9]+)?/g
11
+
12
+ return new Set(
13
+ Array
14
+ .from(source.matchAll(classTokenRegex))
15
+ .map(match => match[0])
16
+ .filter(token => CLASS_NAME_REGEX.test(token)),
17
+ )
18
+ }
19
+
20
+ t.describe("extractClassNames", () => {
21
+ t.test("Basic HTML class attributes", () => {
22
+ const source = `<div class="bg-red-500 text-white">Hello</div>`
23
+ const result = extractClassNames(source)
24
+
25
+ t
26
+ .expect([...result].sort())
27
+ .toEqual(["bg-red-500", "text-white"])
28
+ })
29
+
30
+ t.test("Basic JSX className attributes", () => {
31
+ const source =
32
+ `<div className="flex items-center justify-between">Content</div>`
33
+ const result = extractClassNames(source)
34
+
35
+ t
36
+ .expect([...result].sort())
37
+ .toEqual(["flex", "items-center", "justify-between"])
38
+ })
39
+
40
+ t.test("Single quotes", () => {
41
+ const source = `<div class='bg-blue-500 hover:bg-blue-600'>Button</div>`
42
+ const result = extractClassNames(source)
43
+
44
+ t
45
+ .expect([...result].sort())
46
+ .toEqual(["bg-blue-500", "hover:bg-blue-600"])
47
+ })
48
+
49
+ t.test("Template literals in JSX", () => {
50
+ const source = `<div className={\`bg-\${color} text-lg\`}>Dynamic</div>`
51
+ const result = extractClassNames(source)
52
+
53
+ // Should extract valid static class names from template literals
54
+ t
55
+ .expect([...result].sort())
56
+ .toEqual(["text-lg"])
57
+ })
58
+
59
+ t.test("JSX with quoted strings", () => {
60
+ const source = `<div className={"p-4 m-2"}>Static in braces</div>`
61
+ const result = extractClassNames(source)
62
+
63
+ t
64
+ .expect([...result].sort())
65
+ .toEqual(["m-2", "p-4"])
66
+ })
67
+
68
+ t.test("Multi-line attributes", () => {
69
+ const source = `<div
70
+ className="
71
+ grid
72
+ grid-cols-3
73
+ gap-4
74
+ "
75
+ >Grid</div>`
76
+ const result = extractClassNames(source)
77
+
78
+ t
79
+ .expect([...result].sort())
80
+ .toEqual(["gap-4", "grid", "grid-cols-3"])
81
+ })
82
+
83
+ t.test("Whitespace variations around equals", () => {
84
+ const cases = [
85
+ `<div class="text-sm">Normal</div>`,
86
+ `<div class ="text-md">Space before</div>`,
87
+ `<div class= "text-lg">Space after</div>`,
88
+ `<div class = "text-xl">Spaces both</div>`,
89
+ ]
90
+
91
+ for (const source of cases) {
92
+ const result = extractClassNames(source)
93
+
94
+ t
95
+ .expect(result.size)
96
+ .toBe(1)
97
+ }
98
+ })
99
+
100
+ t.test("Arbitrary value classes", () => {
101
+ const source =
102
+ `<div className="w-[32px] bg-[#ff0000] text-[1.5rem]">Arbitrary</div>`
103
+ const result = extractClassNames(source)
104
+
105
+ t
106
+ .expect([...result].sort())
107
+ .toEqual(["bg-[#ff0000]", "text-[1.5rem]", "w-[32px]"])
108
+ })
109
+
110
+ t.test("Fraction classes", () => {
111
+ const source = `<div className="w-1/2 h-3/4">Fractions</div>`
112
+ const result = extractClassNames(source)
113
+
114
+ t
115
+ .expect([...result].sort())
116
+ .toEqual(["h-3/4", "w-1/2"])
117
+ })
118
+
119
+ t.test("Complex Tailwind classes", () => {
120
+ const source =
121
+ `<div className="sm:w-1/2 md:w-1/3 lg:w-1/4 hover:bg-gray-100 focus:ring-2">Responsive</div>`
122
+ const result = extractClassNames(source)
123
+
124
+ t
125
+ .expect([...result].sort())
126
+ .toEqual([
127
+ "focus:ring-2",
128
+ "hover:bg-gray-100",
129
+ "lg:w-1/4",
130
+ "md:w-1/3",
131
+ "sm:w-1/2",
132
+ ])
133
+ })
134
+
135
+ t.test("Should ignore similar attribute names", () => {
136
+ const source =
137
+ `<div data-class="should-ignore" myclass="also-ignore" class="keep-this">Test</div>`
138
+ const result = extractClassNames(source)
139
+
140
+ t
141
+ .expect([...result])
142
+ .toEqual(["keep-this"])
143
+ })
144
+
145
+ t.test("Should handle case sensitivity", () => {
146
+ const source =
147
+ `<div Class="uppercase-class" class="lowercase-class">Mixed case</div>`
148
+ const result = extractClassNames(source)
149
+
150
+ // Our current implementation only matches lowercase 'class'
151
+ t
152
+ .expect([...result])
153
+ .toEqual(["lowercase-class"])
154
+ })
155
+
156
+ t.test("Empty class attributes", () => {
157
+ const source = `<div class="" className=''>Empty</div>`
158
+ const result = extractClassNames(source)
159
+
160
+ t
161
+ .expect(result.size)
162
+ .toBe(0)
163
+ })
164
+
165
+ t.test("Classes with special characters", () => {
166
+ const source =
167
+ `<div className="group-hover:text-blue-500 peer-focus:ring-2">Special chars</div>`
168
+ const result = extractClassNames(source)
169
+
170
+ t
171
+ .expect([...result].sort())
172
+ .toEqual(["group-hover:text-blue-500", "peer-focus:ring-2"])
173
+ })
174
+
175
+ t.test("Should not match classes in comments", () => {
176
+ const source = `
177
+ <!-- <div class="commented-out">Should not match</div> -->
178
+ <div class="real-class">Should match</div>
179
+ `
180
+ const result = extractClassNames(source)
181
+
182
+ t
183
+ .expect([...result])
184
+ .toEqual(["real-class"])
185
+ })
186
+
187
+ t.test("Should not match classes in strings", () => {
188
+ const source = `
189
+ const message = "This class='fake-class' should not match";
190
+ <div class="real-class">Real element</div>
191
+ `
192
+ const result = extractClassNames(source)
193
+
194
+ t
195
+ .expect([...result])
196
+ .toEqual(["real-class"])
197
+ })
198
+
199
+ t.test("Complex JSX expressions should be ignored", () => {
200
+ const source = `
201
+ <div className={condition ? "conditional-class" : "other-class"}>Conditional</div>
202
+ <div className={\`template-\${variable}\`}>Template</div>
203
+ <div className={getClasses()}>Function call</div>
204
+ <div className="static-class">Static</div>
205
+ `
206
+ const result = extractClassNames(source)
207
+
208
+ // Only the static class should match with our strict implementation
209
+ t
210
+ .expect([...result])
211
+ .toEqual(["static-class"])
212
+ })
213
+
214
+ t.test("Vue.js class bindings should be ignored", () => {
215
+ const source = `
216
+ <div :class="{ 'active': isActive }">Vue object</div>
217
+ <div :class="['base', condition && 'active']">Vue array</div>
218
+ <div class="static-vue-class">Static Vue</div>
219
+ `
220
+ const result = extractClassNames(source)
221
+
222
+ // Only static class should match
223
+ t
224
+ .expect([...result])
225
+ .toEqual(["static-vue-class"])
226
+ })
227
+
228
+ t.test("Svelte class directives should be ignored", () => {
229
+ const source = `
230
+ <div class:active={condition}>Svelte directive</div>
231
+ <div class="static-svelte-class">Static Svelte</div>
232
+ `
233
+ const result = extractClassNames(source)
234
+
235
+ t
236
+ .expect([...result])
237
+ .toEqual(["static-svelte-class"])
238
+ })
239
+
240
+ t.test("Escaped quotes should be handled", () => {
241
+ const source =
242
+ `<div class="text-sm before:content-['Hello']">Escaped quotes</div>`
243
+ const result = extractClassNames(source)
244
+
245
+ t
246
+ .expect([...result].sort())
247
+ .toEqual(["before:content-['Hello']", "text-sm"])
248
+ })
249
+
250
+ t.test("Current broad implementation comparison", () => {
251
+ const source = `
252
+ <div class="bg-red-500 text-white">Element</div>
253
+ <p>Some random-text-with-hyphens in content</p>
254
+ const variable = "some-string";
255
+ `
256
+
257
+ const broadResult = extractClassNamesBroad(source)
258
+ const strictResult = extractClassNames(source)
259
+
260
+ // Broad should pick up more tokens
261
+ t
262
+ .expect(broadResult.size)
263
+ .toBeGreaterThan(strictResult.size)
264
+
265
+ // Strict should only have the actual class names
266
+ t
267
+ .expect([...strictResult].sort())
268
+ .toEqual(["bg-red-500", "text-white"])
269
+ })
270
+
271
+ t.test("Component names with dots", () => {
272
+ const source =
273
+ `<Toast.Toast class="toast toast-top toast-center fixed top-8 z-10">Content</Toast.Toast>`
274
+ const result = extractClassNames(source)
275
+
276
+ t
277
+ .expect([...result].sort())
278
+ .toEqual(["fixed", "toast", "toast-center", "toast-top", "top-8", "z-10"])
279
+ })
280
+
281
+ t.test("Complex component names and attributes", () => {
282
+ const source = `
283
+ <My.Component.Name className="flex items-center">Content</My.Component.Name>
284
+ <Component-with-dashes class="bg-red-500">Content</Component-with-dashes>
285
+ <Component123 className="text-lg">Content</Component123>
286
+ <namespace:element class="border-2">XML style</namespace:element>
287
+ `
288
+ const result = extractClassNames(source)
289
+
290
+ t
291
+ .expect([...result].sort())
292
+ .toEqual([
293
+ "bg-red-500",
294
+ "border-2",
295
+ "flex",
296
+ "items-center",
297
+ "text-lg",
298
+ ])
299
+ })
300
+
301
+ t.test("Conditional JSX with Toast component", () => {
302
+ const source = `{toastParam !== undefined && (
303
+ <Toast.Toast class="toast toast-top toast-center fixed top-8 z-10">
304
+ <div class="alert alert-success">
305
+ <span>
306
+ {toastParam}
307
+ </span>
308
+ </div>
309
+ </Toast.Toast>
310
+ )}`
311
+ const result = extractClassNames(source)
312
+
313
+ t
314
+ .expect([...result].sort())
315
+ .toEqual([
316
+ "alert",
317
+ "alert-success",
318
+ "fixed",
319
+ "toast",
320
+ "toast-center",
321
+ "toast-top",
322
+ "top-8",
323
+ "z-10",
324
+ ])
325
+ })
326
+
327
+ t.test("Template literals with expressions", () => {
328
+ const source = `<div class={\`toast \${props.class ?? ""}\`}>Content</div>`
329
+ const result = extractClassNames(source)
330
+
331
+ t
332
+ .expect([...result].sort())
333
+ .toEqual(["toast"])
334
+ })
335
+ })
@@ -0,0 +1,322 @@
1
+ import type * as Tailwind from "@tailwindcss/node"
2
+ import type { BunPlugin } from "bun"
3
+ import * as NodePath from "node:path"
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
+
16
+ /**
17
+ * Pattern to match component and HTML files for class name extraction.
18
+ */
19
+ filesPattern?: RegExp
20
+
21
+ /**
22
+ * Pattern to match CSS files that import Tailwind.
23
+ */
24
+ cssPattern?: RegExp
25
+
26
+ /**
27
+ * Scan a path for candidates.
28
+ * By default, only class names found in files that are part of the import graph
29
+ * that imports tailwind are considered.
30
+ *
31
+ * This option scans the provided path and ensures that class names found under this path
32
+ * are includedd, even if they are not part of the import graph.
33
+ */
34
+ scanPath?: string
35
+ } = {}): BunPlugin => {
36
+ const {
37
+ filesPattern = /\.(jsx?|tsx?|html|svelte|vue|astro)$/,
38
+ cssPattern = /\.css$/,
39
+ importer = () =>
40
+ import("@tailwindcss/node").catch(err => {
41
+ throw new Error(
42
+ "Tailwind not found: install @tailwindcss/node or provide custom importer option",
43
+ )
44
+ }),
45
+ } = opts
46
+
47
+ return {
48
+ name: "Bun Tailwind.css plugin",
49
+ target: "browser",
50
+ async setup(builder) {
51
+ const Tailwind = await importer()
52
+
53
+ const scannedCandidates = new Set<string>()
54
+ // (file) -> (class names)
55
+ const classNameCandidates = new Map<string, Set<string>>()
56
+ // (importer path) -> (imported paths)
57
+ const importAncestors = new Map<string, Set<string>>()
58
+ // (imported path) -> (importer paths)
59
+ const importDescendants = new Map<string, Set<string>>()
60
+
61
+ if (opts.scanPath) {
62
+ const candidates = await scanFiles(opts.scanPath)
63
+
64
+ candidates.forEach(candidate => scannedCandidates.add(candidate))
65
+ }
66
+
67
+ /**
68
+ * Track import relationships.
69
+ * We do this to scope all class name candidates to tailwind entrypoints
70
+ */
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())
86
+ }
87
+
88
+ if (!importDescendants.has(fullPath)) {
89
+ importDescendants.set(fullPath, new Set())
90
+ }
91
+
92
+ if (!importAncestors.has(args.importer)) {
93
+ importAncestors.set(args.importer, new Set())
94
+ }
95
+
96
+ if (!importDescendants.has(args.importer)) {
97
+ importDescendants.set(args.importer, new Set())
98
+ }
99
+ }
100
+
101
+ importAncestors.get(fullPath)!.add(args.importer)
102
+ importDescendants.get(args.importer)!.add(fullPath)
103
+
104
+ return undefined
105
+ })
106
+
107
+ /**
108
+ * Scan for class name candidates in component files.
109
+ */
110
+ builder.onLoad({
111
+ filter: filesPattern,
112
+ }, async (args) => {
113
+ const contents = await Bun.file(args.path).text()
114
+ const classNames = extractClassNames(contents)
115
+
116
+ if (classNames.size > 0) {
117
+ classNameCandidates.set(args.path, classNames)
118
+ }
119
+
120
+ return undefined
121
+ })
122
+
123
+ /**
124
+ * Compile tailwind entrypoints.
125
+ */
126
+ builder.onLoad({
127
+ filter: cssPattern,
128
+ }, async (args) => {
129
+ const source = await Bun.file(args.path).text()
130
+
131
+ if (!hasCssImport(source, "tailwindcss")) {
132
+ return undefined
133
+ }
134
+
135
+ const compiler = await Tailwind.compile(source, {
136
+ base: NodePath.dirname(args.path),
137
+ shouldRewriteUrls: true,
138
+ onDependency: (path) => {},
139
+ })
140
+
141
+ // 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
+ await args.defer()
145
+
146
+ const candidates = new Set<string>()
147
+
148
+ scannedCandidates.forEach(candidate => candidates.add(candidate))
149
+
150
+ {
151
+ const pendingModules = [
152
+ // get class name candidates from all modules that import this one
153
+ ...(importAncestors.get(args.path) ?? []),
154
+ ]
155
+ const visitedModules = new Set<string>()
156
+
157
+ while (pendingModules.length > 0) {
158
+ const currentPath = pendingModules.shift()!
159
+
160
+ if (visitedModules.has(currentPath)) {
161
+ continue
162
+ }
163
+
164
+ const moduleImports = importDescendants.get(currentPath)
165
+
166
+ moduleImports?.forEach(moduleImport => {
167
+ const moduleCandidates = classNameCandidates.get(moduleImport)
168
+
169
+ moduleCandidates?.forEach(candidate => candidates.add(candidate))
170
+
171
+ pendingModules.push(moduleImport)
172
+ })
173
+
174
+ visitedModules.add(currentPath)
175
+ }
176
+ }
177
+
178
+ const contents = compiler.build([
179
+ ...candidates,
180
+ ])
181
+
182
+ return {
183
+ contents,
184
+ loader: "css",
185
+ }
186
+ })
187
+ },
188
+ }
189
+ }
190
+
191
+ const CSS_IMPORT_REGEX = /@import\s+(?:url\()?["']?([^"')]+)["']?\)?\s*[^;]*;/
192
+
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>()
204
+
205
+ // Remove HTML comments to avoid false matches
206
+ const sourceWithoutComments = source.replace(/<!--[\s\S]*?-->/g, "")
207
+
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*\"([^\"]+)\"",
212
+
213
+ // HTML class attributes with single quotes: <div class='bg-blue-500 text-white'>
214
+ "<[^>]*?\\sclass\\s*=\\s*'([^']+)'",
215
+
216
+ // JSX className attributes with double quotes: <div className="bg-blue-500 text-white">
217
+ "<[^>]*?\\sclassName\\s*=\\s*\"([^\"]+)\"",
218
+
219
+ // JSX className attributes with single quotes: <div className='bg-blue-500 text-white'>
220
+ "<[^>]*?\\sclassName\\s*=\\s*'([^']+)'",
221
+
222
+ // JSX className with braces and double quotes: <div className={"bg-blue-500 text-white"}>
223
+ "<[^>]*?\\sclassName\\s*=\\s*\\{\\s*\"([^\"]+)\"\\s*\\}",
224
+
225
+ // JSX className with braces and single quotes: <div className={'bg-blue-500 text-white'}>
226
+ "<[^>]*?\\sclassName\\s*=\\s*\\{\\s*'([^']+)'\\s*\\}",
227
+
228
+ // JSX className with template literals: <div className={`bg-blue-500 ${variable}`}>
229
+ "<[^>]*?\\sclassName\\s*=\\s*\\{\\s*`([^`]*)`\\s*\\}",
230
+
231
+ // HTML class with template literals: <div class={`bg-blue-500 ${variable}`}>
232
+ "<[^>]*?\\sclass\\s*=\\s*\\{\\s*`([^`]*)`\\s*\\}",
233
+
234
+ // HTML class at start of tag with double quotes: <div class="bg-blue-500">
235
+ "<\\w+\\s+class\\s*=\\s*\"([^\"]+)\"",
236
+
237
+ // HTML class at start of tag with single quotes: <div class='bg-blue-500'>
238
+ "<\\w+\\s+class\\s*=\\s*'([^']+)'",
239
+
240
+ // JSX className at start of tag with double quotes: <div className="bg-blue-500">
241
+ "<\\w+\\s+className\\s*=\\s*\"([^\"]+)\"",
242
+
243
+ // JSX className at start of tag with single quotes: <div className='bg-blue-500'>
244
+ "<\\w+\\s+className\\s*=\\s*'([^']+)'",
245
+
246
+ // JSX className at start with braces and double quotes: <div className={"bg-blue-500"}>
247
+ "<\\w+\\s+className\\s*=\\s*\\{\\s*\"([^\"]+)\"\\s*\\}",
248
+
249
+ // JSX className at start with braces and single quotes: <div className={'bg-blue-500'}>
250
+ "<\\w+\\s+className\\s*=\\s*\\{\\s*'([^']+)'\\s*\\}",
251
+
252
+ // JSX className at start with template literals: <div className={`bg-blue-500 ${variable}`}>
253
+ "<\\w+\\s+className\\s*=\\s*\\{\\s*`([^`]*)`\\s*\\}",
254
+
255
+ // HTML class at start with template literals: <div class={`bg-blue-500 ${variable}`}>
256
+ "<\\w+\\s+class\\s*=\\s*\\{\\s*`([^`]*)`\\s*\\}",
257
+ ]
258
+
259
+ // Combine all patterns into one regex using alternation
260
+ const combinedPattern = patterns
261
+ .map(pattern => `(?:${pattern})`)
262
+ .join("|")
263
+
264
+ const combinedRegex = new RegExp(combinedPattern, "g")
265
+
266
+ for (const match of sourceWithoutComments.matchAll(combinedRegex)) {
267
+ // Find the first non-undefined capture group (skip match[0] which is full match)
268
+ let classString = ""
269
+ for (let i = 1; i < match.length; i++) {
270
+ if (match[i] !== undefined) {
271
+ classString = match[i]
272
+ break
273
+ }
274
+ }
275
+
276
+ if (!classString) {
277
+ continue
278
+ }
279
+
280
+ // Only apply complex processing if the string contains characters that require it
281
+ if (classString.includes("${")) {
282
+ // Split by ${...} expressions and process each static part
283
+ const staticParts = classString.split(/\$\{[^}]*\}/g)
284
+
285
+ for (const part of staticParts) {
286
+ const names = part.trim().split(/\s+/).filter(name => {
287
+ if (name.length === 0) return false
288
+ // Don't extract incomplete classes like "bg-" or "-500"
289
+ if (name.endsWith("-") || name.startsWith("-")) return false
290
+ // Basic Tailwind class pattern validation
291
+ return /^[a-zA-Z0-9_:-]+(\[[^\]]*\])?$/.test(name)
292
+ })
293
+ names.forEach(name => candidates.add(name))
294
+ }
295
+ } else {
296
+ // Simple case: regular class string without expressions
297
+ const names = classString.split(/\s+/).filter(name => name.length > 0)
298
+ names.forEach(name => candidates.add(name))
299
+ }
300
+ }
301
+
302
+ return candidates
303
+ }
304
+
305
+ async function scanFiles(dir: string): Promise<Set<string>> {
306
+ const candidates = new Set<string>()
307
+ const glob = new Bun.Glob("**/*.{js,jsx,ts,tsx,html,vue,svelte,astro}")
308
+
309
+ for await (
310
+ const filePath of glob.scan({
311
+ cwd: dir,
312
+ absolute: true,
313
+ })
314
+ ) {
315
+ const contents = await Bun.file(filePath).text()
316
+ const classNames = extractClassNames(contents)
317
+
318
+ classNames.forEach((className) => candidates.add(className))
319
+ }
320
+
321
+ return candidates
322
+ }