rnwind 0.0.4 → 0.0.5
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.
- package/lib/cjs/core/normalize-classname.cjs +25 -0
- package/lib/cjs/core/normalize-classname.cjs.map +1 -0
- package/lib/cjs/core/normalize-classname.d.ts +10 -0
- package/lib/cjs/core/style-builder/build-style.cjs +258 -58
- package/lib/cjs/core/style-builder/build-style.cjs.map +1 -1
- package/lib/cjs/core/style-builder/build-style.d.ts +6 -1
- package/lib/cjs/core/style-builder/union-builder.cjs +37 -3
- package/lib/cjs/core/style-builder/union-builder.cjs.map +1 -1
- package/lib/cjs/core/style-builder/union-builder.d.ts +21 -1
- package/lib/cjs/metro/dts.cjs +7 -16
- package/lib/cjs/metro/dts.cjs.map +1 -1
- package/lib/cjs/metro/dts.d.ts +2 -4
- package/lib/cjs/metro/state.cjs +30 -78
- package/lib/cjs/metro/state.cjs.map +1 -1
- package/lib/cjs/metro/state.d.ts +8 -25
- package/lib/cjs/metro/transformer.cjs +193 -34
- package/lib/cjs/metro/transformer.cjs.map +1 -1
- package/lib/cjs/metro/with-config.cjs +2 -2
- package/lib/cjs/metro/with-config.cjs.map +1 -1
- package/lib/cjs/metro/with-config.d.ts +11 -26
- package/lib/cjs/metro/wrap-imports.cjs +273 -0
- package/lib/cjs/metro/wrap-imports.cjs.map +1 -0
- package/lib/cjs/metro/wrap-imports.d.ts +26 -0
- package/lib/cjs/runtime/components/rnwind-provider.cjs +0 -17
- package/lib/cjs/runtime/components/rnwind-provider.cjs.map +1 -1
- package/lib/cjs/runtime/components/rnwind-provider.d.ts +0 -14
- package/lib/cjs/runtime/hooks/use-css.cjs +16 -10
- package/lib/cjs/runtime/hooks/use-css.cjs.map +1 -1
- package/lib/cjs/runtime/hooks/use-css.d.ts +15 -9
- package/lib/cjs/runtime/index.cjs +11 -13
- package/lib/cjs/runtime/index.cjs.map +1 -1
- package/lib/cjs/runtime/index.d.ts +4 -9
- package/lib/cjs/runtime/lookup-css.cjs +10 -0
- package/lib/cjs/runtime/lookup-css.cjs.map +1 -1
- package/lib/cjs/runtime/lookup-css.d.ts +7 -0
- package/lib/cjs/runtime/resolve.cjs +348 -0
- package/lib/cjs/runtime/resolve.cjs.map +1 -0
- package/lib/cjs/runtime/resolve.d.ts +61 -0
- package/lib/cjs/runtime/wrap.cjs +254 -0
- package/lib/cjs/runtime/wrap.cjs.map +1 -0
- package/lib/cjs/runtime/wrap.d.ts +37 -0
- package/lib/cjs/testing/index.cjs +81 -50
- package/lib/cjs/testing/index.cjs.map +1 -1
- package/lib/esm/core/normalize-classname.d.ts +10 -0
- package/lib/esm/core/normalize-classname.mjs +23 -0
- package/lib/esm/core/normalize-classname.mjs.map +1 -0
- package/lib/esm/core/style-builder/build-style.d.ts +6 -1
- package/lib/esm/core/style-builder/build-style.mjs +258 -58
- package/lib/esm/core/style-builder/build-style.mjs.map +1 -1
- package/lib/esm/core/style-builder/union-builder.d.ts +21 -1
- package/lib/esm/core/style-builder/union-builder.mjs +37 -3
- package/lib/esm/core/style-builder/union-builder.mjs.map +1 -1
- package/lib/esm/metro/dts.d.ts +2 -4
- package/lib/esm/metro/dts.mjs +7 -16
- package/lib/esm/metro/dts.mjs.map +1 -1
- package/lib/esm/metro/state.d.ts +8 -25
- package/lib/esm/metro/state.mjs +30 -76
- package/lib/esm/metro/state.mjs.map +1 -1
- package/lib/esm/metro/transformer.mjs +194 -35
- package/lib/esm/metro/transformer.mjs.map +1 -1
- package/lib/esm/metro/with-config.d.ts +11 -26
- package/lib/esm/metro/with-config.mjs +2 -2
- package/lib/esm/metro/with-config.mjs.map +1 -1
- package/lib/esm/metro/wrap-imports.d.ts +26 -0
- package/lib/esm/metro/wrap-imports.mjs +250 -0
- package/lib/esm/metro/wrap-imports.mjs.map +1 -0
- package/lib/esm/runtime/components/rnwind-provider.d.ts +0 -14
- package/lib/esm/runtime/components/rnwind-provider.mjs +1 -17
- package/lib/esm/runtime/components/rnwind-provider.mjs.map +1 -1
- package/lib/esm/runtime/hooks/use-css.d.ts +15 -9
- package/lib/esm/runtime/hooks/use-css.mjs +16 -10
- package/lib/esm/runtime/hooks/use-css.mjs.map +1 -1
- package/lib/esm/runtime/index.d.ts +4 -9
- package/lib/esm/runtime/index.mjs +4 -4
- package/lib/esm/runtime/index.mjs.map +1 -1
- package/lib/esm/runtime/lookup-css.d.ts +7 -0
- package/lib/esm/runtime/lookup-css.mjs +10 -1
- package/lib/esm/runtime/lookup-css.mjs.map +1 -1
- package/lib/esm/runtime/resolve.d.ts +61 -0
- package/lib/esm/runtime/resolve.mjs +341 -0
- package/lib/esm/runtime/resolve.mjs.map +1 -0
- package/lib/esm/runtime/wrap.d.ts +37 -0
- package/lib/esm/runtime/wrap.mjs +251 -0
- package/lib/esm/runtime/wrap.mjs.map +1 -0
- package/lib/esm/testing/index.mjs +84 -53
- package/lib/esm/testing/index.mjs.map +1 -1
- package/package.json +2 -1
- package/src/core/normalize-classname.ts +19 -0
- package/src/core/style-builder/build-style.ts +286 -55
- package/src/core/style-builder/union-builder.ts +36 -3
- package/src/metro/dts.ts +7 -19
- package/src/metro/state.ts +29 -74
- package/src/metro/transformer.ts +190 -34
- package/src/metro/with-config.ts +13 -28
- package/src/metro/wrap-imports.ts +260 -0
- package/src/runtime/components/rnwind-provider.tsx +0 -17
- package/src/runtime/hooks/use-css.ts +17 -11
- package/src/runtime/index.ts +3 -26
- package/src/runtime/lookup-css.ts +10 -0
- package/src/runtime/resolve.ts +381 -0
- package/src/runtime/wrap.tsx +267 -0
- package/src/testing/index.ts +106 -56
- package/lib/cjs/core/parser/text-truncate.cjs +0 -78
- package/lib/cjs/core/parser/text-truncate.cjs.map +0 -1
- package/lib/cjs/metro/transform-ast.cjs +0 -1472
- package/lib/cjs/metro/transform-ast.cjs.map +0 -1
- package/lib/cjs/metro/transform-ast.d.ts +0 -88
- package/lib/cjs/runtime/haptics.cjs +0 -113
- package/lib/cjs/runtime/haptics.cjs.map +0 -1
- package/lib/cjs/runtime/haptics.d.ts +0 -48
- package/lib/cjs/runtime/interactive-box.cjs +0 -35
- package/lib/cjs/runtime/interactive-box.cjs.map +0 -1
- package/lib/cjs/runtime/interactive-box.d.ts +0 -40
- package/lib/esm/core/parser/text-truncate.mjs +0 -75
- package/lib/esm/core/parser/text-truncate.mjs.map +0 -1
- package/lib/esm/metro/transform-ast.d.ts +0 -88
- package/lib/esm/metro/transform-ast.mjs +0 -1451
- package/lib/esm/metro/transform-ast.mjs.map +0 -1
- package/lib/esm/runtime/haptics.d.ts +0 -48
- package/lib/esm/runtime/haptics.mjs +0 -110
- package/lib/esm/runtime/haptics.mjs.map +0 -1
- package/lib/esm/runtime/interactive-box.d.ts +0 -40
- package/lib/esm/runtime/interactive-box.mjs +0 -33
- package/lib/esm/runtime/interactive-box.mjs.map +0 -1
- package/src/metro/transform-ast.ts +0 -1729
- package/src/runtime/haptics.ts +0 -120
- package/src/runtime/interactive-box.tsx +0 -57
package/src/metro/state.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { createHash } from 'node:crypto'
|
|
|
4
4
|
import { UnionBuilder } from '../core/style-builder'
|
|
5
5
|
import { TailwindParser, type SourceEntry } from '../core/parser'
|
|
6
6
|
import { resolveThemeCss } from './css-imports'
|
|
7
|
+
import { buildWrapModules } from './wrap-imports'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Default oxide Scanner globs — walk every JS/TS source under the
|
|
@@ -49,12 +50,8 @@ const CSS_ENTRY_ENV = 'RNWIND_CSS_ENTRY_FILE'
|
|
|
49
50
|
const CACHE_DIR_ENV = 'RNWIND_CACHE_DIR'
|
|
50
51
|
/** Env var carrying `watchFolders` from Metro config (NUL-separated). */
|
|
51
52
|
const WATCH_FOLDERS_ENV = 'RNWIND_WATCH_FOLDERS'
|
|
52
|
-
/** Env var carrying extra
|
|
53
|
-
const
|
|
54
|
-
/** Env var carrying extra import sources whose JSX exports get className→style rewrites. Comma-separated. */
|
|
55
|
-
const HOST_SOURCES_ENV = 'RNWIND_HOST_SOURCES'
|
|
56
|
-
/** Env var carrying extra JSX tag names (verbatim, may contain `.`) treated as hosts. Comma-separated. */
|
|
57
|
-
const HOST_COMPONENTS_ENV = 'RNWIND_HOST_COMPONENTS'
|
|
53
|
+
/** Env var carrying extra modules whose component exports get `wrap()`-ed. Comma-separated. */
|
|
54
|
+
const WRAP_MODULES_ENV = 'RNWIND_WRAP_MODULES'
|
|
58
55
|
|
|
59
56
|
/** Memoised library fingerprint — read once per worker process. */
|
|
60
57
|
let libraryFingerprint: string | undefined
|
|
@@ -86,10 +83,10 @@ function readThemeHashFor(cssPath: string): string {
|
|
|
86
83
|
* dev OR npm install of a new version) the file bytes change, the
|
|
87
84
|
* fingerprint rotates, and Metro's transform cache invalidates.
|
|
88
85
|
*
|
|
89
|
-
* Includes the
|
|
90
|
-
* style-builder so a change to the transformer —
|
|
91
|
-
* injected
|
|
92
|
-
* on the next dev run. Without this, a user upgrading rnwind in-place
|
|
86
|
+
* Includes the import-rewriter (`wrap-imports`) and runtime resolver
|
|
87
|
+
* alongside the parser / style-builder so a change to the transformer —
|
|
88
|
+
* e.g. renaming the injected wrap helper — invalidates every stale
|
|
89
|
+
* per-file cache entry on the next dev run. Without this, a user upgrading rnwind in-place
|
|
93
90
|
* would keep loading the old transformed bytes; React-refresh would
|
|
94
91
|
* then preserve fiber state across the version bump and the rendered
|
|
95
92
|
* hook list could shift, surfacing as "change in the order of Hooks"
|
|
@@ -105,14 +102,14 @@ function getLibraryFingerprint(): string {
|
|
|
105
102
|
path.resolve(here, '..', 'core', 'style-builder', 'build-style.cjs'),
|
|
106
103
|
path.resolve(here, '..', 'core', 'parser', 'tw-parser.mjs'),
|
|
107
104
|
path.resolve(here, '..', 'core', 'parser', 'tw-parser.cjs'),
|
|
108
|
-
path.resolve(here, '
|
|
109
|
-
path.resolve(here, '
|
|
105
|
+
path.resolve(here, 'wrap-imports.mjs'),
|
|
106
|
+
path.resolve(here, 'wrap-imports.cjs'),
|
|
110
107
|
path.resolve(here, 'transformer.mjs'),
|
|
111
108
|
path.resolve(here, 'transformer.cjs'),
|
|
112
109
|
// Source-tree fallback for tests + workspace dev (no built lib yet).
|
|
113
110
|
path.resolve(here, '..', '..', 'src', 'core', 'style-builder', 'build-style.ts'),
|
|
114
111
|
path.resolve(here, '..', '..', 'src', 'core', 'parser', 'tw-parser.ts'),
|
|
115
|
-
path.resolve(here, '..', '..', 'src', 'metro', '
|
|
112
|
+
path.resolve(here, '..', '..', 'src', 'metro', 'wrap-imports.ts'),
|
|
116
113
|
path.resolve(here, '..', '..', 'src', 'metro', 'transformer.ts'),
|
|
117
114
|
]
|
|
118
115
|
const hash = createHash('sha256')
|
|
@@ -148,18 +145,14 @@ export interface RnwindState {
|
|
|
148
145
|
* can rebuild the same state without re-reading the Metro config.
|
|
149
146
|
* @param cssEntryFile Absolute path to the user's theme CSS.
|
|
150
147
|
* @param cacheDir Absolute path to the cache dir (`.rnwind`).
|
|
151
|
-
* @param watchFolders
|
|
152
|
-
* @param
|
|
153
|
-
* @param hostSources
|
|
154
|
-
* @param hostComponents
|
|
148
|
+
* @param watchFolders Monorepo watch folders to scan for atoms.
|
|
149
|
+
* @param wrapModules Extra modules whose component exports get `wrap()`-ed.
|
|
155
150
|
*/
|
|
156
151
|
export function configureRnwindState(
|
|
157
152
|
cssEntryFile: string,
|
|
158
153
|
cacheDir: string,
|
|
159
154
|
watchFolders: readonly string[] = [],
|
|
160
|
-
|
|
161
|
-
hostSources?: readonly string[],
|
|
162
|
-
hostComponents?: readonly string[],
|
|
155
|
+
wrapModules?: readonly string[],
|
|
163
156
|
): void {
|
|
164
157
|
process.env[CSS_ENTRY_ENV] = cssEntryFile
|
|
165
158
|
process.env[CACHE_DIR_ENV] = cacheDir
|
|
@@ -168,59 +161,23 @@ export function configureRnwindState(
|
|
|
168
161
|
} else {
|
|
169
162
|
process.env[WATCH_FOLDERS_ENV] = watchFolders.join('\0')
|
|
170
163
|
}
|
|
171
|
-
if (!
|
|
172
|
-
delete process.env[
|
|
164
|
+
if (!wrapModules || wrapModules.length === 0) {
|
|
165
|
+
delete process.env[WRAP_MODULES_ENV]
|
|
173
166
|
} else {
|
|
174
|
-
process.env[
|
|
175
|
-
}
|
|
176
|
-
if (!hostSources || hostSources.length === 0) {
|
|
177
|
-
delete process.env[HOST_SOURCES_ENV]
|
|
178
|
-
} else {
|
|
179
|
-
process.env[HOST_SOURCES_ENV] = hostSources.join(',')
|
|
180
|
-
}
|
|
181
|
-
if (!hostComponents || hostComponents.length === 0) {
|
|
182
|
-
delete process.env[HOST_COMPONENTS_ENV]
|
|
183
|
-
} else {
|
|
184
|
-
process.env[HOST_COMPONENTS_ENV] = hostComponents.join(',')
|
|
167
|
+
process.env[WRAP_MODULES_ENV] = wrapModules.join(',')
|
|
185
168
|
}
|
|
186
169
|
cached = null
|
|
187
170
|
}
|
|
188
171
|
|
|
189
172
|
/**
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
* either way.
|
|
194
|
-
* @returns User-supplied extra prefixes.
|
|
173
|
+
* Effective module → wrap-policy map: the built-in defaults merged with
|
|
174
|
+
* any extra modules the Metro config supplied.
|
|
175
|
+
* @returns Module → policy map the import-rewrite consults.
|
|
195
176
|
*/
|
|
196
|
-
export function
|
|
197
|
-
const raw = process.env[
|
|
198
|
-
|
|
199
|
-
return
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Read the caller-configured extra host module sources out of the
|
|
204
|
-
* worker environment. Empty array when unset — the transformer applies
|
|
205
|
-
* its built-in default list on top either way.
|
|
206
|
-
* @returns User-supplied extra host sources.
|
|
207
|
-
*/
|
|
208
|
-
export function getHostSources(): readonly string[] {
|
|
209
|
-
const raw = process.env[HOST_SOURCES_ENV]
|
|
210
|
-
if (!raw || raw.length === 0) return []
|
|
211
|
-
return raw.split(',').filter((entry) => entry.length > 0)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Read the caller-configured extra host JSX tag names out of the worker
|
|
216
|
-
* environment. Verbatim names — may include `.` for member expressions
|
|
217
|
-
* like `'Animated.View'`.
|
|
218
|
-
* @returns User-supplied extra host component names.
|
|
219
|
-
*/
|
|
220
|
-
export function getHostComponents(): readonly string[] {
|
|
221
|
-
const raw = process.env[HOST_COMPONENTS_ENV]
|
|
222
|
-
if (!raw || raw.length === 0) return []
|
|
223
|
-
return raw.split(',').filter((entry) => entry.length > 0)
|
|
177
|
+
export function getWrapModules(): ReturnType<typeof buildWrapModules> {
|
|
178
|
+
const raw = process.env[WRAP_MODULES_ENV]
|
|
179
|
+
const extra = raw && raw.length > 0 ? raw.split(',').filter((entry) => entry.length > 0) : undefined
|
|
180
|
+
return buildWrapModules(extra)
|
|
224
181
|
}
|
|
225
182
|
|
|
226
183
|
/**
|
|
@@ -261,14 +218,12 @@ export function getRnwindState(projectRoot: string): RnwindState {
|
|
|
261
218
|
*/
|
|
262
219
|
export function getRnwindCacheKey(): string {
|
|
263
220
|
const cssEntry = process.env[CSS_ENTRY_ENV] ?? ''
|
|
264
|
-
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const hostComponents = process.env[HOST_COMPONENTS_ENV] ?? ''
|
|
271
|
-
return `rnwind:${cssEntry}:${readThemeHashFor(cssEntry)}|lib:${getLibraryFingerprint()}|pfx:${prefixes}|hs:${hostSources}|hc:${hostComponents}`
|
|
221
|
+
// Wrap-module config changes which import sites get `wrap()`-ed, so it
|
|
222
|
+
// MUST flip the cache key — otherwise Metro replays stale transforms
|
|
223
|
+
// (a newly-opted-in module keeps its raw import, a removed one keeps
|
|
224
|
+
// the wrap).
|
|
225
|
+
const wrapModules = process.env[WRAP_MODULES_ENV] ?? ''
|
|
226
|
+
return `rnwind:${cssEntry}:${readThemeHashFor(cssEntry)}|lib:${getLibraryFingerprint()}|wm:${wrapModules}`
|
|
272
227
|
}
|
|
273
228
|
|
|
274
229
|
/** Drop the cached state — call after editing the theme CSS. */
|
package/src/metro/transformer.ts
CHANGED
|
@@ -4,8 +4,8 @@ import { parse } from '@babel/parser'
|
|
|
4
4
|
import generate from '@babel/generator'
|
|
5
5
|
import { createHash } from 'node:crypto'
|
|
6
6
|
import { realpathSync } from 'node:fs'
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { getRnwindCacheKey, getRnwindState, getWrapModules, onThemeChange } from './state'
|
|
8
|
+
import { rewriteWrapImports } from './wrap-imports'
|
|
9
9
|
import { STYLE_SPECIFIERS, THEME_SIGNATURE_MODULE } from './resolver'
|
|
10
10
|
import { filterUnknownClassCandidates } from './warn-unknown-classes'
|
|
11
11
|
|
|
@@ -62,15 +62,23 @@ function parseUserSource(source: string): File | null {
|
|
|
62
62
|
* @param candidates Every candidate oxide surfaced from the source.
|
|
63
63
|
* @param atoms Successfully resolved atoms (keys are class names).
|
|
64
64
|
* @param filename Source path, prefixed onto the warning.
|
|
65
|
+
* @param features Feature-atom maps (gradient / haptic) — their names are
|
|
66
|
+
* known classes even though they carry no RN style, so they're excluded
|
|
67
|
+
* from the unknown-class warning.
|
|
65
68
|
*/
|
|
66
69
|
function warnUnknownClasses(
|
|
67
70
|
source: string,
|
|
68
71
|
candidates: readonly string[],
|
|
69
72
|
atoms: ReadonlyMap<string, unknown>,
|
|
70
73
|
filename: string,
|
|
74
|
+
features: ReadonlyArray<ReadonlyMap<string, unknown>> = [],
|
|
71
75
|
): void {
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
// Feature atoms (gradient / haptic) resolve to no RN style, so they're
|
|
77
|
+
// absent from `atoms` — but they're NOT unknown. Fold their names into
|
|
78
|
+
// the known set so `active:haptic-rigid` etc. don't warn at build time.
|
|
79
|
+
const known = new Set(atoms.keys())
|
|
80
|
+
for (const map of features) for (const name of map.keys()) known.add(name)
|
|
81
|
+
const unknown = filterUnknownClassCandidates(source, candidates, known)
|
|
74
82
|
if (unknown.length === 0) return
|
|
75
83
|
// eslint-disable-next-line no-console
|
|
76
84
|
console.warn(`rnwind: unknown class${unknown.length > 1 ? 'es' : ''} in ${filename}: ${unknown.join(', ')}`)
|
|
@@ -114,56 +122,183 @@ function isThemeCssEntry(filename: string): boolean {
|
|
|
114
122
|
}
|
|
115
123
|
|
|
116
124
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
125
|
+
* Wrap host imports + compile any className literals, then regenerate
|
|
126
|
+
* source. Two paths:
|
|
127
|
+
* - **className present**: oxide-scan the file, record its atoms into
|
|
128
|
+
* the union, and inject the generated-style + theme-signature
|
|
129
|
+
* side-effect imports so the runtime registries populate.
|
|
130
|
+
* - **import-only** (a `{...rest}` forwarder or a leaf with no literal
|
|
131
|
+
* `className=`): just wrap the host imports so a forwarded className
|
|
132
|
+
* still resolves at render — no oxide scan, no injected imports.
|
|
133
|
+
*
|
|
134
|
+
* On parse failure, fall back to the original source — a transient parse
|
|
135
|
+
* error shouldn't crash Metro for a file the upstream might handle fine.
|
|
121
136
|
* @param args Metro args; `src` is the original source text.
|
|
122
|
-
* @returns Rewritten source text
|
|
137
|
+
* @returns Rewritten source text.
|
|
123
138
|
*/
|
|
124
139
|
async function rewriteSource(args: BabelTransformerArgs): Promise<string> {
|
|
125
140
|
const ast = parseUserSource(args.src)
|
|
126
141
|
if (!ast) return args.src
|
|
127
142
|
|
|
143
|
+
// Wrap host component imports so `<View className=…>` resolves at render
|
|
144
|
+
// through the runtime `wrap` (works for literal, spread, and forwarded
|
|
145
|
+
// classNames alike). No JSX is rewritten here.
|
|
146
|
+
const wrapped = rewriteWrapImports(ast, getWrapModules())
|
|
147
|
+
|
|
148
|
+
if (!/classname=/i.test(args.src)) {
|
|
149
|
+
// Import-only file: nothing to compile. Drop any stale atom
|
|
150
|
+
// contribution (className may have just been removed) and emit the
|
|
151
|
+
// wrapped imports — or the untouched source when nothing wrapped.
|
|
152
|
+
dropFileSafely(args.filename, projectRootOf(args))
|
|
153
|
+
return wrapped ? generateModule(ast).code : args.src
|
|
154
|
+
}
|
|
155
|
+
|
|
128
156
|
const state = getRnwindState(projectRootOf(args))
|
|
129
157
|
const extension = extensionOf(args.filename)
|
|
130
158
|
const parsed = await state.parser.parseAtoms({ content: args.src, extension })
|
|
131
159
|
|
|
132
|
-
warnUnknownClasses(args.src, parsed.candidates, parsed.atoms, args.filename)
|
|
160
|
+
warnUnknownClasses(args.src, parsed.candidates, parsed.atoms, args.filename, [parsed.gradientAtoms, parsed.hapticAtoms])
|
|
133
161
|
|
|
134
|
-
const classNamePrefixes = getClassNamePrefixes()
|
|
135
|
-
const hostSources = getHostSources()
|
|
136
|
-
const hostComponents = getHostComponents()
|
|
137
162
|
if (parsed.atoms.size === 0) {
|
|
138
163
|
state.builder.dropFile(args.filename)
|
|
139
164
|
await state.builder.writeSchemes()
|
|
140
|
-
transformAst(ast, {
|
|
141
|
-
styleSpecifiers: [],
|
|
142
|
-
gradientAtoms: parsed.gradientAtoms,
|
|
143
|
-
hapticAtoms: parsed.hapticAtoms,
|
|
144
|
-
classNamePrefixes,
|
|
145
|
-
hostSources,
|
|
146
|
-
hostComponents,
|
|
147
|
-
})
|
|
148
165
|
injectThemeSignatureImport(ast)
|
|
149
166
|
return generateModule(ast).code
|
|
150
167
|
}
|
|
151
168
|
|
|
152
|
-
const
|
|
169
|
+
const literals = collectClassNameLiterals(ast)
|
|
170
|
+
const { changed } = await state.builder.recordFile(args.filename, parsed.atoms, parsed.keyframes, literals)
|
|
153
171
|
if (changed) await state.builder.writeSchemes()
|
|
154
172
|
|
|
155
|
-
|
|
156
|
-
styleSpecifiers: STYLE_SPECIFIERS as unknown as readonly string[],
|
|
157
|
-
gradientAtoms: parsed.gradientAtoms,
|
|
158
|
-
hapticAtoms: parsed.hapticAtoms,
|
|
159
|
-
classNamePrefixes,
|
|
160
|
-
hostSources,
|
|
161
|
-
hostComponents,
|
|
162
|
-
})
|
|
173
|
+
injectSideEffectImports(ast, STYLE_SPECIFIERS)
|
|
163
174
|
injectThemeSignatureImport(ast)
|
|
164
175
|
return generateModule(ast).code
|
|
165
176
|
}
|
|
166
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Drop a file's union contribution, swallowing the "state not configured"
|
|
180
|
+
* error unit tests hit when they call the transformer without
|
|
181
|
+
* `configureRnwindState`.
|
|
182
|
+
* @param filename Absolute source path.
|
|
183
|
+
* @param projectRoot Project root for state lookup.
|
|
184
|
+
*/
|
|
185
|
+
function dropFileSafely(filename: string, projectRoot: string): void {
|
|
186
|
+
try {
|
|
187
|
+
getRnwindState(projectRoot).builder.dropFile(filename)
|
|
188
|
+
} catch {
|
|
189
|
+
// State not configured (standalone/unit test). Nothing to drop.
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Whether a JSX attribute names a className-style prop (`className` or
|
|
195
|
+
* any `<prefix>ClassName`).
|
|
196
|
+
* @param node JSX attribute node.
|
|
197
|
+
* @returns True when the attribute is a className prop.
|
|
198
|
+
*/
|
|
199
|
+
function isClassNameAttribute(node: t.JSXAttribute): boolean {
|
|
200
|
+
if (!t.isJSXIdentifier(node.name)) return false
|
|
201
|
+
const {name} = node.name
|
|
202
|
+
return name === 'className' || name.endsWith('ClassName')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Pull static string literals out of a className expression. Handles a
|
|
207
|
+
* bare string, a no-substitution template, and the branches of a
|
|
208
|
+
* ternary / `&&` (so `cond ? 'a' : 'b'` and `flag && 'x'` both register
|
|
209
|
+
* their literals). Dynamic interpolations are skipped — they resolve via
|
|
210
|
+
* the runtime atom path.
|
|
211
|
+
* @param expr Expression inside a `className={...}` container.
|
|
212
|
+
* @param out Accumulator for discovered literals.
|
|
213
|
+
*/
|
|
214
|
+
function collectLiteralsFromExpression(expr: t.Expression | t.JSXEmptyExpression | null | undefined, out: string[]): void {
|
|
215
|
+
if (!expr) return
|
|
216
|
+
if (t.isStringLiteral(expr)) {
|
|
217
|
+
out.push(expr.value)
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
if (t.isTemplateLiteral(expr) && expr.expressions.length === 0 && expr.quasis.length === 1) {
|
|
221
|
+
const cooked = expr.quasis[0]?.value.cooked
|
|
222
|
+
if (typeof cooked === 'string') out.push(cooked)
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
if (t.isConditionalExpression(expr)) {
|
|
226
|
+
collectLiteralsFromExpression(expr.consequent, out)
|
|
227
|
+
collectLiteralsFromExpression(expr.alternate, out)
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
if (t.isLogicalExpression(expr)) {
|
|
231
|
+
collectLiteralsFromExpression(expr.right as t.Expression, out)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** AST node keys the literal walk skips — position / comment metadata. */
|
|
236
|
+
const SKIP_WALK_KEYS = new Set(['type', 'loc', 'start', 'end', 'range', 'leadingComments', 'trailingComments', 'innerComments'])
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Collect the static literals from one className JSX attribute into the
|
|
240
|
+
* dedup accumulator.
|
|
241
|
+
* @param attribute The (already className-matched) JSX attribute.
|
|
242
|
+
* @param seen Dedup set of literals already collected.
|
|
243
|
+
* @param out Ordered accumulator.
|
|
244
|
+
*/
|
|
245
|
+
function collectAttributeLiterals(attribute: t.JSXAttribute, seen: Set<string>, out: string[]): void {
|
|
246
|
+
const { value } = attribute
|
|
247
|
+
const found: string[] = []
|
|
248
|
+
if (t.isStringLiteral(value)) found.push(value.value)
|
|
249
|
+
else if (t.isJSXExpressionContainer(value)) collectLiteralsFromExpression(value.expression, found)
|
|
250
|
+
for (const literal of found) {
|
|
251
|
+
if (seen.has(literal)) continue
|
|
252
|
+
seen.add(literal)
|
|
253
|
+
out.push(literal)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Walk the AST for every `className=` / `<prefix>ClassName=` literal so
|
|
259
|
+
* the builder can pre-merge each into a per-scheme molecule. A generic
|
|
260
|
+
* node walk (no scope build) keeps it cheap; only JSX attribute nodes do
|
|
261
|
+
* any work.
|
|
262
|
+
* @param ast Parsed Babel file.
|
|
263
|
+
* @returns Distinct literal className strings, in first-seen order.
|
|
264
|
+
*/
|
|
265
|
+
function collectClassNameLiterals(ast: File): readonly string[] {
|
|
266
|
+
const out: string[] = []
|
|
267
|
+
const seen = new Set<string>()
|
|
268
|
+
const visit = (node: unknown): void => {
|
|
269
|
+
if (!node || typeof node !== 'object') return
|
|
270
|
+
if (Array.isArray(node)) {
|
|
271
|
+
for (const child of node) visit(child)
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
const typed = node as { type?: string; [key: string]: unknown }
|
|
275
|
+
if (typeof typed.type !== 'string') return
|
|
276
|
+
if (typed.type === 'JSXAttribute' && isClassNameAttribute(node as t.JSXAttribute)) {
|
|
277
|
+
collectAttributeLiterals(node as t.JSXAttribute, seen, out)
|
|
278
|
+
}
|
|
279
|
+
for (const key in typed) {
|
|
280
|
+
if (SKIP_WALK_KEYS.has(key)) continue
|
|
281
|
+
visit(typed[key])
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
visit(ast.program)
|
|
285
|
+
return out
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Prepend side-effect imports (`import '<spec>'`) so the generated
|
|
290
|
+
* per-scheme style + manifest modules load — registering this file's
|
|
291
|
+
* atoms / molecules / features into the runtime registries the wrapper's
|
|
292
|
+
* `resolve` reads.
|
|
293
|
+
* @param ast Babel File AST to mutate in place.
|
|
294
|
+
* @param specifiers Module specifiers to side-effect-import.
|
|
295
|
+
*/
|
|
296
|
+
function injectSideEffectImports(ast: File, specifiers: readonly string[]): void {
|
|
297
|
+
for (const specifier of specifiers) {
|
|
298
|
+
ast.program.body.unshift(t.importDeclaration([], t.stringLiteral(specifier)))
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
167
302
|
/**
|
|
168
303
|
* Prepend `import 'rnwind/__generated/theme-signature'` to every
|
|
169
304
|
* rnwind-transformed file. The resolver maps that specifier to the
|
|
@@ -252,10 +387,16 @@ function loadUpstream(): UpstreamTransformer | null {
|
|
|
252
387
|
*/
|
|
253
388
|
function isRewriteCandidate(args: BabelTransformerArgs): boolean {
|
|
254
389
|
if (!/\.(?:tsx|ts|jsx|js)$/i.test(args.filename)) return false
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
// `
|
|
258
|
-
|
|
390
|
+
// Process the file when it either:
|
|
391
|
+
// - carries a `className=` / `<prefix>ClassName=` literal (case-
|
|
392
|
+
// insensitive — `contentContainerClassName=` has a capital C), or
|
|
393
|
+
// - spreads props (`{...rest}`) onto a host from a wrap-module, where a
|
|
394
|
+
// forwarded className must still get its import wrapped (no literal
|
|
395
|
+
// appears in this file). A style-less `<View/>` with neither is left
|
|
396
|
+
// alone so it never pays for an unused wrapper.
|
|
397
|
+
const hasClassName = /classname=/i.test(args.src)
|
|
398
|
+
const isForwarder = /\{\s*\.\.\./.test(args.src) && mentionsWrapModule(args.src)
|
|
399
|
+
if (!hasClassName && !isForwarder) return false
|
|
259
400
|
if (!args.filename.includes('/node_modules/')) return true
|
|
260
401
|
// node_modules in path → could be a workspace symlink; resolve it.
|
|
261
402
|
try {
|
|
@@ -266,6 +407,21 @@ function isRewriteCandidate(args: BabelTransformerArgs): boolean {
|
|
|
266
407
|
}
|
|
267
408
|
}
|
|
268
409
|
|
|
410
|
+
/**
|
|
411
|
+
* Cheap pre-parse check: does the source import from any configured
|
|
412
|
+
* wrap-module? A quoted specifier match is enough — `rewriteWrapImports`
|
|
413
|
+
* re-verifies precisely on the AST, so a false positive only costs a
|
|
414
|
+
* no-op parse.
|
|
415
|
+
* @param source Source text.
|
|
416
|
+
* @returns True when a wrap-module specifier appears in the source.
|
|
417
|
+
*/
|
|
418
|
+
function mentionsWrapModule(source: string): boolean {
|
|
419
|
+
for (const moduleName of getWrapModules().keys()) {
|
|
420
|
+
if (source.includes(`'${moduleName}'`) || source.includes(`"${moduleName}"`)) return true
|
|
421
|
+
}
|
|
422
|
+
return false
|
|
423
|
+
}
|
|
424
|
+
|
|
269
425
|
/**
|
|
270
426
|
* Fallback parse when no upstream is configured AND Metro didn't hand
|
|
271
427
|
* us an AST. Used by unit tests and standalone setups.
|
package/src/metro/with-config.ts
CHANGED
|
@@ -127,35 +127,20 @@ export interface RnwindMetroOptions {
|
|
|
127
127
|
/** Cache directory. Absolute, or relative to `projectRoot`. Default: `.rnwind` at project root. */
|
|
128
128
|
cacheDir?: string
|
|
129
129
|
/**
|
|
130
|
-
* Extra
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* entries merge on top.
|
|
135
|
-
*/
|
|
136
|
-
classNamePrefixes?: readonly string[]
|
|
137
|
-
/**
|
|
138
|
-
* Extra module specifiers whose JSX exports rnwind should treat as
|
|
139
|
-
* "host components" — i.e. tags whose `className="…"` attribute is
|
|
140
|
-
* rewritten to `style={lookupCss(…)}` at build time (zero runtime
|
|
141
|
-
* cost). Merged with the built-in defaults: `react-native`,
|
|
130
|
+
* Extra module specifiers whose component exports rnwind should
|
|
131
|
+
* auto-wrap at import sites — `import { View } from 'react-native'`
|
|
132
|
+
* becomes `const View = wrap(_rnw0)` so `<View className="…">` resolves
|
|
133
|
+
* styles at runtime. Merged with the built-in defaults: `react-native`,
|
|
142
134
|
* `react-native-reanimated`, `react-native-svg`,
|
|
143
|
-
* `react-native-gesture-handler`, `
|
|
135
|
+
* `react-native-gesture-handler`, `react-native-safe-area-context`,
|
|
136
|
+
* `expo-linear-gradient`, `expo-image`, and more.
|
|
144
137
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
* primitive packages into the
|
|
149
|
-
*/
|
|
150
|
-
hostSources?: readonly string[]
|
|
151
|
-
/**
|
|
152
|
-
* Extra JSX tag names (verbatim — may include `.` for member access
|
|
153
|
-
* like `'Animated.View'`) rnwind should treat as host components,
|
|
154
|
-
* regardless of where they're imported from. Useful for one-off
|
|
155
|
-
* escape-hatches: `import { View as MyBox } from 'react-native'`
|
|
156
|
-
* doesn't change the local name → `'MyBox'` here picks it up.
|
|
138
|
+
* A module NOT in this list keeps its raw imports — the importing
|
|
139
|
+
* component receives `className` as a plain prop and can resolve it
|
|
140
|
+
* via `useCss` / `wrap` itself. Use this to opt your design-system /
|
|
141
|
+
* UI primitive packages into the auto-wrap path.
|
|
157
142
|
*/
|
|
158
|
-
|
|
143
|
+
wrapModules?: readonly string[]
|
|
159
144
|
}
|
|
160
145
|
|
|
161
146
|
/** Shape we mutate on Metro's config. Loose so we don't pin Metro's internal types. */
|
|
@@ -202,7 +187,7 @@ export function withRnwindConfig<C extends MetroConfigLike>(metroConfig: C, opti
|
|
|
202
187
|
|
|
203
188
|
mkdirSync(cacheDir, { recursive: true })
|
|
204
189
|
const watchFolders = (metroConfig.watchFolders ?? []).filter((p) => typeof p === 'string' && p.length > 0)
|
|
205
|
-
configureRnwindState(cssEntry, cacheDir, watchFolders, options.
|
|
190
|
+
configureRnwindState(cssEntry, cacheDir, watchFolders, options.wrapModules)
|
|
206
191
|
|
|
207
192
|
// Warm the state eagerly (in the Metro master process) so oxide's
|
|
208
193
|
// Scanner walks every project source (and every monorepo
|
|
@@ -238,7 +223,7 @@ export function withRnwindConfig<C extends MetroConfigLike>(metroConfig: C, opti
|
|
|
238
223
|
if (options.dtsFile !== false) {
|
|
239
224
|
const dtsPath = options.dtsFile ?? path.resolve(projectRoot, 'rnwind-types.d.ts')
|
|
240
225
|
const schemes = discoverSchemes(cssEntry, projectRoot)
|
|
241
|
-
writeDtsFile(dtsPath, schemes
|
|
226
|
+
writeDtsFile(dtsPath, schemes)
|
|
242
227
|
}
|
|
243
228
|
|
|
244
229
|
// Watch the theme CSS. On edit, we rewrite scheme files AND touch
|