Skip to content

Commit 0ca8b78

Browse files
authored
feat: metro transformer and patch bare metro (#87)
* feat: metro transformer and patch bare metro * chore: always apply patch * chore: fix test
1 parent 957579f commit 0ca8b78

11 files changed

Lines changed: 194 additions & 253 deletions

File tree

packages/uniwind/build.config.ts

Lines changed: 7 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/metro-transformer.ts',
49+
name: 'metro/metro-transformer',
50+
},
4651
{
4752
builder: 'mkdist',
4853
input: './src/metro',
@@ -70,6 +75,8 @@ export default defineBuildConfig({
7075
externals: [
7176
/@tailwindcss/,
7277
'lightningcss',
78+
/metro-cache/,
79+
/metro\/private/,
7380
],
7481
rollup: {
7582
emitCJS: true,

packages/uniwind/specs/utils.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { compileVirtual } from '../src/metro/compileVirtual'
44
import { Platform } from '../src/metro/types'
55
import { UniwindRuntimeMock } from './mocks'
66
import path = require('path')
7+
import type { GenerateStyleSheetsCallback } from '../src/core/types'
78

89
export const getStyleSheetsFromCandidates = async <T extends string>(...candidates: Array<T>) => {
910
const cwd = process.cwd()
@@ -18,9 +19,9 @@ export const getStyleSheetsFromCandidates = async <T extends string>(...candidat
1819
themes: ['light', 'dark'],
1920
})
2021

21-
new Function(virtualJS)()
22+
const { UniwindStore } = await import('../src/core/native')
2223

23-
return globalThis.__uniwind__computeStylesheet(UniwindRuntimeMock)
24+
UniwindStore.reinit(new Function('rt', `return ${virtualJS}`) as GenerateStyleSheetsCallback)
2425
}
2526

2627
export const twSize = (size: number) => size * 4
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { UniwindStore } from '../native'
2+
import { GenerateStyleSheetsCallback } from '../types'
3+
import { Uniwind } from './config'
4+
5+
Object.defineProperty(Uniwind, '__reinit', {
6+
configurable: false,
7+
enumerable: false,
8+
value: (generateStyleSheetCallback: GenerateStyleSheetsCallback) => {
9+
UniwindStore.reinit(generateStyleSheetCallback)
10+
},
11+
})
12+
13+
export * from './config'

packages/uniwind/src/core/config/themeChange.native.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ import { UniwindRuntime } from '../native/runtime'
44

55
export const themeChange = (theme: string) => {
66
UniwindRuntime.currentThemeName = theme
7-
UniwindStore.reload()
7+
UniwindStore.reinit()
88
UniwindStore.notifyListeners([StyleDependency.Theme])
99
}

packages/uniwind/src/core/native/store.ts

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable max-depth */
22
import { Dimensions, Platform } from 'react-native'
33
import { Orientation, StyleDependency } from '../../types'
4-
import { ComponentState, RNStyle, Style, StyleSheets } from '../types'
4+
import { ComponentState, GenerateStyleSheetsCallback, RNStyle, Style, StyleSheets } from '../types'
55
import { parseBoxShadow, parseFontVariant, parseTransformsMutation, resolveGradient } from './parsers'
66
import { UniwindRuntime } from './runtime'
77

@@ -10,7 +10,7 @@ type StylesResult = {
1010
dependencies: Array<StyleDependency>
1111
}
1212

13-
export class UniwindStoreBuilder {
13+
class UniwindStoreBuilder {
1414
stylesheets = {} as StyleSheets
1515
listeners = {
1616
[StyleDependency.ColorScheme]: new Set<() => void>(),
@@ -21,7 +21,6 @@ export class UniwindStoreBuilder {
2121
[StyleDependency.FontScale]: new Set<() => void>(),
2222
[StyleDependency.Rtl]: new Set<() => void>(),
2323
}
24-
initialized = false
2524
runtime = UniwindRuntime
2625
cache = new Map<string, StylesResult>()
2726

@@ -51,11 +50,6 @@ export class UniwindStoreBuilder {
5150
return this.cache.get(cacheKey)!
5251
}
5352

54-
if (!this.initialized) {
55-
this.initialized = true
56-
this.reload()
57-
}
58-
5953
const result = this.resolveStyles(className, state)
6054

6155
this.cache.set(cacheKey, result)
@@ -70,8 +64,8 @@ export class UniwindStoreBuilder {
7064
return result
7165
}
7266

73-
reload = () => {
74-
const styleSheet = globalThis.__uniwind__computeStylesheet(this.runtime)
67+
reinit = (generateStyleSheetCallback?: GenerateStyleSheetsCallback) => {
68+
const styleSheet = generateStyleSheetCallback?.(this.runtime) ?? this.stylesheets
7569
const themeVars = styleSheet[`__uniwind-theme-${this.runtime.currentThemeName}`]
7670
const platformVars = styleSheet[`__uniwind-platform-${Platform.OS}`]
7771

@@ -211,13 +205,6 @@ export class UniwindStoreBuilder {
211205

212206
export const UniwindStore = new UniwindStoreBuilder()
213207

214-
if (__DEV__) {
215-
globalThis.__uniwind__hot_reload = () => {
216-
UniwindStore.reload()
217-
UniwindStore.notifyListeners([StyleDependency.Theme])
218-
}
219-
}
220-
221208
Dimensions.addEventListener('change', ({ window }) => {
222209
const newOrientation = window.width > window.height ? Orientation.Landscape : Orientation.Portrait
223210
const orientationChanged = UniwindStore.runtime.orientation !== newOrientation

packages/uniwind/src/core/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export type Style = {
2222

2323
export type StyleSheets = Record<string, Array<Style> | (() => unknown)>
2424

25+
export type GenerateStyleSheetsCallback = (rt: UniwindRuntime) => StyleSheets
26+
2527
type UserThemes = UniwindConfig extends { themes: infer T extends readonly string[] } ? T
2628
: readonly string[]
2729

@@ -75,8 +77,6 @@ export type UniwindComponentProps =
7577
}
7678

7779
declare global {
78-
var __uniwind__computeStylesheet: (runtime: UniwindRuntime) => StyleSheets
79-
var __uniwind__hot_reload: () => void
8080
var __uniwindThemes__: ReadonlyArray<string> | undefined
8181
}
8282

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { Graph, Result as GraphResult } from '@expo/metro/metro/DeltaBundler/Graph'
2+
import FileStoreBase from 'metro-cache/private/stores/FileStore'
3+
import type { Options as GraphOptions } from 'metro/private/DeltaBundler/types'
4+
import os from 'os'
5+
import path from 'path'
6+
7+
class FileStore<T> extends FileStoreBase<T> {
8+
async set(key: Buffer, value: any): Promise<void> {
9+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
10+
if (value?.output?.[0]?.data?.css?.skipCache) {
11+
return
12+
}
13+
14+
return super.set(key, value)
15+
}
16+
}
17+
18+
export const cacheStore = new FileStore<any>({
19+
root: path.join(os.tmpdir(), 'metro-cache'),
20+
})
21+
22+
interface TraverseDependencies {
23+
(paths: readonly string[], options: GraphOptions<any>): Promise<GraphResult<any>>
24+
__patched?: boolean
25+
}
26+
27+
export const patchMetroGraphToSupportUncachedModules = () => {
28+
const { Graph } = require('metro/private/DeltaBundler/Graph') as typeof import('metro/private/DeltaBundler/Graph')
29+
30+
// eslint-disable-next-line @typescript-eslint/unbound-method
31+
const original_traverseDependencies = Graph.prototype.traverseDependencies as unknown as TraverseDependencies
32+
33+
if (original_traverseDependencies.__patched) {
34+
return
35+
}
36+
37+
original_traverseDependencies.__patched = true
38+
39+
function traverseDependencies(this: Graph, paths: Array<string>, options: GraphOptions<any>) {
40+
this.dependencies.forEach(dependency => {
41+
if (
42+
dependency.output.find(file => file.data.css?.skipCache === true)
43+
&& !paths.includes(dependency.path)
44+
) {
45+
// @ts-expect-error Hidden property
46+
dependency.unstable_transformResultKey = `${dependency.unstable_transformResultKey}.`
47+
paths.push(dependency.path)
48+
}
49+
})
50+
51+
return original_traverseDependencies.call(this, paths, options)
52+
}
53+
54+
// @ts-expect-error patch Graph traverseDependencies method
55+
Graph.prototype.traverseDependencies = traverseDependencies
56+
traverseDependencies.__patched = true
57+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { Scanner } from '@tailwindcss/oxide'
2+
import fs from 'fs'
3+
import { JsTransformerConfig, JsTransformOptions } from 'metro-transform-worker'
4+
import path from 'path'
5+
import { name } from '../../package.json'
6+
import { compileVirtual } from './compileVirtual'
7+
import { getSources } from './getSources'
8+
import { injectThemes } from './injectThemes'
9+
import { Platform, UniwindConfig } from './types'
10+
11+
let worker: typeof import('metro-transform-worker')
12+
13+
try {
14+
worker = require('@expo/metro-config/build/transform-worker/transform-worker.js')
15+
} catch {
16+
worker = require('metro-transform-worker')
17+
}
18+
19+
export const transform = async (
20+
config: JsTransformerConfig & {
21+
uniwind: UniwindConfig
22+
},
23+
projectRoot: string,
24+
filePath: string,
25+
data: Buffer,
26+
options: JsTransformOptions,
27+
) => {
28+
const isCss = options.type !== 'asset' && filePath.endsWith('.css')
29+
30+
if (filePath.endsWith('/components/web/metro-injected.js')) {
31+
const cssPath = path.join(process.cwd(), config.uniwind.cssEntryFile)
32+
const injectedThemesCode = injectThemes({
33+
input: cssPath,
34+
themes: config.uniwind.themes,
35+
dtsPath: config.uniwind.dtsFile,
36+
})
37+
38+
data = Buffer.from(injectedThemesCode)
39+
}
40+
41+
if (!isCss) {
42+
return worker.transform(config, projectRoot, filePath, data, options)
43+
}
44+
45+
const cssPath = path.join(process.cwd(), config.uniwind.cssEntryFile)
46+
const injectedThemesCode = injectThemes({
47+
input: cssPath,
48+
themes: config.uniwind.themes,
49+
dtsPath: config.uniwind.dtsFile,
50+
})
51+
const css = fs.readFileSync(filePath, 'utf-8')
52+
const sources = getSources(css, path.dirname(cssPath))
53+
const candidates = new Scanner({ sources }).scan()
54+
const virtualCode = await compileVirtual({
55+
css,
56+
platform: options.platform as Platform,
57+
themes: config.uniwind.themes,
58+
polyfills: config.uniwind.polyfills,
59+
candidates,
60+
cssPath,
61+
})
62+
const isWeb = options.platform === Platform.Web
63+
64+
data = Buffer.from(
65+
isWeb
66+
? virtualCode
67+
: [
68+
`import { Uniwind } from '${name}';`,
69+
`Uniwind.__reinit(rt => ${virtualCode});`,
70+
injectedThemesCode,
71+
].join(''),
72+
'utf-8',
73+
)
74+
75+
const transform: any = await worker.transform(
76+
config,
77+
projectRoot,
78+
`${filePath}${isWeb ? '' : '.js'}`,
79+
data,
80+
options,
81+
)
82+
83+
transform.output[0].data.css = {
84+
skipCache: true,
85+
code: '',
86+
}
87+
88+
return transform
89+
}

packages/uniwind/src/metro/stylesheet/serializeStylesheet.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,6 @@ const serialize = (value: any): string => {
170170
}
171171

172172
export const serializeStylesheet = (stylesheet: Stylesheet) => {
173-
const hotReloadFN = 'globalThis.__uniwind__hot_reload?.();'
174173
const currentColor = `get currentColor() { return function() { return rt.colorScheme === 'dark' ? '#ffffff' : '#000000' } },`
175174

176175
const serializedStylesheet = Object.entries(stylesheet).map(([key, value]) => {
@@ -181,11 +180,11 @@ export const serializeStylesheet = (stylesheet: Stylesheet) => {
181180
return `"${key}": ${stringifiedValue}`
182181
}).join(',\n')
183182

184-
const js = `globalThis.__uniwind__computeStylesheet = rt => ({ ${currentColor} ${serializedStylesheet} });${hotReloadFN}`
183+
const js = `({ ${currentColor} ${serializedStylesheet} })`
185184

186185
try {
187186
// eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
188-
new Function(`function validateJS() { ${js} }`)
187+
new Function(`function validateJS() { const fn = rt => ${js} }`)
189188
} catch {
190189
Logger.error('Failed to create virtual js')
191190
return ''

packages/uniwind/src/metro/types.ts

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,8 @@ import type {
1010
Token,
1111
TokenOrValue,
1212
} from 'lightningcss'
13-
import type Bundler from 'metro/private/Bundler'
1413
import { ColorScheme, Orientation } from '../types'
1514

16-
type WithUniwindPatch<T> = T & {
17-
__uniwind_patched?: boolean
18-
}
19-
20-
export type ExtendedBundler = Bundler & {
21-
transformFile: WithUniwindPatch<Bundler['transformFile']>
22-
}
23-
24-
export type ExtendedFileSystem = {
25-
getSha1: WithUniwindPatch<(filename: string) => string>
26-
}
27-
2815
export type Polyfills = {
2916
rem?: number
3017
}
@@ -83,13 +70,3 @@ export type ProcessMetaValues = {
8370
export type StyleSheetTemplate = {
8471
[K: string]: Array<MediaQueryResolver & Record<string, unknown>>
8572
}
86-
87-
type FileChange = {
88-
filePath: string
89-
type: string
90-
metadata: any
91-
}
92-
93-
export type FileChangeEvent = {
94-
eventsQueue: Array<FileChange>
95-
}

0 commit comments

Comments
 (0)