@tamagui/static 1.100.6 → 1.101.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,504 @@
1
+ import { basename } from 'node:path'
2
+
3
+ import { type BabelFileResult, transformFromAstSync } from '@babel/core'
4
+ import generator from '@babel/generator'
5
+ import { declare } from '@babel/helper-plugin-utils'
6
+ import { parse } from '@babel/parser'
7
+ import template from '@babel/template'
8
+ import * as t from '@babel/types'
9
+ import type { TamaguiOptions } from '@tamagui/static'
10
+ import {
11
+ createExtractor,
12
+ createLogger,
13
+ getPragmaOptions,
14
+ isSimpleSpread,
15
+ literalToAst,
16
+ loadTamaguiBuildConfigSync,
17
+ } from '@tamagui/static'
18
+
19
+ const importNativeView = template(`
20
+ const __ReactNativeView = require('react-native').View;
21
+ const __ReactNativeText = require('react-native').Text;
22
+ `)
23
+
24
+ const importStyleSheet = template(`
25
+ const __ReactNativeStyleSheet = require('react-native').StyleSheet;
26
+ `)
27
+
28
+ const importWithStyle = template(`
29
+ const __withStableStyle = require('@tamagui/core')._withStableStyle;
30
+ `)
31
+
32
+ const extractor = createExtractor({ platform: 'native' })
33
+
34
+ let tamaguiBuildOptionsLoaded: TamaguiOptions | null
35
+
36
+ export function extractToNative(
37
+ sourceFileName: string,
38
+ sourceCode: string,
39
+ options: TamaguiOptions
40
+ ): BabelFileResult {
41
+ const ast = parse(sourceCode, {
42
+ sourceType: 'module',
43
+ plugins: ['jsx', 'typescript'],
44
+ })
45
+
46
+ const babelPlugin = getBabelPlugin()
47
+
48
+ const out = transformFromAstSync(ast, sourceCode, {
49
+ plugins: [[babelPlugin, options]],
50
+ configFile: false,
51
+ sourceFileName,
52
+ filename: sourceFileName,
53
+ })
54
+
55
+ if (!out) {
56
+ throw new Error(`No output returned`)
57
+ }
58
+
59
+ return out
60
+ }
61
+
62
+ export function getBabelPlugin() {
63
+ return declare((api, options: TamaguiOptions) => {
64
+ api.assertVersion(7)
65
+ return getBabelParseDefinition(options)
66
+ })
67
+ }
68
+
69
+ export function getBabelParseDefinition(options: TamaguiOptions) {
70
+ return {
71
+ name: 'tamagui',
72
+
73
+ visitor: {
74
+ Program: {
75
+ enter(this: any, root) {
76
+ let sourcePath = this.file.opts.filename
77
+ if (sourcePath?.includes('node_modules')) {
78
+ return
79
+ }
80
+ // by default only pick up .jsx / .tsx
81
+ if (!sourcePath?.endsWith('.jsx') && !sourcePath?.endsWith('.tsx')) {
82
+ return
83
+ }
84
+
85
+ // this filename comes back incorrect in react-native, it adds /ios/ for some reason
86
+ // adding a fix here, but it's a bit tentative...
87
+ if (process.env.SOURCE_ROOT?.endsWith('ios')) {
88
+ sourcePath = sourcePath.replace('/ios', '')
89
+ }
90
+
91
+ let hasImportedView = false
92
+ let hasImportedViewWrapper = false
93
+ const sheetStyles = {}
94
+ const sheetIdentifier = root.scope.generateUidIdentifier('sheet')
95
+
96
+ // babel doesnt append the `//` so we need to
97
+ const firstCommentContents = // join because you can join together multiple pragmas
98
+ root.node.body[0]?.leadingComments
99
+ ?.map((comment) => comment?.value || ' ')
100
+ .join(' ') ?? ''
101
+ const firstComment = firstCommentContents ? `//${firstCommentContents}` : ''
102
+
103
+ const { shouldPrintDebug, shouldDisable } = getPragmaOptions({
104
+ source: firstComment,
105
+ path: sourcePath,
106
+ })
107
+
108
+ if (shouldDisable) {
109
+ return
110
+ }
111
+
112
+ if (!options.config && !options.components) {
113
+ // if no config/components given try and load from the tamagui.build.ts file
114
+ tamaguiBuildOptionsLoaded ||= loadTamaguiBuildConfigSync({})
115
+ }
116
+
117
+ const finalOptions = {
118
+ // @ts-ignore just in case they leave it out
119
+ platform: 'native',
120
+ ...tamaguiBuildOptionsLoaded,
121
+ ...options,
122
+ } satisfies TamaguiOptions
123
+
124
+ const printLog = createLogger(sourcePath, finalOptions)
125
+
126
+ function addSheetStyle(style: any, node: t.JSXOpeningElement) {
127
+ const styleIndex = `${Object.keys(sheetStyles).length}`
128
+ let key = `${styleIndex}`
129
+ if (process.env.NODE_ENV === 'development') {
130
+ const lineNumbers = node.loc
131
+ ? node.loc.start.line +
132
+ (node.loc.start.line !== node.loc.end.line
133
+ ? `-${node.loc.end.line}`
134
+ : '')
135
+ : ''
136
+ key += `:${basename(sourcePath)}:${lineNumbers}`
137
+ }
138
+ sheetStyles[key] = style
139
+ return readStyleExpr(key)
140
+ }
141
+
142
+ function readStyleExpr(key: string) {
143
+ return template(`SHEET['KEY']`)({
144
+ SHEET: sheetIdentifier.name,
145
+ KEY: key,
146
+ })['expression'] as t.MemberExpression
147
+ }
148
+
149
+ let res
150
+
151
+ try {
152
+ res = extractor.parseSync(root, {
153
+ importsWhitelist: ['constants.js', 'colors.js'],
154
+ extractStyledDefinitions: options.forceExtractStyleDefinitions,
155
+ excludeProps: new Set([
156
+ 'className',
157
+ 'userSelect',
158
+ 'whiteSpace',
159
+ 'textOverflow',
160
+ 'cursor',
161
+ 'contain',
162
+ ]),
163
+ shouldPrintDebug,
164
+ ...finalOptions,
165
+ // disable this extraction for now at least, need to figure out merging theme vs non-theme
166
+ // because theme need to stay in render(), whereas non-theme can be extracted
167
+ // for now just turn it off entirely at a small perf loss
168
+ disableExtractInlineMedia: true,
169
+ // disable extracting variables as no native concept of them (only theme values)
170
+ disableExtractVariables: options.experimentalFlattenThemesOnNative
171
+ ? false
172
+ : 'theme',
173
+ sourcePath,
174
+
175
+ // disabling flattening for now
176
+ // it's flattening a plain <Paragraph>hello</Paragraph> which breaks things because themes
177
+ // thinking it's not really worth the effort to do much compilation on native
178
+ // for now just disable flatten as it can only run in narrow places on native
179
+ // disableFlattening: 'styled',
180
+
181
+ getFlattenedNode({ isTextView }) {
182
+ if (!hasImportedView) {
183
+ hasImportedView = true
184
+ root.unshiftContainer('body', importNativeView())
185
+ }
186
+ return isTextView ? '__ReactNativeText' : '__ReactNativeView'
187
+ },
188
+
189
+ onExtractTag(props) {
190
+ const { isFlattened } = props
191
+
192
+ if (!isFlattened) {
193
+ // we aren't optimizing at all if not flattened anymore
194
+ return
195
+ }
196
+
197
+ assertValidTag(props.node)
198
+ const stylesExpr = t.arrayExpression([])
199
+ const hocStylesExpr = t.arrayExpression([])
200
+ const expressions: t.Expression[] = []
201
+ const finalAttrs: (t.JSXAttribute | t.JSXSpreadAttribute)[] = []
202
+ const themeKeysUsed = new Set<string>()
203
+
204
+ function getStyleExpression(style: Object | null) {
205
+ if (!style) return
206
+
207
+ // split theme properties and leave them as props since RN has no concept of theme
208
+ const { plain, themed } = splitThemeStyles(style)
209
+
210
+ // TODO: themed is not a good name, because it's not just theme it also includes tokens
211
+ let themeExpr: t.ObjectExpression | null = null
212
+ if (themed && options.experimentalFlattenThemesOnNative) {
213
+ for (const key in themed) {
214
+ themeKeysUsed.add(themed[key].split('$')[1])
215
+ }
216
+
217
+ // make a sub-array
218
+ themeExpr = getThemedStyleExpression(themed)
219
+ }
220
+ const ident = addSheetStyle(plain, props.node)
221
+ if (themeExpr) {
222
+ addStyleExpression(ident)
223
+ addStyleExpression(ident, true)
224
+ return themeExpr
225
+ }
226
+ // since we only do flattened disabling this path
227
+ return ident
228
+ }
229
+
230
+ function addStyleExpression(expr: any, HOC = false) {
231
+ if (Array.isArray(expr)) {
232
+ ;(HOC ? hocStylesExpr : stylesExpr).elements.push(...expr)
233
+ } else {
234
+ ;(HOC ? hocStylesExpr : stylesExpr).elements.push(expr)
235
+ }
236
+ }
237
+
238
+ function getThemedStyleExpression(styles: Object) {
239
+ const themedStylesAst = literalToAst(styles) as t.ObjectExpression
240
+ themedStylesAst.properties.forEach((_) => {
241
+ const prop = _ as t.ObjectProperty
242
+ if (prop.value.type === 'StringLiteral') {
243
+ prop.value = t.memberExpression(
244
+ t.identifier('theme'),
245
+ t.identifier(prop.value.value.slice(1) + '.get()')
246
+ )
247
+ }
248
+ })
249
+ return themedStylesAst
250
+ }
251
+
252
+ let hasDynamicStyle = false
253
+
254
+ for (const attr of props.attrs) {
255
+ switch (attr.type) {
256
+ case 'style': {
257
+ let styleExpr = getStyleExpression(attr.value)
258
+ addStyleExpression(styleExpr)
259
+ if (options.experimentalFlattenThemesOnNative) {
260
+ addStyleExpression(styleExpr, true)
261
+ }
262
+ break
263
+ }
264
+
265
+ case 'ternary': {
266
+ const { consequent, alternate } = attr.value
267
+ const consExpr = getStyleExpression(consequent)
268
+ const altExpr = getStyleExpression(alternate)
269
+
270
+ if (options.experimentalFlattenThemesOnNative) {
271
+ expressions.push(attr.value.test)
272
+ addStyleExpression(
273
+ t.conditionalExpression(
274
+ t.identifier(`_expressions[${expressions.length - 1}]`),
275
+ consExpr || t.nullLiteral(),
276
+ altExpr || t.nullLiteral()
277
+ ),
278
+ true
279
+ )
280
+ }
281
+
282
+ const styleExpr = t.conditionalExpression(
283
+ attr.value.test,
284
+ consExpr || t.nullLiteral(),
285
+ altExpr || t.nullLiteral()
286
+ )
287
+ addStyleExpression(
288
+ styleExpr
289
+ // TODO: what is this for ?
290
+ // isFlattened ? simpleHash(JSON.stringify({ consequent, alternate })) : undefined
291
+ )
292
+ break
293
+ }
294
+
295
+ case 'dynamic-style': {
296
+ hasDynamicStyle = true
297
+ expressions.push(attr.value as t.Expression)
298
+ if (options.experimentalFlattenDynamicValues) {
299
+ addStyleExpression(
300
+ t.objectExpression([
301
+ t.objectProperty(
302
+ t.identifier(attr.name as string),
303
+ t.identifier(`_expressions[${expressions.length - 1}]`)
304
+ ),
305
+ ]),
306
+ true
307
+ )
308
+ } else {
309
+ addStyleExpression(
310
+ t.objectExpression([
311
+ t.objectProperty(
312
+ t.identifier(attr.name as string),
313
+ attr.value as t.Expression
314
+ ),
315
+ ])
316
+ )
317
+ }
318
+ break
319
+ }
320
+
321
+ case 'attr': {
322
+ if (t.isJSXSpreadAttribute(attr.value)) {
323
+ if (isSimpleSpread(attr.value)) {
324
+ stylesExpr.elements.push(
325
+ t.memberExpression(attr.value.argument, t.identifier('style'))
326
+ )
327
+ if (options.experimentalFlattenThemesOnNative) {
328
+ hocStylesExpr.elements.push(
329
+ t.memberExpression(
330
+ attr.value.argument,
331
+ t.identifier('style')
332
+ )
333
+ )
334
+ }
335
+ }
336
+ }
337
+ finalAttrs.push(attr.value)
338
+ break
339
+ }
340
+ }
341
+ }
342
+
343
+ props.node.attributes = finalAttrs
344
+
345
+ if (props.isFlattened) {
346
+ if (
347
+ options.experimentalFlattenThemesOnNative &&
348
+ (themeKeysUsed.size ||
349
+ hocStylesExpr.elements.length > 1 ||
350
+ hasDynamicStyle)
351
+ ) {
352
+ if (!hasImportedViewWrapper) {
353
+ root.unshiftContainer('body', importWithStyle())
354
+ hasImportedViewWrapper = true
355
+ }
356
+
357
+ const name = props.node.name['name']
358
+ const WrapperIdentifier = root.scope.generateUidIdentifier(
359
+ name + 'Wrapper'
360
+ )
361
+
362
+ root.pushContainer(
363
+ 'body',
364
+ t.variableDeclaration('const', [
365
+ t.variableDeclarator(
366
+ WrapperIdentifier,
367
+ t.callExpression(t.identifier('__withStableStyle'), [
368
+ t.identifier(name),
369
+ t.arrowFunctionExpression(
370
+ [t.identifier('theme'), t.identifier('_expressions')],
371
+ t.blockStatement([
372
+ t.returnStatement(
373
+ t.callExpression(
374
+ t.memberExpression(
375
+ t.identifier('React'),
376
+ t.identifier('useMemo')
377
+ ),
378
+ [
379
+ t.arrowFunctionExpression(
380
+ [],
381
+ t.blockStatement([
382
+ t.returnStatement(
383
+ t.arrayExpression([...hocStylesExpr.elements])
384
+ ),
385
+ ])
386
+ ),
387
+ t.arrayExpression([
388
+ t.spreadElement(t.identifier('_expressions')),
389
+ ]),
390
+ ]
391
+ )
392
+ ),
393
+ ])
394
+ ),
395
+ ])
396
+ ),
397
+ ])
398
+ )
399
+
400
+ // @ts-ignore
401
+ props.node.name = WrapperIdentifier
402
+ if (props.jsxPath.node.closingElement) {
403
+ // @ts-ignore
404
+ props.jsxPath.node.closingElement.name = WrapperIdentifier
405
+ }
406
+
407
+ if (expressions.length) {
408
+ props.node.attributes.push(
409
+ t.jsxAttribute(
410
+ t.jsxIdentifier('expressions'),
411
+ t.jsxExpressionContainer(t.arrayExpression(expressions))
412
+ )
413
+ )
414
+ }
415
+ } else {
416
+ props.node.attributes.push(
417
+ t.jsxAttribute(
418
+ t.jsxIdentifier('style'),
419
+ t.jsxExpressionContainer(
420
+ stylesExpr.elements.length === 1
421
+ ? (stylesExpr.elements[0] as any)
422
+ : stylesExpr
423
+ )
424
+ )
425
+ )
426
+ }
427
+ }
428
+ },
429
+ })
430
+ } catch (err) {
431
+ if (err instanceof Error) {
432
+ // metro doesn't show stack so we can
433
+ let message = `${shouldPrintDebug === 'verbose' ? err : err.message}`
434
+ if (message.includes('Unexpected return value from visitor method')) {
435
+ message = 'Unexpected return value from visitor method'
436
+ }
437
+ console.warn('Error in Tamagui parse, skipping', message, err.stack)
438
+ return
439
+ }
440
+ }
441
+
442
+ if (!Object.keys(sheetStyles).length) {
443
+ if (shouldPrintDebug) {
444
+ console.info('END no styles')
445
+ }
446
+ if (res) printLog(res)
447
+ return
448
+ }
449
+
450
+ const sheetObject = literalToAst(sheetStyles)
451
+ const sheetOuter = template(
452
+ 'const SHEET = __ReactNativeStyleSheet.create(null)'
453
+ )({
454
+ SHEET: sheetIdentifier.name,
455
+ }) as any
456
+
457
+ // replace the null with our object
458
+ sheetOuter.declarations[0].init.arguments[0] = sheetObject
459
+ root.unshiftContainer('body', sheetOuter)
460
+ // add import
461
+ root.unshiftContainer('body', importStyleSheet())
462
+
463
+ if (shouldPrintDebug) {
464
+ console.info('\n -------- output code ------- \n')
465
+ console.info(
466
+ generator(root.parent)
467
+ .code.split('\n')
468
+ .filter((x) => !x.startsWith('//'))
469
+ .join('\n')
470
+ )
471
+ }
472
+
473
+ if (res) printLog(res)
474
+ },
475
+ },
476
+ },
477
+ }
478
+ }
479
+
480
+ function assertValidTag(node: t.JSXOpeningElement) {
481
+ if (node.attributes.find((x) => x.type === 'JSXAttribute' && x.name.name === 'style')) {
482
+ // we can just deopt here instead and log warning
483
+ // need to make onExtractTag have a special catch error or similar
484
+ if (process.env.DEBUG?.startsWith('tamagui')) {
485
+ console.warn('⚠️ Cannot pass style attribute to extracted style')
486
+ }
487
+ }
488
+ }
489
+
490
+ function splitThemeStyles(style: Object) {
491
+ const themed: Object = {}
492
+ const plain: Object = {}
493
+ let noTheme = true
494
+ for (const key in style) {
495
+ const val = style[key]
496
+ if (val && val[0] === '$') {
497
+ themed[key] = val
498
+ noTheme = false
499
+ } else {
500
+ plain[key] = val
501
+ }
502
+ }
503
+ return { themed: noTheme ? null : themed, plain }
504
+ }
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ export { createExtractor } from './extractor/createExtractor'
6
6
  export { literalToAst } from './extractor/literalToAst'
