@tamagui/static 1.100.6 → 1.101.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.
@@ -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"}