@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.
Files changed (4) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +330 -0
  3. package/index.cjs +580 -0
  4. 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
+ }