Skip to content

Commit 4b4fc8c

Browse files
committed
feat: separated implementation for expo and bare rn
1 parent 97e4538 commit 4b4fc8c

13 files changed

Lines changed: 862 additions & 449 deletions

File tree

apps/bare/src/global.css

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,4 @@
33

44
@theme {
55
--color-primary: oklch(0.72 0.11 178);
6-
}
7-
8-
/* Auto genereated by uniwind - do not modify */
9-
10-
@custom-variant light (&:where(.light, .light *));
11-
@custom-variant dark (&:where(.dark, .dark *));
12-
13-
/* Auto genereated by uniwind - do not modify */
6+
}

apps/bare/uniwind-types.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// NOTE: This file is generated by uniwind and it should not be edited manually.
2+
/// <reference types="uniwind/types" />
3+
4+
declare module 'uniwind' {
5+
export interface UniwindConfig {
6+
themes: readonly ['light', 'dark']
7+
}
8+
}
9+
10+
export {}

bun.lock

Lines changed: 468 additions & 226 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/uniwind/build.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ export default defineBuildConfig({
4343
input: './src/metro',
4444
name: 'metro/index',
4545
},
46+
{
47+
builder: 'rollup',
48+
input: './src/metro/expo-transformer.ts',
49+
name: 'metro/expo-transformer',
50+
},
4651
{
4752
builder: 'mkdist',
4853
input: './src/metro',

packages/uniwind/package.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"private": false,
33
"name": "uniwind",
4-
"version": "1.0.0-beta.5",
4+
"version": "1.0.0-beta.6",
55
"description": "The fastest Tailwind bindings for React Native",
66
"homepage": "https://github.com/Unistyles-OSS/uniwind",
77
"author": "UnistylesOSS",
@@ -62,8 +62,8 @@
6262
"culori": "4.0.2"
6363
},
6464
"peerDependencies": {
65-
"react": ">=19.0.0",
66-
"react-native": ">=0.81.0",
65+
"react": ">=19",
66+
"react-native": ">=0.81",
6767
"tailwindcss": ">=4",
6868
"postcss": ">=8",
6969
"chalk": ">=2",
@@ -74,11 +74,13 @@
7474
"@types/postcss-js": "4.0.4",
7575
"@types/culori": "4.0.1",
7676
"typescript": "5.9.3",
77-
"metro-config": "0.82.5",
77+
"metro-config": "0.83.3",
7878
"@babel/core": "7.28.4",
7979
"@babel/types": "7.28.4",
8080
"dpdm": "3.14.0",
8181
"unbuild": "3.6.1",
82-
"lightningcss": "1.30.2"
82+
"lightningcss": "1.30.2",
83+
"metro-transform-worker": "0.83.3",
84+
"@expo/metro-config": "54.0.6"
8385
}
8486
}

packages/uniwind/src/components/web/rnw.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import '../../core/config'
2-
import './metro-injected'
2+
import './uniwind-metro-injected'
33

44
const moveCssRulesToUtilitiesLayer = (sourceSheet: CSSStyleSheet) => {
55
const layerRuleIndex = sourceSheet.insertRule(
@@ -24,4 +24,7 @@ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
2424
}
2525
}
2626

27-
export const toRNWClassName = (className?: string) => ({ $$css: true, tailwind: className }) as {}
27+
export const toRNWClassName = (className?: string): {} =>
28+
className !== undefined
29+
? ({ $$css: true, tailwind: className })
30+
: {}

packages/uniwind/src/components/web/metro-injected.ts renamed to packages/uniwind/src/components/web/uniwind-metro-injected.ts

