diff --git a/CONTEXT.md b/CONTEXT.md index 6eed4c84..5959a8d4 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -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. diff --git a/packages/uniwind/src/bundler/adapters/metro/transformer.ts b/packages/uniwind/src/bundler/adapters/metro/transformer.ts index c280f4c8..83ed0276 100644 --- a/packages/uniwind/src/bundler/adapters/metro/transformer.ts +++ b/packages/uniwind/src/bundler/adapters/metro/transformer.ts @@ -3,6 +3,7 @@ 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' @@ -10,6 +11,14 @@ 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() @@ -76,7 +85,7 @@ export const transform = async ( const isWeb = bundlerConfig.platform === Platform.Web const importedStylesheets = new Set() 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) } }) diff --git a/packages/uniwind/tests/native/bundler/transformer.test.ts b/packages/uniwind/tests/native/bundler/transformer.test.ts new file mode 100644 index 00000000..6455b4ec --- /dev/null +++ b/packages/uniwind/tests/native/bundler/transformer.test.ts @@ -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) => { + 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 = [] + 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'), + ]) +})