turbopack-unocss-transform 1.0.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/LICENSE +21 -0
- package/README.md +30 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.mjs +44 -0
- package/dist/loader.mjs +196 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 vizet
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# turbopack-unocss-transform
|
|
2
|
+
A Turbopack loader for Next.js that applies UnoCSS transformers (like variant-group and attributify-jsx) directly to your TS/JS/TSX/JSX source before CSS generation. Actual CSS is produced by @unocss/postcss; this loader only transforms source code.
|
|
3
|
+
|
|
4
|
+
**Usage**
|
|
5
|
+
|
|
6
|
+
`next.config.ts`
|
|
7
|
+
```typescript
|
|
8
|
+
import type {NextConfig} from "next"
|
|
9
|
+
import withUnoTransform from "turbopack-unocss-transform"
|
|
10
|
+
|
|
11
|
+
const nextConfig: NextConfig = withUnoTransform({
|
|
12
|
+
// your Next.js config
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export default nextConfig
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`postcss.config.mjs`
|
|
19
|
+
```js
|
|
20
|
+
import unoConfig from "./src/styles/uno.config"
|
|
21
|
+
|
|
22
|
+
export default {
|
|
23
|
+
plugins: [
|
|
24
|
+
["@unocss/postcss", {
|
|
25
|
+
configOrPath: unoConfig
|
|
26
|
+
}],
|
|
27
|
+
"autoprefixer"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
```
|
package/dist/index.d.ts
ADDED
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
|
|
3
|
+
export default function withUnoTransform(userConfig = {}) {
|
|
4
|
+
const loaderPath = path.resolve(process.cwd(), "node_modules/turbopack-unocss-transform/dist/loader.mjs")
|
|
5
|
+
const base = {
|
|
6
|
+
turbopack: {
|
|
7
|
+
rules: {
|
|
8
|
+
"**/*.{ts,tsx,js,jsx}": {
|
|
9
|
+
loaders: [loaderPath]
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return merge(base, userConfig)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function merge(a, b) {
|
|
19
|
+
if (!b) return a
|
|
20
|
+
|
|
21
|
+
const out = JSON.parse(JSON.stringify(a))
|
|
22
|
+
|
|
23
|
+
mergeInto(out, b)
|
|
24
|
+
|
|
25
|
+
return out
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function mergeInto(t, s) {
|
|
29
|
+
for (const k of Object.keys(s)) {
|
|
30
|
+
const sv = s[k], tv = t[k]
|
|
31
|
+
|
|
32
|
+
if (Array.isArray(sv)) {
|
|
33
|
+
t[k] = Array.isArray(tv) ? [...tv, ...sv] : [...sv]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
else if (sv && typeof sv === "object") {
|
|
37
|
+
if (!tv || typeof tv !== "object") t[k] = {}
|
|
38
|
+
|
|
39
|
+
mergeInto(t[k], sv)
|
|
40
|
+
} else {
|
|
41
|
+
t[k] = sv
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
package/dist/loader.mjs
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import MagicString from "magic-string"
|
|
3
|
+
import {createGenerator} from "unocss"
|
|
4
|
+
import {createRequire} from "node:module"
|
|
5
|
+
|
|
6
|
+
const nodeRequire = createRequire(import.meta.url)
|
|
7
|
+
|
|
8
|
+
let uno = null
|
|
9
|
+
let unoInitPromise = null
|
|
10
|
+
let cfg = null
|
|
11
|
+
let cfgLoaded = false
|
|
12
|
+
|
|
13
|
+
function loadPostcssConfig() {
|
|
14
|
+
const jiti = nodeRequire("jiti")(import.meta.url, {
|
|
15
|
+
interopDefault: true,
|
|
16
|
+
esmResolve: true
|
|
17
|
+
})
|
|
18
|
+
const pcPath = path.join(process.cwd(), "postcss.config.mjs")
|
|
19
|
+
|
|
20
|
+
let mod = jiti(pcPath)
|
|
21
|
+
|
|
22
|
+
return mod?.default ?? mod
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function loadUnoConfigFromPostcss() {
|
|
26
|
+
if (cfgLoaded) return cfg
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
nodeRequire("tsconfig-paths/register")
|
|
30
|
+
} catch {}
|
|
31
|
+
|
|
32
|
+
const pc = loadPostcssConfig()
|
|
33
|
+
|
|
34
|
+
if (!pc || !Array.isArray(pc.plugins)) {
|
|
35
|
+
throw new Error("[UnoCSS-TP] postcss.config.mjs is invalid (no plugins).")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let found = null
|
|
39
|
+
|
|
40
|
+
for (const entry of pc.plugins) {
|
|
41
|
+
if (!Array.isArray(entry)) continue
|
|
42
|
+
|
|
43
|
+
const [nameOrFn, opts] = entry
|
|
44
|
+
const isUno = (
|
|
45
|
+
typeof nameOrFn === "string" && nameOrFn === "@unocss/postcss"
|
|
46
|
+
) || (
|
|
47
|
+
typeof nameOrFn === "function" && (nameOrFn.name?.toLowerCase().includes("unocss") || nameOrFn.name?.includes("Uno"))
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if (!isUno) continue
|
|
51
|
+
|
|
52
|
+
found = opts?.configOrPath
|
|
53
|
+
|
|
54
|
+
break
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!found || typeof found !== "object") {
|
|
58
|
+
throw new Error("[UnoCSS-TP] Required object unoConfig: [\"@unocss/postcss\", { configOrPath: unoConfig }]")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
cfg = found
|
|
62
|
+
cfgLoaded = true
|
|
63
|
+
|
|
64
|
+
return cfg
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function getUno() {
|
|
68
|
+
if (uno) return uno
|
|
69
|
+
if (unoInitPromise) return unoInitPromise
|
|
70
|
+
|
|
71
|
+
const config = loadUnoConfigFromPostcss()
|
|
72
|
+
|
|
73
|
+
unoInitPromise = createGenerator(config).then(u => (uno = u))
|
|
74
|
+
|
|
75
|
+
return unoInitPromise
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isProcessable(id) {
|
|
79
|
+
return !!id
|
|
80
|
+
&& !id.includes("node_modules")
|
|
81
|
+
&& !/\.d\.ts$/.test(id)
|
|
82
|
+
&& !/\.(test|spec)\.(t|j)sx?$/.test(id)
|
|
83
|
+
&& /\.(t|j)sx?$/.test(id)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function pickTransformers(enforce = "default") {
|
|
87
|
+
const list = (cfg?.transformers || [])
|
|
88
|
+
|
|
89
|
+
return list.filter(t => (t?.enforce || "default") === enforce)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function applyTransformersPipeline(code, id) {
|
|
93
|
+
const u = await getUno()
|
|
94
|
+
const original = code
|
|
95
|
+
const phases = ["pre","default","post"]
|
|
96
|
+
let current = code
|
|
97
|
+
|
|
98
|
+
for (const phase of phases) {
|
|
99
|
+
const transformers = pickTransformers(phase)
|
|
100
|
+
|
|
101
|
+
if (!transformers.length) continue
|
|
102
|
+
|
|
103
|
+
let s = new MagicString(current)
|
|
104
|
+
let changed = false
|
|
105
|
+
|
|
106
|
+
for (const t of transformers) {
|
|
107
|
+
if (!t) continue
|
|
108
|
+
|
|
109
|
+
if (t.idFilter) {
|
|
110
|
+
try {
|
|
111
|
+
if (!t.idFilter(id)) continue
|
|
112
|
+
} catch {}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const fn = t.transform || t
|
|
116
|
+
|
|
117
|
+
if (typeof fn !== "function") continue
|
|
118
|
+
|
|
119
|
+
const ctx = {
|
|
120
|
+
uno: u,
|
|
121
|
+
filename: id,
|
|
122
|
+
tokens: new Set(),
|
|
123
|
+
filter: isProcessable
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
await fn(s, id, ctx)
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error(`[UnoCSS-TP] transform failed in ${t.name || "transform"} for ${path.relative(process.cwd(), id)}:`, error?.stack || error?.message || error)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (s.hasChanged()) {
|
|
133
|
+
current = s.toString()
|
|
134
|
+
s = new MagicString(current)
|
|
135
|
+
changed = true
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!changed) continue
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (current !== original) {
|
|
143
|
+
return {
|
|
144
|
+
code: current
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const memo = new Map()
|
|
152
|
+
const MEMO_LIMIT = 500
|
|
153
|
+
|
|
154
|
+
function sha1Sync(s) {
|
|
155
|
+
const {createHash} = nodeRequire("node:crypto")
|
|
156
|
+
|
|
157
|
+
return createHash("sha1").update(s).digest("hex")
|
|
158
|
+
}
|
|
159
|
+
function memoGet(key) {
|
|
160
|
+
if (!memo.has(key)) return null
|
|
161
|
+
|
|
162
|
+
const v = memo.get(key)
|
|
163
|
+
|
|
164
|
+
memo.delete(key)
|
|
165
|
+
memo.set(key, v)
|
|
166
|
+
|
|
167
|
+
return v
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function memoSet(key, val) {
|
|
171
|
+
memo.set(key, val)
|
|
172
|
+
|
|
173
|
+
if (memo.size > MEMO_LIMIT) {
|
|
174
|
+
memo.delete(memo.keys().next().value)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export default async function unoLoader(source) {
|
|
179
|
+
const code = String(source)
|
|
180
|
+
const file = this?.resourcePath || this?.resource || ""
|
|
181
|
+
|
|
182
|
+
if (!isProcessable(file)) return code
|
|
183
|
+
if (code.length < 10) return code
|
|
184
|
+
|
|
185
|
+
const key = file + ":" + sha1Sync(code)
|
|
186
|
+
const cached = memoGet(key)
|
|
187
|
+
|
|
188
|
+
if (cached) return cached
|
|
189
|
+
|
|
190
|
+
const res = await applyTransformersPipeline(code, file)
|
|
191
|
+
const out = (res?.code && res.code !== code) ? res.code : code
|
|
192
|
+
|
|
193
|
+
memoSet(key, out)
|
|
194
|
+
|
|
195
|
+
return out
|
|
196
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "turbopack-unocss-transform",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"author": "vizet <v@kogita.net>",
|
|
6
|
+
"description": "A Turbopack loader for Next.js that applies UnoCSS transformers (like variant-group and attributify-jsx) directly to your TS/JS/TSX/JSX source before CSS generation.",
|
|
7
|
+
"homepage": "https://github.com/vizet/turbopack-unocss-transform",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/vizet/turbopack-unocss-transform.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"nextjs",
|
|
14
|
+
"turbopack",
|
|
15
|
+
"unocss",
|
|
16
|
+
"postcss",
|
|
17
|
+
"loader",
|
|
18
|
+
"transform",
|
|
19
|
+
"transformer",
|
|
20
|
+
"transformers",
|
|
21
|
+
"attributify",
|
|
22
|
+
"attributify-jsx"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "dist/loader.mjs",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"default": "./dist/index.mjs"
|
|
30
|
+
},
|
|
31
|
+
"./loader": "./dist/loader.mjs"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist"
|
|
35
|
+
],
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"unocss": ">=0.58.0"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"jiti": "^1.21.0",
|
|
41
|
+
"magic-string": "^0.30.10",
|
|
42
|
+
"next": "^16.0.1",
|
|
43
|
+
"tsconfig-paths": "^4.2.0"
|
|
44
|
+
}
|
|
45
|
+
}
|