@tamagui/tailwind 0.0.0-bootstrap.0 → 3.0.0-beta.1093.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/dist/cjs/candidate.cjs +540 -0
  2. package/dist/cjs/candidate.native.cjs +578 -0
  3. package/dist/cjs/candidate.native.js +580 -0
  4. package/dist/cjs/candidate.native.js.map +1 -0
  5. package/dist/cjs/compose.cjs +352 -0
  6. package/dist/cjs/compose.native.cjs +358 -0
  7. package/dist/cjs/compose.native.js +360 -0
  8. package/dist/cjs/compose.native.js.map +1 -0
  9. package/dist/cjs/composedResolver.cjs +175 -0
  10. package/dist/cjs/composedResolver.native.cjs +201 -0
  11. package/dist/cjs/composedResolver.native.js +203 -0
  12. package/dist/cjs/composedResolver.native.js.map +1 -0
  13. package/dist/cjs/frontend.cjs +54 -0
  14. package/dist/cjs/frontend.native.cjs +62 -0
  15. package/dist/cjs/frontend.native.js +64 -0
  16. package/dist/cjs/frontend.native.js.map +1 -0
  17. package/dist/cjs/index.cjs +38 -0
  18. package/dist/cjs/index.native.cjs +40 -0
  19. package/dist/cjs/index.native.js +42 -0
  20. package/dist/cjs/index.native.js.map +1 -0
  21. package/dist/cjs/styled.cjs +29 -0
  22. package/dist/cjs/styled.native.cjs +31 -0
  23. package/dist/cjs/styled.native.js +33 -0
  24. package/dist/cjs/styled.native.js.map +1 -0
  25. package/dist/cjs/types.cjs +16 -0
  26. package/dist/cjs/types.native.cjs +18 -0
  27. package/dist/cjs/types.native.js +20 -0
  28. package/dist/cjs/types.native.js.map +1 -0
  29. package/dist/cjs/vite/state.cjs +249 -0
  30. package/dist/cjs/vite/state.native.cjs +331 -0
  31. package/dist/cjs/vite/state.native.js +333 -0
  32. package/dist/cjs/vite/state.native.js.map +1 -0
  33. package/dist/cjs/vite.cjs +147 -0
  34. package/dist/cjs/vite.native.cjs +187 -0
  35. package/dist/cjs/vite.native.js +189 -0
  36. package/dist/cjs/vite.native.js.map +1 -0
  37. package/dist/esm/candidate.mjs +514 -0
  38. package/dist/esm/candidate.mjs.map +1 -0
  39. package/dist/esm/candidate.native.js +550 -0
  40. package/dist/esm/candidate.native.js.map +1 -0
  41. package/dist/esm/compose.mjs +328 -0
  42. package/dist/esm/compose.mjs.map +1 -0
  43. package/dist/esm/compose.native.js +332 -0
  44. package/dist/esm/compose.native.js.map +1 -0
  45. package/dist/esm/composedResolver.mjs +155 -0
  46. package/dist/esm/composedResolver.mjs.map +1 -0
  47. package/dist/esm/composedResolver.native.js +179 -0
  48. package/dist/esm/composedResolver.native.js.map +1 -0
  49. package/dist/esm/frontend.mjs +32 -0
  50. package/dist/esm/frontend.mjs.map +1 -0
  51. package/dist/esm/frontend.native.js +38 -0
  52. package/dist/esm/frontend.native.js.map +1 -0
  53. package/dist/esm/index.mjs +11 -0
  54. package/dist/esm/index.mjs.map +1 -0
  55. package/dist/esm/index.native.js +11 -0
  56. package/dist/esm/index.native.js.map +1 -0
  57. package/dist/esm/styled.mjs +10 -0
  58. package/dist/esm/styled.mjs.map +1 -0
  59. package/dist/esm/styled.native.js +10 -0
  60. package/dist/esm/styled.native.js.map +1 -0
  61. package/dist/esm/types.mjs +0 -0
  62. package/dist/esm/types.native.js +0 -0
  63. package/dist/esm/vite/state.mjs +216 -0
  64. package/dist/esm/vite/state.mjs.map +1 -0
  65. package/dist/esm/vite/state.native.js +296 -0
  66. package/dist/esm/vite/state.native.js.map +1 -0
  67. package/dist/esm/vite.mjs +116 -0
  68. package/dist/esm/vite.mjs.map +1 -0
  69. package/dist/esm/vite.native.js +154 -0
  70. package/dist/esm/vite.native.js.map +1 -0
  71. package/package.json +90 -5
  72. package/src/candidate.ts +805 -0
  73. package/src/frontend.ts +73 -0
  74. package/src/index.tsx +35 -0
  75. package/src/styled.tsx +52 -0
  76. package/src/types.ts +249 -0
  77. package/src/vite/state.ts +274 -0
  78. package/src/vite.ts +204 -0
  79. package/types/candidate.d.ts +18 -0
  80. package/types/candidate.d.ts.map +1 -0
  81. package/types/compose.d.ts +19 -0
  82. package/types/compose.d.ts.map +1 -0
  83. package/types/composedResolver.d.ts +18 -0
  84. package/types/composedResolver.d.ts.map +1 -0
  85. package/types/frontend.d.ts +19 -0
  86. package/types/frontend.d.ts.map +1 -0
  87. package/types/index.d.ts +17 -0
  88. package/types/index.d.ts.map +1 -0
  89. package/types/styled.d.ts +12 -0
  90. package/types/styled.d.ts.map +1 -0
  91. package/types/types.d.ts +169 -0
  92. package/types/types.d.ts.map +1 -0
  93. package/types/vite/state.d.ts +28 -0
  94. package/types/vite/state.d.ts.map +1 -0
  95. package/types/vite.d.ts +20 -0
  96. package/types/vite.d.ts.map +1 -0
  97. package/README.md +0 -1
