@cssxjs/babel-plugin-rn-stylename-to-style 0.2.0
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/CHANGELOG.md +17 -0
- package/README.md +330 -0
- package/index.cjs +580 -0
- package/package.json +47 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# v0.2.0 (Tue Nov 04 2025)
|
|
2
|
+
|
|
3
|
+
#### 🚀 Enhancement
|
|
4
|
+
|
|
5
|
+
- feat: refactor all babel plugins to perform early transformation of all code in Program.enter block ([@cray0000](https://github.com/cray0000))
|
|
6
|
+
- feat: add TypeScript support, write a more comprehensive example in TSX ([@cray0000](https://github.com/cray0000))
|
|
7
|
+
- feat(runtime): implement support for both React Native and pure Web ([@cray0000](https://github.com/cray0000))
|
|
8
|
+
- feat: make it work for pure web through a babel plugin [#2](https://github.com/startupjs/cssx/pull/2) ([@cray0000](https://github.com/cray0000))
|
|
9
|
+
- feat: move over styles-related packages from startupjs ([@cray0000](https://github.com/cray0000))
|
|
10
|
+
|
|
11
|
+
#### ⚠️ Pushed to `master`
|
|
12
|
+
|
|
13
|
+
- tests: create a general test command which loops through all packages and runs tests. Update tests for babel-plugin-rn-stylename-to-style ([@cray0000](https://github.com/cray0000))
|
|
14
|
+
|
|
15
|
+
#### Authors: 1
|
|
16
|
+
|
|
17
|
+
- Pavel Zhukov ([@cray0000](https://github.com/cray0000))
|
package/README.md
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# @startupjs/babel-plugin-rn-stylename-to-style
|
|
2
|
+
|
|
3
|
+
Transform JSX `styleName` property to `style` property in react-native. The `styleName` attribute and syntax are based on [babel-plugin-react-css-modules](https://github.com/gajus/babel-plugin-react-css-modules#conventions).
|
|
4
|
+
|
|
5
|
+
## Information
|
|
6
|
+
|
|
7
|
+
This is the fork of https://github.com/kristerkari/babel-plugin-react-native-stylename-to-style
|
|
8
|
+
|
|
9
|
+
The differences are:
|
|
10
|
+
|
|
11
|
+
1. Support resolving multi-class selectors in CSS:
|
|
12
|
+
|
|
13
|
+
```jsx
|
|
14
|
+
import classnames from 'classnames'
|
|
15
|
+
|
|
16
|
+
function Button ({
|
|
17
|
+
variant, // [string] 'primary' | 'secondary'
|
|
18
|
+
dark, // [bool]
|
|
19
|
+
disabled // [bool]
|
|
20
|
+
}) {
|
|
21
|
+
return (
|
|
22
|
+
<Text
|
|
23
|
+
styleName={classnames('button', [variant, { dark, disabled }])}
|
|
24
|
+
>CLICK ME</Text>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```sass
|
|
30
|
+
.button
|
|
31
|
+
background-color: blue
|
|
32
|
+
&.primary
|
|
33
|
+
color: #ff0000
|
|
34
|
+
&.disabled
|
|
35
|
+
color: rgba(#ff0000, 0.5)
|
|
36
|
+
&.secondary
|
|
37
|
+
color: #00ff00
|
|
38
|
+
&.disabled
|
|
39
|
+
color: rgba(#00ff00, 0.5)
|
|
40
|
+
&.disabled
|
|
41
|
+
color: #777
|
|
42
|
+
|
|
43
|
+
.dark
|
|
44
|
+
&.button
|
|
45
|
+
background-color: purple
|
|
46
|
+
&.primary
|
|
47
|
+
color: white
|
|
48
|
+
&.disabled
|
|
49
|
+
color: #ddd
|
|
50
|
+
&.disabled
|
|
51
|
+
color: #eee
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
And what's important is that selectors` specificity is properly emulated. For example:
|
|
55
|
+
|
|
56
|
+
Styles for `.button.primary.disabled` (specificity *30*) will override styles of `.button.disabled` (specificity *20*),
|
|
57
|
+
even though `.button.disabled` is written later in the CSS.
|
|
58
|
+
|
|
59
|
+
This simple change brings a lot more capabilities in theming your components for a dynamic look.
|
|
60
|
+
|
|
61
|
+
2. Convert any `*StyleName` attribute to the according `*Style` attribute. This is very useful for passing the sub-element styles (which are usually exposed by react-native libraries) directly from CSS.
|
|
62
|
+
|
|
63
|
+
3. If the `styleName` value is an object or an array, automatically pipe it through the `classnames`-like library.
|
|
64
|
+
|
|
65
|
+
4. Support for multiple named css file imports is removed
|
|
66
|
+
|
|
67
|
+
## Usage
|
|
68
|
+
|
|
69
|
+
**WARNING:** This plugin is already built in into the `babel-preset-startupjs` and is included into the default StartupJS project.
|
|
70
|
+
|
|
71
|
+
If you want to use this plugin separately from StartupJS:
|
|
72
|
+
|
|
73
|
+
### Step 1: Install
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
yarn add --dev @startupjs/babel-plugin-rn-stylename-to-style
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
or
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
npm install --save-dev @startupjs/babel-plugin-rn-stylename-to-style
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Step 2: Configure `.babelrc`
|
|
86
|
+
|
|
87
|
+
You must give one or more file extensions inside an array in the plugin options.
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
{
|
|
91
|
+
"presets": [
|
|
92
|
+
"react-native"
|
|
93
|
+
],
|
|
94
|
+
"plugins": [
|
|
95
|
+
["react-native-dynamic-stylename-to-style", {
|
|
96
|
+
"extensions": ["css"]
|
|
97
|
+
}]
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Plugin Options
|
|
103
|
+
|
|
104
|
+
#### `extensions`
|
|
105
|
+
|
|
106
|
+
**Required**
|
|
107
|
+
|
|
108
|
+
List of css extensions to process (`css`, `styl`, `sass`, etc.)
|
|
109
|
+
|
|
110
|
+
#### `useImport`
|
|
111
|
+
|
|
112
|
+
**Default:** `false`
|
|
113
|
+
|
|
114
|
+
Whether to generate ESM `import` instead of CJS `require`.
|
|
115
|
+
|
|
116
|
+
#### `parseJson`
|
|
117
|
+
|
|
118
|
+
**Default:** `false`
|
|
119
|
+
|
|
120
|
+
Whether the imported css is expected to be a JSON string or an object.
|
|
121
|
+
If this flag is specified then JSON string is expected and it will do `JSON.parse` on it.
|
|
122
|
+
|
|
123
|
+
#### `cache`
|
|
124
|
+
|
|
125
|
+
**Default:** `undefined`
|
|
126
|
+
|
|
127
|
+
Whether to use integration with some caching library. Currently supported ones:
|
|
128
|
+
- `"teamplay"`
|
|
129
|
+
|
|
130
|
+
#### `platform`
|
|
131
|
+
|
|
132
|
+
**Default:** `'web'`
|
|
133
|
+
|
|
134
|
+
Which platform is targeted for compilation. Supported ones:
|
|
135
|
+
- `"web"`
|
|
136
|
+
- `"react-native"`
|
|
137
|
+
|
|
138
|
+
## Syntax
|
|
139
|
+
|
|
140
|
+
## Anonymous reference
|
|
141
|
+
|
|
142
|
+
Anonymous reference can be used when there is only one stylesheet import.
|
|
143
|
+
|
|
144
|
+
### Single class
|
|
145
|
+
|
|
146
|
+
```jsx
|
|
147
|
+
import "./Button.css";
|
|
148
|
+
|
|
149
|
+
<View styleName="wrapper">
|
|
150
|
+
<Text>Foo</Text>
|
|
151
|
+
</View>;
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
↓ ↓ ↓ ↓ ↓ ↓
|
|
155
|
+
|
|
156
|
+
```jsx
|
|
157
|
+
import Button from "./Button.css";
|
|
158
|
+
|
|
159
|
+
<View style={Button.wrapper}>
|
|
160
|
+
<Text>Foo</Text>
|
|
161
|
+
</View>;
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Multiple classes
|
|
165
|
+
|
|
166
|
+
```jsx
|
|
167
|
+
import "./Button.css";
|
|
168
|
+
|
|
169
|
+
<View styleName="wrapper red-background">
|
|
170
|
+
<Text>Foo</Text>
|
|
171
|
+
</View>;
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
↓ ↓ ↓ ↓ ↓ ↓
|
|
175
|
+
|
|
176
|
+
```jsx
|
|
177
|
+
import Button from "./Button.css";
|
|
178
|
+
|
|
179
|
+
<View style={[Button.wrapper, Button["red-background"]]}>
|
|
180
|
+
<Text>Foo</Text>
|
|
181
|
+
</View>;
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Expression
|
|
185
|
+
|
|
186
|
+
```jsx
|
|
187
|
+
import "./Button.css";
|
|
188
|
+
const name = "wrapper";
|
|
189
|
+
|
|
190
|
+
<View styleName={name}>
|
|
191
|
+
<Text>Foo</Text>
|
|
192
|
+
</View>;
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
↓ ↓ ↓ ↓ ↓ ↓
|
|
196
|
+
|
|
197
|
+
```jsx
|
|
198
|
+
import Button from "./Button.css";
|
|
199
|
+
const name = "wrapper";
|
|
200
|
+
|
|
201
|
+
<View
|
|
202
|
+
style={(name || "")
|
|
203
|
+
.split(" ")
|
|
204
|
+
.filter(Boolean)
|
|
205
|
+
.map(function(name) {
|
|
206
|
+
Button[name];
|
|
207
|
+
})}
|
|
208
|
+
>
|
|
209
|
+
<Text>Foo</Text>
|
|
210
|
+
</View>;
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Expression with ternary
|
|
214
|
+
|
|
215
|
+
```jsx
|
|
216
|
+
import "./Button.css";
|
|
217
|
+
|
|
218
|
+
const condition = true;
|
|
219
|
+
const name = "wrapper";
|
|
220
|
+
|
|
221
|
+
<View styleName={condition ? name : "bar"}>
|
|
222
|
+
<Text>Foo</Text>
|
|
223
|
+
</View>;
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
↓ ↓ ↓ ↓ ↓ ↓
|
|
227
|
+
|
|
228
|
+
```jsx
|
|
229
|
+
import Button from "./Button.css";
|
|
230
|
+
|
|
231
|
+
const condition = true;
|
|
232
|
+
const name = "wrapper";
|
|
233
|
+
|
|
234
|
+
<View
|
|
235
|
+
style={((condition ? name : "bar") || "")
|
|
236
|
+
.split(" ")
|
|
237
|
+
.filter(Boolean)
|
|
238
|
+
.map(function(name) {
|
|
239
|
+
Button[name];
|
|
240
|
+
})}
|
|
241
|
+
>
|
|
242
|
+
<Text>Foo</Text>
|
|
243
|
+
</View>;
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### with `styleName` and `style`:
|
|
247
|
+
|
|
248
|
+
```jsx
|
|
249
|
+
import "./Button.css";
|
|
250
|
+
|
|
251
|
+
<View styleName="wrapper" style={{ height: 10 }}>
|
|
252
|
+
<Text>Foo</Text>
|
|
253
|
+
</View>;
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
↓ ↓ ↓ ↓ ↓ ↓
|
|
257
|
+
|
|
258
|
+
```jsx
|
|
259
|
+
import Button from "./Button.css";
|
|
260
|
+
|
|
261
|
+
<View style={[Button.wrapper, { height: 10 }]}>
|
|
262
|
+
<Text>Foo</Text>
|
|
263
|
+
</View>;
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## ::part() selector
|
|
267
|
+
|
|
268
|
+
### Preprocess `part` attribute.
|
|
269
|
+
|
|
270
|
+
- Each `part` gets its styles from the `{part}Style` prop.
|
|
271
|
+
- `part='root'` is magic -- it's linked to the pure `style` prop.
|
|
272
|
+
|
|
273
|
+
Here is an example `<Card>` component which specifies its root container, title and footer as stylizable parts:
|
|
274
|
+
|
|
275
|
+
```jsx
|
|
276
|
+
// Card.js
|
|
277
|
+
|
|
278
|
+
function Card ({ title }) {
|
|
279
|
+
return (
|
|
280
|
+
<View part='root'>
|
|
281
|
+
<Text part='header'>{title}</Text>
|
|
282
|
+
<Text part='footer'>Copyright</Text>
|
|
283
|
+
</View>
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**↓ ↓ ↓ ↓ ↓ ↓**
|
|
289
|
+
|
|
290
|
+
```jsx
|
|
291
|
+
function Card ({ title, style, headerStyle, footerStyle }) {
|
|
292
|
+
return (
|
|
293
|
+
<View style={style}>
|
|
294
|
+
<Text style={headerStyle}>{title}</Text>
|
|
295
|
+
<Text style={footerStyle}>Copyright</Text>
|
|
296
|
+
</View>
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Preprocess `::part()` selector from CSS file to style any component which uses `part` attributes.
|
|
302
|
+
|
|
303
|
+
Following an example `<Card>` component above, we can call `<Card>` from the `<App>` and customize its parts styles:
|
|
304
|
+
|
|
305
|
+
```jsx
|
|
306
|
+
// App.js
|
|
307
|
+
|
|
308
|
+
import Card from './Card'
|
|
309
|
+
import './index.styl'
|
|
310
|
+
|
|
311
|
+
function App ({ users }) {
|
|
312
|
+
return users.map(user => (
|
|
313
|
+
<Card styleName='user' title={user.name} />
|
|
314
|
+
))
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
```styl
|
|
319
|
+
// index.styl
|
|
320
|
+
|
|
321
|
+
.user
|
|
322
|
+
margin-top 16px
|
|
323
|
+
|
|
324
|
+
&:part(header)
|
|
325
|
+
background-color black
|
|
326
|
+
color white
|
|
327
|
+
|
|
328
|
+
&:part(footer)
|
|
329
|
+
font-weight bold
|
|
330
|
+
```
|
package/index.cjs
ADDED
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
const nodePath = require('path')
|
|
2
|
+
const t = require('@babel/types')
|
|
3
|
+
const template = require('@babel/template').default
|
|
4
|
+
|
|
5
|
+
const COMPILERS = ['css', 'styl'] // used in rn-stylename-inline. TODO: move to a shared place
|
|
6
|
+
const RUNTIME_LIBRARY = 'cssxjs/runtime'
|
|
7
|
+
const STYLE_NAME_REGEX = /(?:^s|S)tyleName$/
|
|
8
|
+
const STYLE_REGEX = /(?:^s|S)tyle$/
|
|
9
|
+
const ROOT_STYLE_PROP_NAME = 'style'
|
|
10
|
+
const RUNTIME_PROCESS_NAME = 'cssx'
|
|
11
|
+
const OPTIONS_CACHE = ['teamplay']
|
|
12
|
+
const OPTIONS_PLATFORM = ['react-native', 'web']
|
|
13
|
+
|
|
14
|
+
const GLOBAL_OBSERVER_LIBRARY = 'startupjs'
|
|
15
|
+
const GLOBAL_OBSERVER_DEFAULT_NAME = 'observer'
|
|
16
|
+
|
|
17
|
+
const { GLOBAL_NAME, LOCAL_NAME } = require('@cssxjs/runtime/constants')
|
|
18
|
+
|
|
19
|
+
const buildSafeVar = template.expression(`
|
|
20
|
+
typeof %%variable%% !== 'undefined' && %%variable%%
|
|
21
|
+
`)
|
|
22
|
+
|
|
23
|
+
const buildImport = template(`
|
|
24
|
+
import %%name%% from %%runtimePath%%
|
|
25
|
+
`)
|
|
26
|
+
|
|
27
|
+
const buildRequire = template(`
|
|
28
|
+
const %%name%% = require(%%runtimePath%%)
|
|
29
|
+
`)
|
|
30
|
+
|
|
31
|
+
const buildJsonParse = template(`
|
|
32
|
+
const %%name%% = JSON.parse(%%jsonStyle%%)
|
|
33
|
+
`)
|
|
34
|
+
|
|
35
|
+
module.exports = function (babel) {
|
|
36
|
+
let styleHash = {}
|
|
37
|
+
let cssIdentifier
|
|
38
|
+
let hasObserver
|
|
39
|
+
let $program
|
|
40
|
+
let usedCompilers
|
|
41
|
+
|
|
42
|
+
function getStyleFromExpression (expression, state) {
|
|
43
|
+
state.hasTransformedClassName = true
|
|
44
|
+
const cssStyles = cssIdentifier.name
|
|
45
|
+
const processCall = t.callExpression(
|
|
46
|
+
state.reqName,
|
|
47
|
+
[expression, t.identifier(cssStyles)]
|
|
48
|
+
)
|
|
49
|
+
return processCall
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function addPartStyleToProps ($jsxAttribute) {
|
|
53
|
+
const parts = getParts($jsxAttribute.get('value'))
|
|
54
|
+
const $fnComponent = findReactFnComponent($jsxAttribute)
|
|
55
|
+
if (!$fnComponent) {
|
|
56
|
+
throw $jsxAttribute.buildCodeFrameError(`
|
|
57
|
+
Closest react functional component not found for 'part' attribute.
|
|
58
|
+
Or your component is named lowercase.'
|
|
59
|
+
`)
|
|
60
|
+
}
|
|
61
|
+
let props = $fnComponent.node.params[0]
|
|
62
|
+
if (!props) {
|
|
63
|
+
props = $jsxAttribute.scope.generateUidIdentifier('props')
|
|
64
|
+
$fnComponent.node.params[0] = props
|
|
65
|
+
}
|
|
66
|
+
if (t.isAssignmentPattern(props)) {
|
|
67
|
+
props = props.left
|
|
68
|
+
}
|
|
69
|
+
const styleProps = []
|
|
70
|
+
if (t.isIdentifier(props)) {
|
|
71
|
+
for (const part of parts) {
|
|
72
|
+
const partStyleAttr = convertPartName(part.name)
|
|
73
|
+
const value = t.memberExpression(props, t.identifier(partStyleAttr))
|
|
74
|
+
styleProps.push(buildDynamicPart(value, part))
|
|
75
|
+
}
|
|
76
|
+
} else if (t.isObjectPattern(props)) {
|
|
77
|
+
// TODO: optimize to be more efficient than O(n^2)
|
|
78
|
+
for (const part of parts) {
|
|
79
|
+
const partStyleAttr = convertPartName(part.name)
|
|
80
|
+
let exists
|
|
81
|
+
// Check whether the part style property already exists
|
|
82
|
+
for (const property of props.properties) {
|
|
83
|
+
if (!t.isObjectProperty(property)) continue
|
|
84
|
+
if (property.key.name === partStyleAttr) {
|
|
85
|
+
styleProps.push(buildDynamicPart(property.value, part))
|
|
86
|
+
exists = true
|
|
87
|
+
break
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (exists) continue
|
|
91
|
+
// If part style property doesn't exist, inject it
|
|
92
|
+
const key = t.identifier(partStyleAttr)
|
|
93
|
+
const value = $jsxAttribute.scope.generateUidIdentifier(partStyleAttr)
|
|
94
|
+
props.properties.unshift(t.objectProperty(key, value))
|
|
95
|
+
styleProps.push(buildDynamicPart(value, part))
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
throw $jsxAttribute.buildCodeFrameError(`
|
|
99
|
+
Can't find props attribute and embed 'part' style props into it.
|
|
100
|
+
Supported props formats:
|
|
101
|
+
- function Hello ({ one, two }) {}
|
|
102
|
+
- function Hello (props) {}
|
|
103
|
+
`)
|
|
104
|
+
}
|
|
105
|
+
return styleProps
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function handleStyleNames (jsxOpeningElementPath, state) {
|
|
109
|
+
if (!Object.keys(styleHash).length) return
|
|
110
|
+
const styleName = styleHash[ROOT_STYLE_PROP_NAME]?.styleName
|
|
111
|
+
const partStyle = styleHash[ROOT_STYLE_PROP_NAME]?.partStyle
|
|
112
|
+
const inlineStyles = []
|
|
113
|
+
|
|
114
|
+
// Always process if 'observer' import is found in the file
|
|
115
|
+
// which is needed for styles caching.
|
|
116
|
+
// Otherwise, if no 'observer' found and no 'styleName' or 'part' found then skip
|
|
117
|
+
if (!(hasObserver || styleName || partStyle)) return
|
|
118
|
+
|
|
119
|
+
// Check if styleName exists and if it can be processed
|
|
120
|
+
if (styleName != null) {
|
|
121
|
+
if (!(
|
|
122
|
+
t.isStringLiteral(styleName.node.value) ||
|
|
123
|
+
t.isJSXExpressionContainer(styleName.node.value)
|
|
124
|
+
)) {
|
|
125
|
+
throw jsxOpeningElementPath.buildCodeFrameError(`
|
|
126
|
+
styleName attribute has an unsupported type. It must be either a string or an expression.
|
|
127
|
+
|
|
128
|
+
Most likely you wrote styleName=[] instead of styleName={[]}
|
|
129
|
+
`)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Gather all inline styles
|
|
134
|
+
for (const key in styleHash) {
|
|
135
|
+
if (styleHash[key].style || styleHash[key].partStyle) {
|
|
136
|
+
let style = []
|
|
137
|
+
if (styleHash[key].style) {
|
|
138
|
+
style.push(styleHash[key].style.node.value.expression)
|
|
139
|
+
}
|
|
140
|
+
// Part style has higher priority, so must be last
|
|
141
|
+
if (styleHash[key].partStyle) {
|
|
142
|
+
style = style.concat(styleHash[key].partStyle)
|
|
143
|
+
}
|
|
144
|
+
if (style.length > 1) {
|
|
145
|
+
style = t.arrayExpression(style)
|
|
146
|
+
} else {
|
|
147
|
+
style = style[0]
|
|
148
|
+
}
|
|
149
|
+
inlineStyles.push(t.objectProperty(
|
|
150
|
+
t.identifier(key),
|
|
151
|
+
style
|
|
152
|
+
))
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Create a `process` function call
|
|
157
|
+
state.hasTransformedClassName = true
|
|
158
|
+
const processCall = t.callExpression(
|
|
159
|
+
state.reqName,
|
|
160
|
+
[
|
|
161
|
+
styleName
|
|
162
|
+
? (
|
|
163
|
+
t.isStringLiteral(styleName.node.value)
|
|
164
|
+
? styleName.node.value
|
|
165
|
+
: styleName.node.value.expression
|
|
166
|
+
)
|
|
167
|
+
: t.stringLiteral(''),
|
|
168
|
+
cssIdentifier
|
|
169
|
+
? t.identifier(cssIdentifier.name)
|
|
170
|
+
: t.objectExpression([]),
|
|
171
|
+
buildSafeVar({ variable: t.identifier(GLOBAL_NAME) }),
|
|
172
|
+
buildSafeVar({ variable: t.identifier(LOCAL_NAME) }),
|
|
173
|
+
t.objectExpression(inlineStyles)
|
|
174
|
+
]
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
jsxOpeningElementPath.node.attributes.push(
|
|
178
|
+
t.jsxSpreadAttribute(processCall)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
// Remove old attributes
|
|
182
|
+
for (const key in styleHash) {
|
|
183
|
+
if (styleHash[key].style) styleHash[key].style.remove()
|
|
184
|
+
if (styleHash[key].styleName) styleHash[key].styleName.remove()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Clear hash since we handled everything
|
|
188
|
+
for (const key in styleHash) {
|
|
189
|
+
delete styleHash[key].styleName
|
|
190
|
+
delete styleHash[key].style
|
|
191
|
+
delete styleHash[key].partStyle
|
|
192
|
+
delete styleHash[key]
|
|
193
|
+
}
|
|
194
|
+
styleHash = {}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function handleStyleName (state, convertedName, styleName, style) {
|
|
198
|
+
let expressions
|
|
199
|
+
|
|
200
|
+
if (
|
|
201
|
+
styleName == null ||
|
|
202
|
+
cssIdentifier == null ||
|
|
203
|
+
!(
|
|
204
|
+
t.isStringLiteral(styleName.node.value) ||
|
|
205
|
+
t.isJSXExpressionContainer(styleName.node.value)
|
|
206
|
+
)
|
|
207
|
+
) {
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (t.isStringLiteral(styleName.node.value)) {
|
|
212
|
+
expressions = [
|
|
213
|
+
getStyleFromExpression(styleName.node.value, state)
|
|
214
|
+
]
|
|
215
|
+
} else if (t.isJSXExpressionContainer(styleName.node.value)) {
|
|
216
|
+
expressions = [
|
|
217
|
+
getStyleFromExpression(styleName.node.value.expression, state)
|
|
218
|
+
]
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const hasStyleNameAndStyle =
|
|
222
|
+
styleName &&
|
|
223
|
+
style &&
|
|
224
|
+
styleName.parentPath.node === style.parentPath.node
|
|
225
|
+
|
|
226
|
+
if (hasStyleNameAndStyle) {
|
|
227
|
+
style.node.value = t.jsxExpressionContainer(
|
|
228
|
+
t.arrayExpression(
|
|
229
|
+
expressions.concat([style.node.value.expression])
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
styleName.remove()
|
|
233
|
+
} else {
|
|
234
|
+
if (expressions.length > 1) {
|
|
235
|
+
styleName.node.value = t.jsxExpressionContainer(t.arrayExpression(expressions))
|
|
236
|
+
} else {
|
|
237
|
+
styleName.node.value = t.jsxExpressionContainer(expressions[0])
|
|
238
|
+
}
|
|
239
|
+
styleName.node.name.name = convertedName
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
post () {
|
|
245
|
+
styleHash = {}
|
|
246
|
+
cssIdentifier = undefined
|
|
247
|
+
hasObserver = undefined
|
|
248
|
+
$program = undefined
|
|
249
|
+
usedCompilers = undefined
|
|
250
|
+
},
|
|
251
|
+
visitor: {
|
|
252
|
+
Program: {
|
|
253
|
+
// TODO: Refactor this to move the sub-visitor out into getVisitor() function
|
|
254
|
+
enter ($this, state) {
|
|
255
|
+
// 1. Init
|
|
256
|
+
usedCompilers = getUsedCompilers($this)
|
|
257
|
+
state.reqName = $this.scope.generateUidIdentifier(RUNTIME_PROCESS_NAME)
|
|
258
|
+
$program = $this
|
|
259
|
+
|
|
260
|
+
// 2. Run early traversal of everything
|
|
261
|
+
$this.traverse({
|
|
262
|
+
ImportDeclaration ($this, state) {
|
|
263
|
+
if (!hasObserver) hasObserver = checkObserverImport($this)
|
|
264
|
+
|
|
265
|
+
const extensions =
|
|
266
|
+
Array.isArray(state.opts.extensions) &&
|
|
267
|
+
state.opts.extensions
|
|
268
|
+
|
|
269
|
+
if (!extensions) {
|
|
270
|
+
throw new Error(
|
|
271
|
+
'You have not specified any extensions in the plugin options.'
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const node = $this.node
|
|
276
|
+
|
|
277
|
+
if (extensions.indexOf(getExt(node)) === -1) {
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const anonymousImports = $this.container.filter(n => {
|
|
282
|
+
return (
|
|
283
|
+
t.isImportDeclaration(n) &&
|
|
284
|
+
n.specifiers.length === 0 &&
|
|
285
|
+
extensions.indexOf(getExt(n)) > -1
|
|
286
|
+
)
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
if (anonymousImports.length > 1) {
|
|
290
|
+
throw $this.buildCodeFrameError(
|
|
291
|
+
'Cannot use anonymous style name with more than one stylesheet import.'
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let specifier = node.specifiers[0]
|
|
296
|
+
|
|
297
|
+
if (!specifier) {
|
|
298
|
+
specifier = t.ImportDefaultSpecifier(
|
|
299
|
+
$this.scope.generateUidIdentifier('css')
|
|
300
|
+
)
|
|
301
|
+
node.specifiers = [specifier]
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
cssIdentifier = specifier.local
|
|
305
|
+
|
|
306
|
+
// Do JSON.parse() on the css file if we receive it as a json string:
|
|
307
|
+
// import css from './index.styl'
|
|
308
|
+
// v v v
|
|
309
|
+
// import jsonCss from './index.styl'
|
|
310
|
+
// const css = JSON.parse(jsonCss)
|
|
311
|
+
if (state.opts.parseJson) {
|
|
312
|
+
const lastImportOrRequire = $program
|
|
313
|
+
.get('body')
|
|
314
|
+
.filter(p => p.isImportDeclaration() || isRequire(p.node))
|
|
315
|
+
.pop()
|
|
316
|
+
const tempCssIdentifier = $this.scope.generateUidIdentifier('jsonCss')
|
|
317
|
+
node.specifiers[0].local = tempCssIdentifier
|
|
318
|
+
lastImportOrRequire.insertAfter(
|
|
319
|
+
buildJsonParse({
|
|
320
|
+
name: cssIdentifier,
|
|
321
|
+
jsonStyle: tempCssIdentifier
|
|
322
|
+
})
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
JSXOpeningElement: {
|
|
327
|
+
exit ($this, state) {
|
|
328
|
+
// TODO: Don't handle *StyleName separately, instead merge it into handleStyleNames()
|
|
329
|
+
for (const key in styleHash) {
|
|
330
|
+
// root styleName can only be handled by new logic in handleStyleNames()
|
|
331
|
+
if (key === ROOT_STYLE_PROP_NAME) continue
|
|
332
|
+
if (styleHash[key].styleName) {
|
|
333
|
+
handleStyleName(state, key, styleHash[key].styleName, styleHash[key].style)
|
|
334
|
+
delete styleHash[key].styleName
|
|
335
|
+
delete styleHash[key].style
|
|
336
|
+
delete styleHash[key]
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// New logic with support for ::part(name) pseudo-class
|
|
340
|
+
handleStyleNames($this, state)
|
|
341
|
+
styleHash = {}
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
JSXAttribute ($this, state) {
|
|
345
|
+
const name = $this.node.name.name
|
|
346
|
+
if (STYLE_NAME_REGEX.test(name)) {
|
|
347
|
+
const convertedName = convertStyleName(name)
|
|
348
|
+
if (!styleHash[convertedName]) styleHash[convertedName] = {}
|
|
349
|
+
styleHash[convertedName].styleName = $this
|
|
350
|
+
// Some react-native built-in stuff might have props like 'barStyle' which
|
|
351
|
+
// is a string. We skip those.
|
|
352
|
+
} else if (STYLE_REGEX.test(name) && !$this.get('value').isStringLiteral()) {
|
|
353
|
+
if (!styleHash[name]) styleHash[name] = {}
|
|
354
|
+
styleHash[name].style = $this
|
|
355
|
+
} else if (name === 'part') {
|
|
356
|
+
validatePart($this)
|
|
357
|
+
const styleProps = addPartStyleToProps($this)
|
|
358
|
+
if (!styleHash[ROOT_STYLE_PROP_NAME]) styleHash[ROOT_STYLE_PROP_NAME] = {}
|
|
359
|
+
styleHash[ROOT_STYLE_PROP_NAME].partStyle = styleProps
|
|
360
|
+
$this.remove()
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
CallExpression ($this, state) {
|
|
364
|
+
const $callee = $this.get('callee')
|
|
365
|
+
if (!$callee.isIdentifier()) return
|
|
366
|
+
if (!usedCompilers?.[$callee.node.name]) return
|
|
367
|
+
// Create a `process` function call
|
|
368
|
+
state.hasTransformedClassName = true
|
|
369
|
+
const processCall = t.callExpression(
|
|
370
|
+
state.reqName,
|
|
371
|
+
[
|
|
372
|
+
$this.get('arguments.0')
|
|
373
|
+
? $this.get('arguments.0').node
|
|
374
|
+
: t.stringLiteral(''),
|
|
375
|
+
cssIdentifier
|
|
376
|
+
? t.identifier(cssIdentifier.name)
|
|
377
|
+
: t.objectExpression([]),
|
|
378
|
+
buildSafeVar({ variable: t.identifier(GLOBAL_NAME) }),
|
|
379
|
+
buildSafeVar({ variable: t.identifier(LOCAL_NAME) }),
|
|
380
|
+
$this.get('arguments.1')
|
|
381
|
+
? $this.get('arguments.1').node
|
|
382
|
+
: t.objectExpression([])
|
|
383
|
+
]
|
|
384
|
+
)
|
|
385
|
+
$this.replaceWith(processCall)
|
|
386
|
+
}
|
|
387
|
+
}, state)
|
|
388
|
+
|
|
389
|
+
// 3. Finalize
|
|
390
|
+
if (!state.hasTransformedClassName) {
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const lastImportOrRequire = $this
|
|
395
|
+
.get('body')
|
|
396
|
+
.filter(p => p.isImportDeclaration() || isRequire(p.node))
|
|
397
|
+
.pop()
|
|
398
|
+
|
|
399
|
+
if (lastImportOrRequire) {
|
|
400
|
+
const useImport = state.opts.useImport == null ? true : state.opts.useImport
|
|
401
|
+
const runtimePath = getRuntimePath($this, state)
|
|
402
|
+
lastImportOrRequire.insertAfter(
|
|
403
|
+
useImport
|
|
404
|
+
? buildImport({ name: state.reqName, runtimePath: t.stringLiteral(runtimePath) })
|
|
405
|
+
: buildRequire({ name: state.reqName, runtimePath: t.stringLiteral(runtimePath) })
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function isRequire (node) {
|
|
415
|
+
return node?.declarations?.[0]?.init?.callee?.name === 'require'
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function getExt (node) {
|
|
419
|
+
return nodePath.extname(node.source.value).replace(/^\./, '')
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function convertStyleName (name) {
|
|
423
|
+
return name.replace(/Name$/, '')
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function convertPartName (partName) {
|
|
427
|
+
if (partName === 'root') return 'style'
|
|
428
|
+
return partName + 'Style'
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function validatePart ($jsxAttribute) {
|
|
432
|
+
const $value = $jsxAttribute.get('value')
|
|
433
|
+
if ($value.isStringLiteral()) return true
|
|
434
|
+
if ($value.isJSXExpressionContainer()) {
|
|
435
|
+
const $expr = $value.get('expression')
|
|
436
|
+
if ($expr.isObjectExpression()) {
|
|
437
|
+
return validateDynamicPartObject($expr)
|
|
438
|
+
} else if ($expr.isArrayExpression()) {
|
|
439
|
+
return validateDynamicPartArray($expr)
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
throw $jsxAttribute.buildCodeFrameError(`
|
|
443
|
+
'part' attribute might only be the following:
|
|
444
|
+
- static string
|
|
445
|
+
- array (with static strings or objects)
|
|
446
|
+
- object (with static keys)
|
|
447
|
+
Basically the rule is that the name of the part must be static so that
|
|
448
|
+
it is possible to determine at compile time which parts are being used.
|
|
449
|
+
`)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function validateDynamicPartObject ($object) {
|
|
453
|
+
for (const $property of $object.get('properties')) {
|
|
454
|
+
if (!$property.isObjectProperty() || $property.node.computed) {
|
|
455
|
+
throw $property.buildCodeFrameError(`
|
|
456
|
+
'part' attribute only supports literal or string keys in object.
|
|
457
|
+
Dynamic keys or spreads are not supported.
|
|
458
|
+
`)
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return true
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function validateDynamicPartArray ($array) {
|
|
465
|
+
for (const $element of $array.get('elements')) {
|
|
466
|
+
if ($element.isStringLiteral()) continue
|
|
467
|
+
if ($element.isObjectExpression()) {
|
|
468
|
+
validateDynamicPartObject($element)
|
|
469
|
+
continue
|
|
470
|
+
}
|
|
471
|
+
throw $element.buildCodeFrameError(`
|
|
472
|
+
'part' attribute only supports static strings or objects inside an array.
|
|
473
|
+
`)
|
|
474
|
+
}
|
|
475
|
+
return true
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function getParts ($value) {
|
|
479
|
+
if ($value.isStringLiteral()) {
|
|
480
|
+
return $value.node.value.split(' ').filter(Boolean).map(i => ({ name: i }))
|
|
481
|
+
} else if ($value.isJSXExpressionContainer()) {
|
|
482
|
+
return getParts($value.get('expression'))
|
|
483
|
+
} else if ($value.isArrayExpression()) {
|
|
484
|
+
return $value.get('elements').map($el => getParts($el)).flat()
|
|
485
|
+
} else if ($value.isObjectExpression()) {
|
|
486
|
+
return $value.get('properties').map($prop => ({
|
|
487
|
+
name: $prop.node.key.name || $prop.node.key.value,
|
|
488
|
+
condition: $prop.node.value
|
|
489
|
+
}))
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function buildDynamicPart (expr, part) {
|
|
494
|
+
if (part.condition) {
|
|
495
|
+
return t.conditionalExpression(
|
|
496
|
+
part.condition,
|
|
497
|
+
expr,
|
|
498
|
+
t.identifier('undefined')
|
|
499
|
+
)
|
|
500
|
+
} else {
|
|
501
|
+
return expr
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function checkObserverImport ($import, {
|
|
506
|
+
observerLibrary = GLOBAL_OBSERVER_LIBRARY,
|
|
507
|
+
observerDefaultName = GLOBAL_OBSERVER_DEFAULT_NAME
|
|
508
|
+
} = {}) {
|
|
509
|
+
if ($import.node.source.value !== observerLibrary) return
|
|
510
|
+
for (const $specifier of $import.get('specifiers')) {
|
|
511
|
+
if (!$specifier.isImportSpecifier()) continue
|
|
512
|
+
const { imported } = $specifier.node
|
|
513
|
+
if (imported.name === observerDefaultName) return true
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// find topmost function (which is not a lowercase named one).
|
|
518
|
+
// .getFunctionParent() returns undefined when we reach Program
|
|
519
|
+
function findReactFnComponent ($jsxAttribute) {
|
|
520
|
+
let $current = $jsxAttribute.getFunctionParent()
|
|
521
|
+
let $potentialComponentFn
|
|
522
|
+
|
|
523
|
+
while ($current) {
|
|
524
|
+
// if function is named and starts with a capital letter then it's definitely a component
|
|
525
|
+
// and we return it right away
|
|
526
|
+
if ($current.node.id?.name && /^[A-Z]/.test($current.node.id.name)) return $current
|
|
527
|
+
// set function as component candidate,
|
|
528
|
+
// BUT ignore it if it's a named function which starts from a lowercase or underscore,
|
|
529
|
+
// because such function can never be a react component
|
|
530
|
+
if (!($current.node.id?.name && /^[a-z_]/.test($current.node.id.name))) {
|
|
531
|
+
$potentialComponentFn = $current
|
|
532
|
+
}
|
|
533
|
+
// and get the parent function definition
|
|
534
|
+
$current = $current.getFunctionParent()
|
|
535
|
+
}
|
|
536
|
+
return $potentialComponentFn
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Get compilers from the magic import
|
|
540
|
+
function getUsedCompilers ($program) {
|
|
541
|
+
for (const $import of $program.get('body')) {
|
|
542
|
+
if (!$import.isImportDeclaration()) continue
|
|
543
|
+
if ($import.get('source').node.value !== GLOBAL_OBSERVER_LIBRARY) continue
|
|
544
|
+
const usedCompilers = {}
|
|
545
|
+
for (const $specifier of $import.get('specifiers')) {
|
|
546
|
+
if (!$specifier.isImportSpecifier()) continue
|
|
547
|
+
const importedName = $specifier.get('imported').node.name
|
|
548
|
+
if (COMPILERS.includes(importedName)) {
|
|
549
|
+
const localName = $specifier.get('local').node.name
|
|
550
|
+
usedCompilers[localName] = true
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return usedCompilers
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function getRuntimePath ($node, state) {
|
|
558
|
+
const cache = state.opts.cache
|
|
559
|
+
if (cache && !OPTIONS_CACHE.includes(cache)) {
|
|
560
|
+
throw $node.buildCodeFrameError(
|
|
561
|
+
`Invalid cache option value: "${cache}". Supported values: ${OPTIONS_CACHE.join(', ')}`
|
|
562
|
+
)
|
|
563
|
+
}
|
|
564
|
+
const platform = state.opts.platform
|
|
565
|
+
if (platform && !OPTIONS_PLATFORM.includes(platform)) {
|
|
566
|
+
throw $node.buildCodeFrameError(
|
|
567
|
+
`Invalid platform option value: "${platform}". Supported values: ${OPTIONS_PLATFORM.join(', ')}`
|
|
568
|
+
)
|
|
569
|
+
}
|
|
570
|
+
let runtimePath = RUNTIME_LIBRARY
|
|
571
|
+
if (platform) {
|
|
572
|
+
runtimePath += `/${platform}`
|
|
573
|
+
if (cache) {
|
|
574
|
+
runtimePath += `-${cache}`
|
|
575
|
+
}
|
|
576
|
+
} else if (cache) {
|
|
577
|
+
runtimePath += `/${cache}`
|
|
578
|
+
}
|
|
579
|
+
return runtimePath
|
|
580
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cssxjs/babel-plugin-rn-stylename-to-style",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "Dynamically resolve styleName in RN with support for multi-class selectors (for easier modifiers)",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"babel",
|
|
10
|
+
"babel-plugin",
|
|
11
|
+
"react-native",
|
|
12
|
+
"stylename",
|
|
13
|
+
"style"
|
|
14
|
+
],
|
|
15
|
+
"main": "index.cjs",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./index.cjs",
|
|
18
|
+
"./process": "./process.js",
|
|
19
|
+
"./processCached": "./processCached.js",
|
|
20
|
+
"./constants": "./constants.cjs"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "jest"
|
|
25
|
+
},
|
|
26
|
+
"author": "Pavel Zhukov",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/startupjs/startupjs"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@babel/template": "^7.4.0",
|
|
34
|
+
"@babel/types": "^7.0.0",
|
|
35
|
+
"@cssxjs/runtime": "^0.2.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@babel/plugin-syntax-jsx": "^7.0.0",
|
|
39
|
+
"@cssxjs/babel-plugin-react-pug": "^0.2.0",
|
|
40
|
+
"babel-plugin-tester": "^9.1.0",
|
|
41
|
+
"jest": "^30.0.4"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"teamplay": "*"
|
|
45
|
+
},
|
|
46
|
+
"gitHead": "a80a44b4d14c116871ca03997ce772b6beabf5d8"
|
|
47
|
+
}
|