-
Notifications
You must be signed in to change notification settings - Fork 54
feat: resolve platform entry files for cssEntryFile #678
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,8 +4,26 @@ import { UniwindCSSVisitor } from '@/bundler/css-visitor' | |
| import type { UniwindConfig, UniwindMetroConfig } from '@/bundler/types' | ||
| import { Platform } from '@/common/consts' | ||
| import { isDefined } from '@/common/utils' | ||
| import fs from 'fs' | ||
| import path from 'path' | ||
|
|
||
| /** | ||
| * Which suffixed entries a platform accepts, most specific first. | ||
| * | ||
| * Mirrors how Metro resolves `.ios` / `.native` modules, including that web never falls back | ||
| * to `.native`. The suffixes are the platform variants Uniwind already generates, so an entry | ||
| * is named after the prefix you would otherwise write inside it. | ||
| */ | ||
| const CSS_ENTRY_PLATFORM_FALLBACKS: Record<Platform, Array<Platform>> = { | ||
| [Platform.Web]: [Platform.Web], | ||
| [Platform.iOS]: [Platform.iOS, Platform.Native], | ||
| [Platform.Android]: [Platform.Android, Platform.Native], | ||
| [Platform.Native]: [Platform.Native], | ||
| [Platform.TV]: [Platform.TV, Platform.Native], | ||
| [Platform.AndroidTV]: [Platform.AndroidTV, Platform.TV, Platform.Android, Platform.Native], | ||
| [Platform.AppleTV]: [Platform.AppleTV, Platform.TV, Platform.iOS, Platform.Native], | ||
| } | ||
|
|
||
| export class UniwindBundlerConfig { | ||
| static fromMetroConfig(config: UniwindMetroConfig, platform?: string | null) { | ||
| const getPlatform = () => { | ||
|
|
@@ -57,8 +75,27 @@ export class UniwindBundlerConfig { | |
|
|
||
| constructor(private readonly config: UniwindMetroConfig, readonly platform: Platform) {} | ||
|
|
||
| /** | ||
| * The stylesheet to compile, honouring a platform file beside the configured entry. | ||
| * | ||
| * `global.web.css` overrules `global.css` on web, `global.native.css` does on both native | ||
| * platforms, and so on. The configured entry stays the fallback and the identity of the | ||
| * module Metro transforms, so nothing else in the pipeline needs to know. | ||
| */ | ||
| get cssPath() { | ||
| return path.join(process.cwd(), this.config.cssEntryFile) | ||
| const entryPath = path.join(process.cwd(), this.config.cssEntryFile) | ||
| const extension = path.extname(entryPath) | ||
| const stem = entryPath.slice(0, entryPath.length - extension.length) | ||
|
|
||
| for (const suffix of CSS_ENTRY_PLATFORM_FALLBACKS[this.platform] ?? []) { | ||
| const platformEntryPath = `${stem}.${suffix}${extension}` | ||
|
|
||
| if (fs.existsSync(platformEntryPath)) { | ||
| return platformEntryPath | ||
|
Comment on lines
+93
to
+94
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When a developer creates, edits, or removes a platform entry such as Knowledge Base Used: Bundler adapters and generated artifacts |
||
| } | ||
| } | ||
|
|
||
| return entryPath | ||
| } | ||
|
|
||
| get themes() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' | ||
| import { tmpdir } from 'node:os' | ||
| import { join, relative } from 'node:path' | ||
| import { UniwindBundlerConfig } from '../../../src/bundler/config' | ||
|
|
||
| /** | ||
| * `cssPath` resolves against `process.cwd()`, so the entry is handed over as a path relative to | ||
| * it rather than chdir-ing the worker. | ||
| */ | ||
| const withEntries = (entries: Array<string>, assert: (cssEntryFile: string) => void) => { | ||
| const root = mkdtempSync(join(tmpdir(), 'uniwind-css-entry-')) | ||
|
|
||
| try { | ||
| entries.forEach(entry => writeFileSync(join(root, entry), '')) | ||
|
|
||
| assert(relative(process.cwd(), join(root, 'global.css'))) | ||
| } finally { | ||
| rmSync(root, { recursive: true, force: true }) | ||
| } | ||
| } | ||
|
|
||
| test('uses the configured entry when no platform file sits beside it', () => { | ||
| withEntries(['global.css'], cssEntryFile => { | ||
| const forWeb = UniwindBundlerConfig.fromMetroConfig({ cssEntryFile }, 'web') | ||
| const forIOS = UniwindBundlerConfig.fromMetroConfig({ cssEntryFile }, 'ios') | ||
|
|
||
| expect(forWeb.cssPath.endsWith('global.css')).toBe(true) | ||
| expect(forIOS.cssPath.endsWith('global.css')).toBe(true) | ||
| }) | ||
| }) | ||
|
|
||
| test('prefers a platform file over the configured entry', () => { | ||
| withEntries(['global.css', 'global.web.css'], cssEntryFile => { | ||
| const forWeb = UniwindBundlerConfig.fromMetroConfig({ cssEntryFile }, 'web') | ||
| const forIOS = UniwindBundlerConfig.fromMetroConfig({ cssEntryFile }, 'ios') | ||
|
|
||
| expect(forWeb.cssPath.endsWith('global.web.css')).toBe(true) | ||
| expect(forIOS.cssPath.endsWith('global.css')).toBe(true) | ||
| }) | ||
| }) | ||
|
|
||
| test('falls back from a platform file to the native one', () => { | ||
| withEntries(['global.css', 'global.native.css'], cssEntryFile => { | ||
| const forIOS = UniwindBundlerConfig.fromMetroConfig({ cssEntryFile }, 'ios') | ||
| const forAndroid = UniwindBundlerConfig.fromMetroConfig({ cssEntryFile }, 'android') | ||
|
|
||
| expect(forIOS.cssPath.endsWith('global.native.css')).toBe(true) | ||
| expect(forAndroid.cssPath.endsWith('global.native.css')).toBe(true) | ||
| }) | ||
| }) | ||
|
|
||
| test('takes the more specific platform file when both exist', () => { | ||
| withEntries(['global.css', 'global.native.css', 'global.ios.css'], cssEntryFile => { | ||
| const forIOS = UniwindBundlerConfig.fromMetroConfig({ cssEntryFile }, 'ios') | ||
| const forAndroid = UniwindBundlerConfig.fromMetroConfig({ cssEntryFile }, 'android') | ||
|
|
||
| expect(forIOS.cssPath.endsWith('global.ios.css')).toBe(true) | ||
| expect(forAndroid.cssPath.endsWith('global.native.css')).toBe(true) | ||
| }) | ||
| }) | ||
|
|
||
| // Metro resolves `.native` for native platforms only, and web is not one of them. | ||
| test('never falls back to the native file on web', () => { | ||
| withEntries(['global.css', 'global.native.css'], cssEntryFile => { | ||
| const forWeb = UniwindBundlerConfig.fromMetroConfig({ cssEntryFile }, 'web') | ||
|
|
||
| expect(forWeb.cssPath.endsWith('global.css')).toBe(true) | ||
| }) | ||
| }) | ||
|
|
||
| test('resolves the TV entries when isTV maps the platform', () => { | ||
| withEntries(['global.css', 'global.native.css', 'global.ios.css', 'global.apple-tv.css'], cssEntryFile => { | ||
| const forAppleTV = UniwindBundlerConfig.fromMetroConfig({ cssEntryFile, isTV: true }, 'ios') | ||
|
|
||
| expect(forAppleTV.cssPath.endsWith('global.apple-tv.css')).toBe(true) | ||
| }) | ||
| }) | ||
|
|
||
| test('an absent platform argument resolves the native entry', () => { | ||
| withEntries(['global.css', 'global.native.css'], cssEntryFile => { | ||
| const forNative = UniwindBundlerConfig.fromMetroConfig({ cssEntryFile }) | ||
|
|
||
| expect(forNative.cssPath.endsWith('global.native.css')).toBe(true) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -59,6 +59,30 @@ module.exports = withUniwindConfig(withOtherConfig(config, opts), { cssEntryFile | |
| module.exports = withOtherConfig(withUniwindConfig(config, { cssEntryFile: './global.css' }), opts); | ||
| ``` | ||
|
|
||
| ### Platform entry files | ||
|
|
||
| A file named after a platform, sitting beside `cssEntryFile`, overrules it when Uniwind compiles for that platform — the same way Metro resolves `.ios` / `.native` modules. Keep pointing `cssEntryFile` at the base entry; the suffixed file is picked up behind it. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This presents platform entry files as a general configuration feature, but only Metro compiles through Knowledge Base Used: |
||
|
|
||
| ``` | ||
| global.css # the configured entry, and the fallback for every platform | ||
| global.web.css # used on web instead | ||
| global.native.css # used on iOS and Android instead | ||
| global.ios.css # used on iOS, in preference to global.native.css | ||
| ``` | ||
|
|
||
| Suffixes are the platform variants Uniwind already generates — `ios`, `android`, `web`, `native`, `tv`, `android-tv`, `apple-tv` — so an entry is named after the prefix you would otherwise write inside it. Resolution is most-specific-first, and web never falls back to `native`. | ||
|
|
||
| Use it when a platform needs stylesheets the others must not get — web-only vendor CSS you override, for instance, which would otherwise be compiled into the native bundle as dead weight: | ||
|
|
||
| ```css | ||
| /* global.web.css */ | ||
| @import 'tailwindcss'; | ||
| @import 'uniwind'; | ||
| @import './vendor-overrides.css'; | ||
| ``` | ||
|
|
||
| Each entry is compiled on its own, so every one of them must carry the full set of bare imports (`tailwindcss`, `uniwind`, …). Put the shared remainder in a file they both `@import`. | ||
|
|
||
| ### Vite Configuration (v1.2.0+) | ||
|
|
||
| If user has storybook setup, add extra vite config: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This adds a public build and runtime contract for platform-specific CSS entries without updating
CONTEXT.md. The repository requiresCONTEXT.mdto be updated whenever public APIs, build/runtime contracts, supported platforms, or architecture change. This requirement must be satisfied before merging.Context Used: CLAUDE.md (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!