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 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
+ ```
@@ -0,0 +1,3 @@
1
+ import type {NextConfig} from "next"
2
+ export type WithUnoTransformOptions = Partial<NextConfig>
3
+ export default function withUnoTransform(options?: WithUnoTransformOptions): NextConfig
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
+ }
@@ -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
+ }