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 +9 -12
- package/package.json +2 -1
- package/src/NodeUtils.ts +23 -0
- package/src/bun/BunTailwindPlugin.ts +93 -94
- package/src/x/tailwind/plugin.ts +17 -0
package/README.md
CHANGED
|
@@ -52,27 +52,24 @@ src/routes
|
|
|
52
52
|
|
|
53
53
|
### Tailwind CSS Support
|
|
54
54
|
|
|
55
|
-
|
|
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
|
|
61
|
+
bun add tailwindcss
|
|
59
62
|
```
|
|
60
63
|
|
|
61
|
-
|
|
64
|
+
Then, register a plugin in `bunfig.toml`:
|
|
62
65
|
|
|
63
66
|
```toml
|
|
64
67
|
[serve.static]
|
|
65
|
-
plugins = ["
|
|
68
|
+
plugins = ["effect-start/x/tailwind/plugin"]
|
|
66
69
|
```
|
|
67
70
|
|
|
68
|
-
Finally, include it in your `src/app.
|
|
71
|
+
Finally, include it in your `src/app.css`:
|
|
69
72
|
|
|
70
73
|
```html
|
|
71
|
-
|
|
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.
|
|
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
|
},
|
package/src/NodeUtils.ts
ADDED
|
@@ -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
|
|
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
|
-
*
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Register every visited module.
|
|
89
|
+
*/
|
|
90
|
+
{
|
|
91
|
+
if (!importAncestors.has(fullPath)) {
|
|
92
|
+
importAncestors.set(fullPath, new Set())
|
|
93
|
+
}
|
|
91
94
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
if (!importDescendants.has(fullPath)) {
|
|
96
|
+
importDescendants.set(fullPath, new Set())
|
|
97
|
+
}
|
|
95
98
|
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
102
|
-
|
|
108
|
+
importAncestors.get(fullPath)!.add(importer)
|
|
109
|
+
importDescendants.get(importer)!.add(fullPath)
|
|
103
110
|
|
|
104
|
-
|
|
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:
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
//
|
|
206
|
-
|
|
208
|
+
// JSX className attributes with double quotes: <div className="bg-blue-500 text-white">
|
|
209
|
+
"<[^>]*?\\sclassName\\s*=\\s*\"([^\"]+)\"",
|
|
207
210
|
|
|
208
|
-
//
|
|
209
|
-
|
|
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
|
-
|
|
214
|
-
|
|
214
|
+
// JSX className with braces and double quotes: <div className={"bg-blue-500 text-white"}>
|
|
215
|
+
"<[^>]*?\\sclassName\\s*=\\s*\\{\\s*\"([^\"]+)\"\\s*\\}",
|
|
215
216
|
|
|
216
|
-
|
|
217
|
-
|
|
217
|
+
// JSX className with braces and single quotes: <div className={'bg-blue-500 text-white'}>
|
|
218
|
+
"<[^>]*?\\sclassName\\s*=\\s*\\{\\s*'([^']+)'\\s*\\}",
|
|
218
219
|
|
|
219
|
-
|
|
220
|
-
|
|
220
|
+
// JSX className with template literals: <div className={`bg-blue-500 ${variable}`}>
|
|
221
|
+
"<[^>]*?\\sclassName\\s*=\\s*\\{\\s*`([^`]*)`\\s*\\}",
|
|
221
222
|
|
|
222
|
-
|
|
223
|
-
|
|
223
|
+
// HTML class with template literals: <div class={`bg-blue-500 ${variable}`}>
|
|
224
|
+
"<[^>]*?\\sclass\\s*=\\s*\\{\\s*`([^`]*)`\\s*\\}",
|
|
224
225
|
|
|
225
|
-
|
|
226
|
-
|
|
226
|
+
// HTML class at start of tag with double quotes: <div class="bg-blue-500">
|
|
227
|
+
"<\\w+\\s+class\\s*=\\s*\"([^\"]+)\"",
|
|
227
228
|
|
|
228
|
-
|
|
229
|
-
|
|
229
|
+
// HTML class at start of tag with single quotes: <div class='bg-blue-500'>
|
|
230
|
+
"<\\w+\\s+class\\s*=\\s*'([^']+)'",
|
|
230
231
|
|
|
231
|
-
|
|
232
|
-
|
|
232
|
+
// JSX className at start of tag with double quotes: <div className="bg-blue-500">
|
|
233
|
+
"<\\w+\\s+className\\s*=\\s*\"([^\"]+)\"",
|
|
233
234
|
|
|
234
|
-
|
|
235
|
-
|
|
235
|
+
// JSX className at start of tag with single quotes: <div className='bg-blue-500'>
|
|
236
|
+
"<\\w+\\s+className\\s*=\\s*'([^']+)'",
|
|
236
237
|
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
241
|
-
|
|
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
|
-
|
|
244
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
+
const CLASS_NAME_REGEX = new RegExp(
|
|
252
|
+
CLASS_NAME_PATTERNS.map(pattern => `(?:${pattern})`).join("|"),
|
|
253
|
+
"g",
|
|
254
|
+
)
|
|
251
255
|
|
|
252
|
-
|
|
253
|
-
|
|
256
|
+
function hasCssImport(css: string, specifier?: string): boolean {
|
|
257
|
+
const [, importPath] = css.match(CSS_IMPORT_REGEX) ?? []
|
|
254
258
|
|
|
255
|
-
|
|
256
|
-
"<\\w+\\s+class\\s*=\\s*\\{\\s*`([^`]*)`\\s*\\}",
|
|
257
|
-
]
|
|
259
|
+
if (!importPath) return false
|
|
258
260
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
.join("|")
|
|
261
|
+
return specifier === undefined
|
|
262
|
+
|| importPath.includes(specifier)
|
|
263
|
+
}
|
|
263
264
|
|
|
264
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
})
|