@@ -0,0 +1,73 @@
1
+ import {
2
+ type FrontendStaticConfig,
3
+ type FrontendStaticConfigNormalization,
4
+ type StyleFrontend,
5
+ type StyleFrontendConfig,
6
+ } from '@tamagui/core/internal-runtime'
7
+ import { resolveTailwindCandidate, resolveTailwindClassName } from './candidate'
8
+ import { composedResolver } from './composedResolver'
9
+
10
+ /**
11
+ * The Tailwind frontend descriptor.
12
+ *
13
+ * The shared style cursor tokenizes `className` once and asks this descriptor to
14
+ * emit the entries for each candidate. Everything after this point — value programs,
15
+ * per-longhand forward merging, web lowering, native evaluation — is shared.
16
+ *
17
+ * Owned candidates are never string-merged: each contributes at its authored
18
+ * position and the shared resolver's last-contribution-wins rule decides. That is
19
+ * why `tailwind-merge` is not a dependency here.
20
+ */
21
+
22
+ /**
23
+ * Parse a static class string (a `styled()` base, a string variant value) into
24
+ * ordinary style props.
25
+ */
26
+ export function parseStaticStyle(
27
+ input: string,
28
+ config: StyleFrontendConfig
29
+ ): Record<string, any> {
30
+ return resolveTailwindClassName(input, config)
31
+ }
32
+
33
+ function normalizeTailwindStaticConfig(
34
+ staticConfig: FrontendStaticConfig,
35
+ config: StyleFrontendConfig
36
+ ): FrontendStaticConfigNormalization {
37
+ let variants = staticConfig.variants
38
+ if (variants) {
39
+ variants = Object.fromEntries(
40
+ Object.entries(variants).map(([variantName, definition]) => [
41
+ variantName,
42
+ typeof definition === 'object' && definition
43
+ ? Object.fromEntries(
44
+ Object.entries(definition).map(([matcher, value]) => [
45
+ matcher,
46
+ typeof value === 'string' ? parseStaticStyle(value, config) : value,
47
+ ])
48
+ )
49
+ : definition,
50
+ ])
51
+ )
52
+ }
53
+
54
+ // A class base is the one class string with no authored position, so its unclaimed
55
+ // classes cannot ride the forward pass the way a call-site className does. Partition
56
+ // them out: `baseStyle` holds styles only, and the raw remainder goes to
57
+ // `passthroughClassName` for the renderer to prepend.
58
+ let baseStyle = staticConfig.baseStyle
59
+ let passthroughClassName = staticConfig.passthroughClassName
60
+ if (staticConfig.baseClassName) {
61
+ const { className, ...styles } = parseStaticStyle(staticConfig.baseClassName, config)
62
+ baseStyle = styles
63
+ passthroughClassName = className
64
+ }
65
+
66
+ return { baseStyle, passthroughClassName, variants }
67
+ }
68
+
69
+ export const tailwindStyleFrontend: StyleFrontend = {
70
+ resolveClassName: resolveTailwindCandidate,
71
+ normalizeStaticConfig: normalizeTailwindStaticConfig,
72
+ compose: composedResolver,
73
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Tailwind authoring for Tamagui.
3
+ *
4
+ * Components here render through the same runtime as `@tamagui/core` — same
5
+ * config, tokens, themes, media, events, refs, accessibility, animations, and
6
+ * normalized style output. Only the authoring syntax differs, and it is selected
7
+ * by which package a component was imported from, never by a global setting.
8
+ *
9
+ * This entry is runtime-only. The official Tailwind scanner and build integration
10
+ * live behind `@tamagui/tailwind/vite` and are never reachable from here.
11
+ */
12
+ import { createFrontendHTML, createFrontendViews } from '@tamagui/core/internal-runtime'
13
+ import { tailwindStyleFrontend } from './frontend'
14
+ import type { TailwindHTML, TailwindText, TailwindView } from './types'
15
+
16
+ const frontendViews = createFrontendViews(tailwindStyleFrontend)
17
+
18
+ /**
19
+ * The tailwind View and Text. Composed utilities (ring, gradient, filter,
20
+ * shadow, transform, text-shadow) are folded by the descriptor's `compose` hook
21
+ * during the className walk, so these carry no resolver of their own.
22
+ */
23
+ export const View = frontendViews.View as unknown as TailwindView
24
+
25
+ export const Text = frontendViews.Text as unknown as TailwindText
26
+
27
+ /**
28
+ * The semantic elements. Identical to `html` from `tamagui` down to the element
29
+ * defaults, differing only in that a class string is the styling input.
30
+ */
31
+ export const html = createFrontendHTML(tailwindStyleFrontend) as unknown as TailwindHTML
32
+
33
+ export { styled } from './styled'
34
+ export { parseStaticStyle, tailwindStyleFrontend } from './frontend'
35
+ export type * from './types'
package/src/styled.tsx ADDED
@@ -0,0 +1,52 @@
1
+ import { createFrontendStyled } from '@tamagui/core/internal-runtime'
2
+ import { tailwindStyleFrontend } from './frontend'
3
+ import type {
4
+ GetTailwindNonStyleProps,
5
+ GetTailwindRef,
6
+ GetTailwindVariantProps,
7
+ TailwindComponent,
8
+ TailwindStyledOptions,
9
+ TailwindVariantDefinitions,
10
+ TailwindVariantProps,
11
+ } from './types'
12
+
13
+ const styledWithFrontend = createFrontendStyled(tailwindStyleFrontend)
14
+
15
+ /**
16
+ * Class-first `styled()`. The base is a class string, variant values are class
17
+ * strings, and the resulting component accepts `className` plus its variants —
18
+ * never Tamagui inline style props.
19
+ *
20
+ * The class base and variant values are parsed once per (component, config) pair
21
+ * by the frontend's `normalizeStaticConfig`, so authoring cost is not per render.
22
+ */
23
+ export function styled<
24
+ Parent extends TailwindComponent<any, any, any>,
25
+ Variants extends TailwindVariantDefinitions = {},
26
+ >(
27
+ Component: Parent,
28
+ baseClassName: string,
29
+ options?: TailwindStyledOptions<Variants>
30
+ ): TailwindComponent<
31
+ GetTailwindRef<Parent>,
32
+ GetTailwindNonStyleProps<Parent>,
33
+ GetTailwindVariantProps<Parent> & TailwindVariantProps<Variants>
34
+ >
35
+ export function styled<
36
+ Parent extends TailwindComponent<any, any, any>,
37
+ Variants extends TailwindVariantDefinitions = {},
38
+ >(
39
+ Component: Parent,
40
+ options?: TailwindStyledOptions<Variants>
41
+ ): TailwindComponent<
42
+ GetTailwindRef<Parent>,
43
+ GetTailwindNonStyleProps<Parent>,
44
+ GetTailwindVariantProps<Parent> & TailwindVariantProps<Variants>
45
+ >
46
+ export function styled(
47
+ Component: any,
48
+ optionsOrBaseClassName?: any,
49
+ maybeOptions?: any
50
+ ): any {
51
+ return styledWithFrontend(Component, optionsOrBaseClassName, maybeOptions)
52
+ }
package/src/types.ts ADDED
@@ -0,0 +1,249 @@
1
+ import type {
2
+ FrontendHTMLTag,
3
+ FrontendStaticConfig,
4
+ } from '@tamagui/core/internal-runtime'
5
+ import type {
6
+ AriaAttributes,
7
+ ComponentPropsWithoutRef,
8
+ CSSProperties,
9
+ FunctionComponent,
10
+ HTMLAttributes,
11
+ ReactElement,
12
+ ReactNode,
13
+ Ref as ReactRef,
14
+ } from 'react'
15
+ import type {
16
+ PressableProps,
17
+ StyleProp,
18
+ Text as ReactNativeText,
19
+ TextProps as ReactNativeTextProps,
20
+ TextStyle,
21
+ View as ReactNativeView,
22
+ ViewProps as ReactNativeViewProps,
23
+ ViewStyle,
24
+ } from '@tamagui/react-native-types'
25
+
26
+ type WebEventProps = {
27
+ onMouseEnter?: HTMLAttributes<HTMLDivElement>['onMouseEnter']
28
+ onMouseLeave?: HTMLAttributes<HTMLDivElement>['onMouseLeave']
29
+ onMouseDown?: HTMLAttributes<HTMLDivElement>['onMouseDown']
30
+ onMouseUp?: HTMLAttributes<HTMLDivElement>['onMouseUp']
31
+ onMouseMove?: HTMLAttributes<HTMLDivElement>['onMouseMove']
32
+ onMouseOver?: HTMLAttributes<HTMLDivElement>['onMouseOver']
33
+ onMouseOut?: HTMLAttributes<HTMLDivElement>['onMouseOut']
34
+ onFocus?: HTMLAttributes<HTMLDivElement>['onFocus']
35
+ onBlur?: HTMLAttributes<HTMLDivElement>['onBlur']
36
+ onClick?: HTMLAttributes<HTMLDivElement>['onClick']
37
+ onDoubleClick?: HTMLAttributes<HTMLDivElement>['onDoubleClick']
38
+ onContextMenu?: HTMLAttributes<HTMLDivElement>['onContextMenu']
39
+ onWheel?: HTMLAttributes<HTMLDivElement>['onWheel']
40
+ onKeyDown?: HTMLAttributes<HTMLDivElement>['onKeyDown']
41
+ onKeyUp?: HTMLAttributes<HTMLDivElement>['onKeyUp']
42
+ onChange?: HTMLAttributes<HTMLDivElement>['onChange']
43
+ onInput?: HTMLAttributes<HTMLDivElement>['onInput']
44
+ onBeforeInput?: HTMLAttributes<HTMLDivElement>['onBeforeInput']
45
+ onScroll?: HTMLAttributes<HTMLDivElement>['onScroll']
46
+ onCopy?: HTMLAttributes<HTMLDivElement>['onCopy']
47
+ onCut?: HTMLAttributes<HTMLDivElement>['onCut']
48
+ onPaste?: HTMLAttributes<HTMLDivElement>['onPaste']
49
+ onDrag?: HTMLAttributes<HTMLDivElement>['onDrag']
50
+ onDragStart?: HTMLAttributes<HTMLDivElement>['onDragStart']
51
+ onDragEnd?: HTMLAttributes<HTMLDivElement>['onDragEnd']
52
+ onDragEnter?: HTMLAttributes<HTMLDivElement>['onDragEnter']
53
+ onDragLeave?: HTMLAttributes<HTMLDivElement>['onDragLeave']
54
+ onDragOver?: HTMLAttributes<HTMLDivElement>['onDragOver']
55
+ onDrop?: HTMLAttributes<HTMLDivElement>['onDrop']
56
+ onPointerDown?: HTMLAttributes<HTMLDivElement>['onPointerDown']
57
+ onPointerMove?: HTMLAttributes<HTMLDivElement>['onPointerMove']
58
+ onPointerUp?: HTMLAttributes<HTMLDivElement>['onPointerUp']
59
+ onPointerCancel?: HTMLAttributes<HTMLDivElement>['onPointerCancel']
60
+ }
61
+
62
+ type TailwindComponentState = {
63
+ unmounted: boolean | 'should-enter'
64
+ disabled?: boolean
65
+ hover?: boolean
66
+ press?: boolean
67
+ pressIn?: boolean
68
+ focus?: boolean
69
+ focusVisible?: boolean
70
+ focusWithin?: boolean
71
+ }
72
+
73
+ type TailwindCommonProps = AriaAttributes &
74
+ WebEventProps & {
75
+ target?: string
76
+ htmlFor?: string
77
+ asChild?: boolean | 'except-style' | 'except-style-web' | 'web'
78
+ dangerouslySetInnerHTML?: { __html: string }
79
+ children?: ReactNode
80
+ debug?: boolean | 'break' | 'verbose' | 'visualize' | 'profile'
81
+ disabled?: boolean
82
+ themeShallow?: boolean
83
+ id?: string
84
+ render?:
85
+ | keyof HTMLElementTagNameMap
86
+ | (string & {})
87
+ | ReactElement
88
+ | ((
89
+ props: Record<string, any> & { ref?: ReactRef<any> },
90
+ state: TailwindComponentState
91
+ ) => ReactElement)
92
+ theme?: string | null
93
+ group?: string | boolean
94
+ container?: boolean | string
95
+ untilMeasured?: 'hide' | 'show'
96
+ name?: string
97
+ tabIndex?: string | number
98
+ role?: HTMLAttributes<HTMLDivElement>['role']
99
+ disableOptimization?: boolean
100
+ forceStyle?: 'hover' | 'press' | 'focus' | 'focusVisible' | 'focusWithin'
101
+ disableClassName?: boolean
102
+ hitSlop?:
103
+ | number
104
+ | { top?: number; left?: number; bottom?: number; right?: number }
105
+ | null
106
+ animatedBy?: string | null
107
+ onPress?: PressableProps['onPress']
108
+ onLongPress?: PressableProps['onLongPress']
109
+ onPressIn?: PressableProps['onPress']
110
+ onPressOut?: PressableProps['onPress']
111
+ }
112
+
113
+ type LooseCombinedObjects<A extends object, B extends object> = A | B | (A & B)
114
+
115
+ export type TailwindViewNonStyleProps = Omit<
116
+ ReactNativeViewProps,
117
+ 'children' | 'display' | 'hitSlop' | 'pointerEvents' | 'style' | keyof WebEventProps
118
+ > &
119
+ TailwindCommonProps & {
120
+ style?: StyleProp<LooseCombinedObjects<CSSProperties, ViewStyle>>
121
+ }
122
+
123
+ export type TailwindTextNonStyleProps = Omit<
124
+ ReactNativeTextProps,
125
+ 'children' | 'style' | keyof WebEventProps
126
+ > &
127
+ TailwindCommonProps & {
128
+ style?: StyleProp<LooseCombinedObjects<CSSProperties, TextStyle>>
129
+ }
130
+
131
+ type MeasureOnSuccessCallback = (
132
+ x: number,
133
+ y: number,
134
+ width: number,
135
+ height: number,
136
+ pageX: number,
137
+ pageY: number
138
+ ) => void
139
+
140
+ export interface TailwindElementMethods {
141
+ measure(callback: MeasureOnSuccessCallback): void
142
+ measureInWindow(
143
+ callback: (x: number, y: number, width: number, height: number) => void
144
+ ): void
145
+ measureLayout(
146
+ relativeToNativeNode: ReactNativeView | HTMLElement,
147
+ onSuccess: (left: number, top: number, width: number, height: number) => void,
148
+ onFail?: () => void
149
+ ): void
150
+ focus(): void
151
+ blur(): void
152
+ }
153
+
154
+ export type TailwindViewElement = (HTMLElement & TailwindElementMethods) | ReactNativeView
155
+ export type TailwindTextElement = (HTMLElement & TailwindElementMethods) | ReactNativeText
156
+
157
+ /**
158
+ * The complete styling surface of a `@tamagui/tailwind` component.
159
+ *
160
+ * Everything else a component accepts is ordinary behavior: children, refs,
161
+ * accessibility, events, ids, and the raw platform `style` escape hatch, all
162
+ * inherited from the shared non-style prop types. Tamagui inline style props
163
+ * (`padding`, `bg`, shorthands, state/media clauses, theme style props) are
164
+ * deliberately absent — that authoring syntax belongs to `@tamagui/core`.
165
+ */
166
+ export type TailwindStyleProps = {
167
+ className?: string
168
+ }
169
+
170
+ /** `true`/`false` matchers become a boolean prop; every other matcher is its own literal. */
171
+ type VariantAcceptedValue<Key> = Key extends 'true' | 'false' ? boolean : Key
172
+
173
+ /**
174
+ * Finite variant inference: one optional prop per variant, accepting exactly the
175
+ * matchers that variant declares. It never reaches core's style prop graph.
176
+ */
177
+ export type TailwindVariantProps<Variants> = Variants extends object
178
+ ? {
179
+ [Key in keyof Variants]?: VariantAcceptedValue<keyof Variants[Key]>
180
+ }
181
+ : {}
182
+
183
+ /** Variant values are class strings — the same authoring syntax as `className`. */
184
+ export type TailwindVariantDefinitions = {
185
+ [variantName: string]: { [matcher: string]: string }
186
+ }
187
+
188
+ export type TailwindStyledOptions<Variants extends TailwindVariantDefinitions> = {
189
+ displayName?: string
190
+ variants?: Variants
191
+ defaultVariants?: TailwindVariantProps<Variants>
192
+ }
193
+
194
+ export interface TailwindComponent<
195
+ Ref = any,
196
+ NonStyleProps = {},
197
+ VariantProps = {},
198
+ > extends FunctionComponent<
199
+ NonStyleProps & TailwindStyleProps & VariantProps & { ref?: ReactRef<Ref> }
200
+ > {
201
+ staticConfig: FrontendStaticConfig
202
+ /** phantom carrier so `styled()` can recover the parts of a parent component */
203
+ __tailwind?: { ref: Ref; props: NonStyleProps; variants: VariantProps }
204
+ }
205
+
206
+ export type GetTailwindRef<Component> =
207
+ Component extends TailwindComponent<infer Ref, any, any> ? Ref : any
208
+
209
+ export type GetTailwindNonStyleProps<Component> =
210
+ Component extends TailwindComponent<any, infer Props, any> ? Props : {}
211
+
212
+ export type GetTailwindVariantProps<Component> =
213
+ Component extends TailwindComponent<any, any, infer Variants> ? Variants : {}
214
+
215
+ export type TailwindView = TailwindComponent<
216
+ TailwindViewElement,
217
+ TailwindViewNonStyleProps
218
+ >
219
+ export type TailwindText = TailwindComponent<
220
+ TailwindTextElement,
221
+ TailwindTextNonStyleProps
222
+ >
223
+
224
+ export type TailwindViewProps = TailwindViewNonStyleProps & TailwindStyleProps
225
+ export type TailwindTextProps = TailwindTextNonStyleProps & TailwindStyleProps
226
+
227
+ /**
228
+ * One tag's props: the element's own DOM contract plus the shared Tamagui
229
+ * behavior props, with `className` the only styling input. Tamagui non-style
230
+ * props win where both name the same thing, matching the regular `html`.
231
+ */
232
+ type TailwindHTMLProps<Tag extends FrontendHTMLTag> = Omit<
233
+ ComponentPropsWithoutRef<Tag>,
234
+ 'className' | 'style' | keyof TailwindCommonProps
235
+ > &
236
+ TailwindCommonProps & {
237
+ style?: StyleProp<LooseCombinedObjects<CSSProperties, ViewStyle>>
238
+ }
239
+
240
+ /**
241
+ * The semantic elements, authored in Tailwind classes. Same tags and same
242
+ * element defaults as `html` from `tamagui`; only the styling syntax differs.
243
+ */
244
+ export type TailwindHTML = {
245
+ [Tag in FrontendHTMLTag]: TailwindComponent<
246
+ HTMLElementTagNameMap[Tag],
247
+ TailwindHTMLProps<Tag>
248
+ >
249
+ }
@@ -0,0 +1,274 @@
1
+ import { compile } from '@tailwindcss/node'
2
+ import { Scanner } from '@tailwindcss/oxide'
3
+ import {
4
+ createGrammarConfigView,
5
+ type GrammarSourceConfig,
6
+ } from '@tamagui/style-grammar/runtime'
7
+ import { classifyCandidate } from '@tamagui/style-grammar/tooling'
8
+ import { readFile, realpath } from 'node:fs/promises'
9
+ import { createRequire } from 'node:module'
10
+ import path from 'node:path'
11
+
12
+ export const TAILWIND_VERSION = '4.3.0'
13
+ export const TAILWIND_VIRTUAL_ID = 'virtual:tamagui-tailwind.css'
14
+ export const TAILWIND_RESOLVED_ID = `\0${TAILWIND_VIRTUAL_ID}`
15
+
16
+ export function wrapWithTamaguiLayer(css: string): string {
17
+ return `@layer tamagui {\n${css}\n}`
18
+ }
19
+
20
+ export function isTamaguiCoreResetCSS(id: string): boolean {
21
+ const normalizedId = id.split('?', 1)[0].replace(/\\/g, '/')
22
+ return (
23
+ normalizedId.endsWith('/@tamagui/core/reset.css') ||
24
+ normalizedId.endsWith('/code/core/core/reset.css')
25
+ )
26
+ }
27
+
28
+ const TAILWIND_INPUT = `@layer tamagui, theme, utilities;
29
+ @import "tailwindcss/theme.css" layer(theme);
30
+ @import "tailwindcss/utilities.css" layer(utilities);`
31
+
32
+ type TailwindCompiler = Awaited<ReturnType<typeof compile>>
33
+
34
+ export type TailwindScannerState = ReturnType<typeof createTailwindScannerState>
35
+
36
+ export type TailwindWatchEvent = 'create' | 'update' | 'delete'
37
+
38
+ function extensionForFile(id: string): string {
39
+ return path.extname(id.split('?', 1)[0]).slice(1) || 'html'
40
+ }
41
+
42
+ async function assertHostTailwind(root: string): Promise<void> {
43
+ const requireFromRoot = createRequire(path.join(root, 'package.json'))
44
+ let packagePath: string
45
+ try {
46
+ packagePath = requireFromRoot.resolve('tailwindcss/package.json')
47
+ } catch {
48
+ throw new Error(
49
+ `[tamagui] @tamagui/tailwind/vite requires tailwindcss@${TAILWIND_VERSION} in the app. Install it with \`bun add -D tailwindcss@${TAILWIND_VERSION}\` (resolved from ${root}).`
50
+ )
51
+ }
52
+
53
+ const packageJSON = JSON.parse(await readFile(packagePath, 'utf8')) as {
54
+ version?: string
55
+ }
56
+ if (packageJSON.version !== TAILWIND_VERSION) {
57
+ throw new Error(
58
+ `[tamagui] @tamagui/tailwind/vite requires tailwindcss@${TAILWIND_VERSION}, but ${packageJSON.version || 'an unknown version'} was resolved from ${packagePath}. Align the app dependency before starting Vite.`
59
+ )
60
+ }
61
+ }
62
+
63
+ /**
64
+ * The official Tailwind scanner and compiler, owned by `@tamagui/tailwind/vite`.
65
+ *
66
+ * It compiles only the candidates Tamagui's own grammar does not claim: claimed
67
+ * candidates already became Tamagui atoms through the shared renderer, so emitting
68
+ * them again would duplicate the rules and hand the cascade to whichever landed
69
+ * last. There is no style-mode gate here — the state exists because the app
70
+ * imported the Tailwind plugin.
71
+ */
72
+ export function createTailwindScannerState() {
73
+ let root = ''
74
+ let generation = -1
75
+ let enabled = false
76
+ let scanner: Scanner | null = null
77
+ let compiler: TailwindCompiler | null = null
78
+ let grammarConfig = createGrammarConfigView({})
79
+ let candidatesByFile = new Map<string, Set<string>>()
80
+ let compiledCandidates = new Set<string>()
81
+ let addDependency: (file: string) => void = () => {}
82
+ let css = ''
83
+
84
+ function clear() {
85
+ root = ''
86
+ generation = -1
87
+ enabled = false
88
+ scanner = null
89
+ compiler = null
90
+ grammarConfig = createGrammarConfigView({})
91
+ candidatesByFile = new Map()
92
+ compiledCandidates = new Set()
93
+ addDependency = () => {}
94
+ css = ''
95
+ }
96
+
97
+ async function configure(
98
+ nextRoot: string,
99
+ nextGeneration: number,
100
+ config: GrammarSourceConfig | null | undefined,
101
+ onDependency: (file: string) => void,
102
+ onSourceGlob: (glob: string) => void = () => {}
103
+ ): Promise<boolean> {
104
+ // a null config means Tamagui itself is disabled for this build, and without a
105
+ // config there is no grammar view to decide which candidates Tamagui owns
106
+ if (!config) {
107
+ clear()
108
+ generation = nextGeneration
109
+ return false
110
+ }
111
+ if (
112
+ compiler &&
113
+ scanner &&
114
+ enabled &&
115
+ generation === nextGeneration &&
116
+ root === nextRoot
117
+ ) {
118
+ return true
119
+ }
120
+
121
+ await assertHostTailwind(nextRoot)
122
+ root = nextRoot
123
+ generation = nextGeneration
124
+ enabled = true
125
+ grammarConfig = createGrammarConfigView(config)
126
+ addDependency = onDependency
127
+ scanner = new Scanner({
128
+ sources: [
129
+ { base: root, pattern: '**/*', negated: false },
130
+ { base: root, pattern: 'node_modules/**', negated: true },
131
+ { base: root, pattern: '.git/**', negated: true },
132
+ { base: root, pattern: '.vite/**', negated: true },
133
+ { base: root, pattern: 'dist/**', negated: true },
134
+ { base: root, pattern: 'build/**', negated: true },
135
+ { base: root, pattern: 'coverage/**', negated: true },
136
+ ],
137
+ })
138
+ compiler = await createCompiler()
139
+ scanner.scan()
140
+ registerScannerSources(scanner, onDependency, onSourceGlob)
141
+ candidatesByFile = new Map()
142
+ await Promise.all(
143
+ scanner.files.map(async (file) => {
144
+ try {
145
+ const source = await readFile(file, 'utf8')
146
+ candidatesByFile.set(
147
+ await normalizeSourcePath(file),
148
+ candidatesForSource(scanner!, file, source)
149
+ )
150
+ } catch {
151
+ // A source may disappear between oxide's inventory and this read.
152
+ }
153
+ })
154
+ )
155
+ await rebuild()
156
+ return true
157
+ }
158
+
159
+ async function createCompiler() {
160
+ return compile(TAILWIND_INPUT, {
161
+ base: root,
162
+ onDependency: addDependency,
163
+ })
164
+ }
165
+
166
+ async function rebuild(): Promise<boolean> {
167
+ if (!compiler) return false
168
+ const allCandidates = new Set<string>()
169
+ for (const candidates of candidatesByFile.values()) {
170
+ for (const candidate of candidates) allCandidates.add(candidate)
171
+ }
172
+ const passthrough = [...allCandidates]
173
+ .filter(
174
+ (candidate) => classifyCandidate(candidate, grammarConfig).kind === 'passthrough'
175
+ )
176
+ .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
177
+ const nextCandidates = new Set(passthrough)
178
+ const removedCandidate = [...compiledCandidates].some(
179
+ (candidate) => !nextCandidates.has(candidate)
180
+ )
181
+ if (removedCandidate) {
182
+ compiler = await createCompiler()
183
+ }
184
+ const nextCSS = compiler.build(passthrough)
185
+ compiledCandidates = nextCandidates
186
+ if (nextCSS === css) return false
187
+ css = nextCSS
188
+ return true
189
+ }
190
+
191
+ async function scanSource(id: string, source: string): Promise<boolean> {
192
+ if (!enabled || !scanner) return false
193
+ candidatesByFile.set(
194
+ await normalizeSourcePath(id),
195
+ candidatesForSource(scanner, id, source)
196
+ )
197
+ return rebuild()
198
+ }
199
+
200
+ async function removeSource(id: string): Promise<boolean> {
201
+ if (!candidatesByFile.delete(await normalizeSourcePath(id))) return false
202
+ return rebuild()
203
+ }
204
+
205
+ return {
206
+ clear,
207
+ configure,
208
+ removeSource,
209
+ scanSource,
210
+ get enabled() {
211
+ return enabled
212
+ },
213
+ get css() {
214
+ return css
215
+ },
216
+ get candidateCount() {
217
+ return compiledCandidates.size
218
+ },
219
+ }
220
+ }
221
+
222
+ export async function updateTailwindForWatchChange(
223
+ state: TailwindScannerState,
224
+ id: string,
225
+ event: TailwindWatchEvent,
226
+ configure: () => Promise<boolean>
227
+ ): Promise<boolean> {
228
+ if (!(await configure())) return false
229
+ if (event === 'delete') {
230
+ return state.removeSource(id)
231
+ }
232
+ try {
233
+ return state.scanSource(id, await readFile(id, 'utf8'))
234
+ } catch {
235
+ return state.removeSource(id)
236
+ }
237
+ }
238
+
239
+ function registerScannerSources(
240
+ scanner: Scanner,
241
+ onDependency: (file: string) => void,
242
+ onSourceGlob: (glob: string) => void
243
+ ) {
244
+ for (const file of scanner.files) {
245
+ onDependency(file)
246
+ }
247
+ for (const { base, pattern } of scanner.globs) {
248
+ onSourceGlob(path.join(base, pattern))
249
+ }
250
+ }
251
+
252
+ async function normalizeSourcePath(id: string): Promise<string> {
253
+ const cleanId = path.resolve(id.split('?', 1)[0])
254
+ try {
255
+ return await realpath(cleanId)
256
+ } catch {
257
+ try {
258
+ return path.join(await realpath(path.dirname(cleanId)), path.basename(cleanId))
259
+ } catch {
260
+ return cleanId
261
+ }
262
+ }
263
+ }
264
+
265
+ function candidatesForSource(scanner: Scanner, id: string, source: string): Set<string> {
266
+ return new Set(
267
+ scanner
268
+ .getCandidatesWithPositions({
269
+ content: source,
270
+ extension: extensionForFile(id),
271
+ })
272
+ .map(({ candidate }) => candidate)
273
+ )
274
+ }