Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ Metro integration:

- `withUniwindConfig(config, uniwindConfig)` patches Metro graph support for uncached modules.
- Metro adds `css` as source extension and removes it from asset extensions.
- Metro transformer handles the configured CSS entry file specially. In development, native entries declare imported local CSS files as Metro dependencies, including nested imports and workspace files resolved outside `node_modules`, so token-only edits trigger recompilation. Dependencies are collected afresh on each compile.
- Metro transformer handles the configured CSS entry file specially. In development, native entries declare imported local CSS files as Metro dependencies, including nested imports and workspace files resolved outside `node_modules`, so token-only edits trigger recompilation. The generated `uniwind.css` is never declared (compared by real path): when uniwind is linked, `@import 'uniwind'` resolves to it outside `node_modules`, and every artifact write would rebuild the entry. Dependencies are collected afresh on each compile.
- Non-entry native CSS is an empty module in plain Metro; Expo keeps its own CSS handling. Web CSS handling is unchanged.
- Metro transformer worker selection is lazy, cached per Expo/non-Expo config type, and follows Expo transformer paths or Expo-specific config markers.
- Native platform CSS transforms into a JS module that calls `Uniwind.__reinit(...)` with a fingerprint of the generated styles and themes. During development, the native runtime skips reinitialization when that fingerprint is unchanged.
Expand Down
11 changes: 10 additions & 1 deletion packages/uniwind/src/bundler/adapters/metro/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,22 @@ import { compileCSS } from '@/bundler/css-compiler'
import type { UniwindMetroConfig } from '@/bundler/types'
import { Platform } from '@/common/consts'
import type * as ExpoMetroConfig from '@expo/metro-config'
import fs from 'fs'
import type * as MetroTransformWorker from 'metro-transform-worker'
import type { JsTransformerConfig, JsTransformOptions } from 'metro-transform-worker'
import { createHash } from 'node:crypto'
import path from 'path'

const cssArtifactPath = path.resolve(__dirname, '../../uniwind.css')

// Local stylesheets a development entry requires, so editing one re-runs its transform. When uniwind is linked
// (a workspace package, `npm link`), `@import 'uniwind'` resolves to the generated artifact outside node_modules.
// Requiring it would rebuild the entry after each write, and Metro servers with different themes after each other's.
export const isImportedStylesheet = (stylesheet: string, artifactPath: string) =>
stylesheet.endsWith('.css')
&& !stylesheet.includes(`${path.sep}node_modules${path.sep}`)
&& fs.realpathSync(stylesheet) !== fs.realpathSync(artifactPath)

// Cache workers separately for Expo (`true`) and plain Metro (`false`) configs.
const workerCache = new Map<boolean, typeof MetroTransformWorker>()

Expand Down Expand Up @@ -76,7 +85,7 @@ export const transform = async (
const isWeb = bundlerConfig.platform === Platform.Web
const importedStylesheets = new Set<string>()
const virtualCode = await compileCSS(bundlerConfig, dependency => {
if (!isWeb && options.dev && dependency.endsWith('.css') && !dependency.includes(`${path.sep}node_modules${path.sep}`)) {
if (!isWeb && options.dev && isImportedStylesheet(dependency, cssArtifactPath)) {
importedStylesheets.add(dependency)
}
})
Expand Down
62 changes: 62 additions & 0 deletions packages/uniwind/tests/native/bundler/transformer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join, relative } from 'node:path'
import { isImportedStylesheet } from '../../../src/bundler/adapters/metro/transformer'
import { UniwindBundlerConfig } from '../../../src/bundler/config'
import { compileTailwind } from '../../../src/bundler/css-compiler/compileTailwind'

let root = ''

beforeEach(() => {
root = mkdtempSync(join(tmpdir(), 'uniwind-transformer-'))
})

afterEach(() => {
rmSync(root, { recursive: true, force: true })
})

const writeFiles = (files: Record<string, string>) => {
Object.entries(files).forEach(([file, content]) => {
mkdirSync(dirname(join(root, file)), { recursive: true })
writeFileSync(join(root, file), content)
})
}

// The stylesheets Tailwind reports while compiling the entry, as the transformer receives them.
const compileDependencies = async (cssEntryFile: string) => {
const dependencies: Array<string> = []
const bundlerConfig = UniwindBundlerConfig.fromMetroConfig({ cssEntryFile: relative(process.cwd(), join(root, cssEntryFile)) }, 'ios')

await compileTailwind(bundlerConfig, dependency => dependencies.push(dependency))

return dependencies
}

test.each([
['its real path', () => join(realpathSync(root), 'packages', 'uniwind', 'uniwind.css')],
['the linked path', () => join(root, 'app', 'node_modules', 'uniwind', 'uniwind.css')],
])('skips the generated artifact of a linked uniwind, given %s', async (_, artifactPath) => {
// A workspace package or `npm link`: node_modules/uniwind links to a directory outside node_modules.
writeFiles({
'packages/uniwind/package.json': JSON.stringify({ name: 'uniwind', style: './uniwind.css' }),
'packages/uniwind/uniwind.css': '@theme { --color-artifact: #000000; }',
'packages/brand/package.json': JSON.stringify({ name: 'brand', style: './brand.css' }),
'packages/brand/brand.css': '@theme { --color-brand: #111111; }',
'app/node_modules/theme/package.json': JSON.stringify({ name: 'theme', style: './theme.css' }),
'app/node_modules/theme/theme.css': '@theme { --color-theme: #222222; }',
'app/global.css': ['@import "uniwind";', '@import "brand";', '@import "theme";', '@import "./tokens.css";'].join('\n'),
'app/tokens.css': '@theme { --color-token: #333333; }',
})
symlinkSync(join(root, 'packages', 'uniwind'), join(root, 'app', 'node_modules', 'uniwind'), 'dir')
symlinkSync(join(root, 'packages', 'brand'), join(root, 'app', 'node_modules', 'brand'), 'dir')

const realRoot = realpathSync(root)
const dependencies = await compileDependencies('app/global.css')

// Tailwind reports linked stylesheets by their real paths, outside node_modules.
expect(dependencies).toContain(join(realRoot, 'packages', 'uniwind', 'uniwind.css'))
expect(dependencies.filter(dependency => isImportedStylesheet(dependency, artifactPath())).sort()).toEqual([
join(realRoot, 'app', 'tokens.css'),
join(realRoot, 'packages', 'brand', 'brand.css'),
])
})
Loading