7
7
  export * from './constants'
8
8
  export * from './extractor/extractToClassNames'
9
+ export * from './extractor/extractToNative'
9
10
  export * from './extractor/extractHelpers'
10
11
  export * from './extractor/loadTamagui'
11
12
  export * from './extractor/watchTamaguiConfig'
@@ -0,0 +1,13 @@
1
+ import { type BabelFileResult } from '@babel/core';
2
+ import type { TamaguiOptions } from '@tamagui/static';
3
+ export declare function extractToNative(sourceFileName: string, sourceCode: string, options: TamaguiOptions): BabelFileResult;
4
+ export declare function getBabelPlugin(): any;
5
+ export declare function getBabelParseDefinition(options: TamaguiOptions): {
6
+ name: string;
7
+ visitor: {
8
+ Program: {
9
+ enter(this: any, root: any): void;
10
+ };
11
+ };
12
+ };
13
+ //# sourceMappingURL=extractToNative.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extractToNative.d.ts","sourceRoot":"","sources":["../../src/extractor/extractToNative.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,eAAe,EAAwB,MAAM,aAAa,CAAA;AAMxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AA2BrD,wBAAgB,eAAe,CAC7B,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,cAAc,GACtB,eAAe,CAoBjB;AAED,wBAAgB,cAAc,QAK7B;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,cAAc;;;;wBAM3C,GAAG;;;EAmZtB"}
package/types/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export { createExtractor } from './extractor/createExtractor';
6
6
  export { literalToAst } from './extractor/literalToAst';
7
7
  export * from './constants';
8
8
  export * from './extractor/extractToClassNames';
9
+ export * from './extractor/extractToNative';
9
10
  export * from './extractor/extractHelpers';
10
11
  export * from './extractor/loadTamagui';
11
12
  export * from './extractor/watchTamaguiConfig';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,CAAA;AAChB,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,SAAS,CAAA;AACvB,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACvD,cAAc,aAAa,CAAA;AAC3B,cAAc,iCAAiC,CAAA;AAC/C,cAAc,4BAA4B,CAAA;AAC1C,cAAc,yBAAyB,CAAA;AACvC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,0BAA0B,CAAA;AACxC,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,CAAA;AAChB,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,SAAS,CAAA;AACvB,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACvD,cAAc,aAAa,CAAA;AAC3B,cAAc,iCAAiC,CAAA;AAC/C,cAAc,6BAA6B,CAAA;AAC3C,cAAc,4BAA4B,CAAA;AAC1C,cAAc,yBAAyB,CAAA;AACvC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,0BAA0B,CAAA;AACxC,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA"}