File renamed without changes.
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { Scanner } from '@tailwindcss/oxide'
2+
import fs from 'fs'
3+
import { ServerConfigT } from 'metro-config'
4+
import { Resolution } from 'metro-resolver'
5+
import path from 'path'
6+
import { compileVirtual } from './compileVirtual'
7+
import { getSources } from './getSources'
8+
import { injectThemes } from './injectThemes'
9+
import { ExtendedBundler, ExtendedFileSystem, FileChangeEvent, Platform, UniwindMetroTransformerOptions } from './types'
10+
import { areSetsEqual } from './utils'
11+
12+
const getVirtualPath = (platform: string) => `${platform}.uniwind.${platform === Platform.Web ? 'css' : 'js'}`
13+
const getPlatformFromVirtualPath = (path: string) => {
14+
const [, platform] = path.match(/^(\w+)\.uniwind\./) ?? []
15+
16+
return platform as Platform | undefined
17+
}
18+
const platforms = [Platform.iOS, Platform.Android, Platform.Web]
19+
20+
type EnchangeMiddlewareParameters = Parameters<ServerConfigT['enhanceMiddleware']>
21+
22+
export class UniwindBareRN {
23+
private virtualModules = new Map<string, string>()
24+
private injectedThemesScript = ''
25+
private css = ''
26+
private candidates = new Set<string>()
27+
28+
constructor(private readonly uniwindOptions: UniwindMetroTransformerOptions) {
29+
this.injectedThemesScript = injectThemes(uniwindOptions)
30+
}
31+
32+
init(metroServer: EnchangeMiddlewareParameters[1]) {
33+
const bundler = metroServer.getBundler().getBundler()
34+
const watcher = bundler.getWatcher()
35+
36+
bundler.getDependencyGraph().then(async graph => {
37+
// @ts-expect-error Hidden property
38+
this.ensureFileSystemPatched(graph._fileSystem)
39+
this.ensureBundlerPatched(bundler)
40+
41+
watcher.on('change', (event: FileChangeEvent) => {
42+
if ('eventsQueue' in event) {
43+
if (
44+
// Listen only to changes in JS/TS/css files
45+
!event.eventsQueue.some(event => {
46+
return ['.js', '.jsx', '.ts', '.tsx', '.css'].some(ext => event.filePath.endsWith(ext))
47+
}) || event.eventsQueue.every(event => event.filePath.endsWith('uniwind.css'))
48+
) {
49+
return
50+
}
51+
52+
const css = fs.readFileSync(this.uniwindOptions.input, 'utf-8')
53+
const candidates = new Set(this.getCandidates(css))
54+
const tailwindHasChanged = css !== this.css || !areSetsEqual(this.candidates, candidates)
55+
56+
if (!tailwindHasChanged) {
57+
return
58+
}
59+
60+
this.injectedThemesScript = injectThemes(this.uniwindOptions)
61+
this.css = css
62+
this.candidates = candidates
63+
64+
platforms.forEach(platform => {
65+
watcher.emit(
66+
'change',
67+
{
68+
eventsQueue: [{
69+
filePath: getVirtualPath(platform),
70+
metadata: {
71+
modifiedTime: Date.now(),
72+
size: 1,
73+
type: 'virtual',
74+
},
75+
type: 'change',
76+
}],
77+
} satisfies FileChangeEvent,
78+
)
79+
})
80+
}
81+
})
82+
83+
await Promise.all(platforms.map(platform => this.getVirtualFile(platform)))
84+
})
85+
}
86+
87+
resolve(resolved: Resolution, platform: string | null) {
88+
if (('filePath' in resolved && resolved.filePath !== this.uniwindOptions.input)) {
89+
return resolved
90+
}
91+
92+
if (platform !== Platform.iOS && platform !== Platform.Android && platform !== Platform.Web) {
93+
return resolved
94+
}
95+
96+
return {
97+
...resolved,
98+
filePath: getVirtualPath(platform),
99+
}
100+
}
101+
102+
private ensureBundlerPatched(bundler: ExtendedBundler) {
103+
if (bundler.transformFile.__uniwind_patched) {
104+
return
105+
}
106+
107+
const transformFile = bundler.transformFile.bind(bundler)
108+
109+
bundler.transformFile = async (
110+
filePath,
111+
transformOptions,
112+
fileBuffer,
113+
) => {
114+
const isVirtualFile = this.virtualModules.has(filePath)
115+
116+
if (filePath.endsWith('/components/web/uniwind-metro-injected.js')) {
117+
fileBuffer = Buffer.from(this.injectedThemesScript)
118+
}
119+
120+
if (isVirtualFile) {
121+
const platform = getPlatformFromVirtualPath(filePath)
122+
123+
if (platform) {
124+
const virtualFile = await this.getVirtualFile(platform)
125+
126+
fileBuffer = Buffer.from([
127+
virtualFile,
128+
platform !== Platform.Web ? this.injectedThemesScript : '',
129+
].join(''))
130+
}
131+
}
132+
133+
return transformFile(filePath, transformOptions, fileBuffer)
134+
}
135+
136+
bundler.transformFile.__uniwind_patched = true
137+
}
138+
139+
private ensureFileSystemPatched(fs: ExtendedFileSystem) {
140+
if (!fs.getSha1.__uniwind_patched) {
141+
const original_getSha1 = fs.getSha1.bind(fs)
142+
143+
fs.getSha1 = filename => {
144+
if (this.virtualModules.has(filename)) {
145+
return `${filename}-${Date.now()}`
146+
}
147+
148+
return original_getSha1(filename)
149+
}
150+
fs.getSha1.__uniwind_patched = true
151+
}
152+
153+
return fs
154+
}
155+
156+
private async getVirtualFile(platform: Platform) {
157+
const virtualFile = await compileVirtual({
158+
candidates: Array.from(this.candidates),
159+
platform,
160+
css: this.css,
161+
cssPath: this.uniwindOptions.input,
162+
themes: this.uniwindOptions.themes,
163+
})
164+
const injected = injectThemes(this.uniwindOptions)
165+
166+
this.injectedThemesScript = injected
167+
this.virtualModules.set(getVirtualPath(platform), virtualFile)
168+
169+
return virtualFile
170+
}
171+
172+
private getCandidates(css: string) {
173+
const sources = getSources(css, path.dirname(this.uniwindOptions.input))
174+
const candidates = new Scanner({ sources }).scan()
175+
176+
return candidates
177+
}
178+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { Scanner } from '@tailwindcss/oxide'
2+
import fs from 'fs'
3+
import type {
4+
JsTransformerConfig,
5+
JsTransformOptions,
6+
TransformResponse,
7+
} from 'metro-transform-worker'
8+
import path from 'path'
9+
import { compileVirtual } from './compileVirtual'
10+
import { getSources } from './getSources'
11+
import { injectThemes } from './injectThemes'
12+
import { Platform, UniwindMetroTransformerOptions } from './types'
13+
import { areSetsEqual } from './utils'
14+
15+
let metroTransformerPath: string
16+
let worker: typeof import('metro-transform-worker')
17+
18+
try {
19+
metroTransformerPath = (require('@expo/metro-config') as typeof import('@expo/metro-config')).unstable_transformerPath
20+
} catch {
21+
metroTransformerPath = '@expo/metro-config/build/transform-worker/transform-worker.js'
22+
}
23+
24+
try {
25+
worker = require(metroTransformerPath)
26+
} catch {
27+
worker = require('metro-transform-worker')
28+
}
29+
30+
const uniwindCache = {
31+
css: '',
32+
candidates: new Set<string>(),
33+
virtual: {
34+
[Platform.Web]: null,
35+
[Platform.iOS]: null,
36+
[Platform.Android]: null,
37+
[Platform.Native]: null,
38+
} as Record<Platform, TransformResponse | null>,
39+
}
40+
41+
export const transform = async (
42+
config: JsTransformerConfig & { uniwind?: UniwindMetroTransformerOptions },
43+
projectRoot: string,
44+
filePath: string,
45+
data: Buffer,
46+
options: JsTransformOptions & { uniwind?: UniwindMetroTransformerOptions },
47+
): Promise<TransformResponse> => {
48+
const uniwindOptions = options.uniwind ?? config.uniwind
49+
50+
if (uniwindOptions === undefined || options.platform === undefined) {
51+
return worker.transform(config, projectRoot, filePath, data, options)
52+
}
53+
54+
if (filePath.endsWith('uniwind-metro-injected.js')) {
55+
const injected = injectThemes({
56+
input: uniwindOptions.input,
57+
themes: uniwindOptions.themes,
58+
dtsPath: uniwindOptions.dtsPath,
59+
})
60+
61+
data = Buffer.from(injected)
62+
}
63+
64+
const isCss = options.type !== 'asset' && filePath.endsWith('.css')
65+
66+
if (!isCss) {
67+
return worker.transform(config, projectRoot, filePath, data, options)
68+
}
69+
70+
const css = fs.readFileSync(filePath, 'utf-8')
71+
const platform = options.platform as Platform
72+
const sources = getSources(css, path.dirname(uniwindOptions.input))
73+
const candidates = new Scanner({ sources }).scan()
74+
const candidatesSet = new Set(candidates)
75+
76+
if (
77+
uniwindCache.virtual[platform] !== null
78+
&& uniwindCache.css === css
79+
&& areSetsEqual(candidatesSet, uniwindCache.candidates)
80+
) {
81+
return uniwindCache.virtual[platform]
82+
}
83+
84+
uniwindCache.css = css
85+
uniwindCache.candidates = new Set(candidates)
86+
87+
const virtualFile = await compileVirtual({
88+
candidates,
89+
css,
90+
cssPath: uniwindOptions.input,
91+
platform,
92+
themes: uniwindOptions.themes,
93+
})
94+
95+
Buffer.from([
96+
virtualFile,
97+
platform !== Platform.Web ? injectThemes(uniwindOptions) : '',
98+
].join(''))
99+
100+
const transform = await worker.transform(
101+
config,
102+
projectRoot,
103+
`${filePath}${platform === Platform.Web ? '' : '.js'}`,
104+
data,
105+
options,
106+
)
107+
;(transform as any).output[0].data.css = {
108+
skipCache: true,
109+
code: '',
110+
}
111+
112+
uniwindCache.virtual[platform] = transform
113+
114+
return transform
115+
}

0 commit comments

Comments
 (0)