@tamagui/compiler-core 0.0.0-bootstrap.0 → 3.0.0-beta.643.1

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 (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -1
  3. package/dist/cjs/ast.cjs +95 -0
  4. package/dist/cjs/contracts.cjs +62 -0
  5. package/dist/cjs/diagnostics.cjs +69 -0
  6. package/dist/cjs/evaluate.cjs +351 -0
  7. package/dist/cjs/graph.cjs +279 -0
  8. package/dist/cjs/hash.cjs +49 -0
  9. package/dist/cjs/index.cjs +44 -0
  10. package/dist/cjs/ir.cjs +33 -0
  11. package/dist/cjs/lower.cjs +225 -0
  12. package/dist/cjs/materialize.cjs +200 -0
  13. package/dist/cjs/normalize.cjs +622 -0
  14. package/dist/cjs/output.cjs +149 -0
  15. package/dist/cjs/planCache.cjs +224 -0
  16. package/dist/cjs/session.cjs +119 -0
  17. package/dist/cjs/yuku.cjs +159 -0
  18. package/dist/cjs/zero.cjs +387 -0
  19. package/dist/esm/ast.mjs +65 -0
  20. package/dist/esm/ast.mjs.map +1 -0
  21. package/dist/esm/contracts.mjs +34 -0
  22. package/dist/esm/contracts.mjs.map +1 -0
  23. package/dist/esm/diagnostics.mjs +44 -0
  24. package/dist/esm/diagnostics.mjs.map +1 -0
  25. package/dist/esm/evaluate.mjs +327 -0
  26. package/dist/esm/evaluate.mjs.map +1 -0
  27. package/dist/esm/graph.mjs +256 -0
  28. package/dist/esm/graph.mjs.map +1 -0
  29. package/dist/esm/hash.mjs +26 -0
  30. package/dist/esm/hash.mjs.map +1 -0
  31. package/dist/esm/index.mjs +30 -0
  32. package/dist/esm/ir.mjs +12 -0
  33. package/dist/esm/ir.mjs.map +1 -0
  34. package/dist/esm/lower.mjs +202 -0
  35. package/dist/esm/lower.mjs.map +1 -0
  36. package/dist/esm/materialize.mjs +179 -0
  37. package/dist/esm/materialize.mjs.map +1 -0
  38. package/dist/esm/normalize.mjs +596 -0
  39. package/dist/esm/normalize.mjs.map +1 -0
  40. package/dist/esm/output.mjs +119 -0
  41. package/dist/esm/output.mjs.map +1 -0
  42. package/dist/esm/planCache.mjs +194 -0
  43. package/dist/esm/planCache.mjs.map +1 -0
  44. package/dist/esm/session.mjs +99 -0
  45. package/dist/esm/session.mjs.map +1 -0
  46. package/dist/esm/yuku.mjs +136 -0
  47. package/dist/esm/yuku.mjs.map +1 -0
  48. package/dist/esm/zero.mjs +352 -0
  49. package/dist/esm/zero.mjs.map +1 -0
  50. package/package.json +42 -4
  51. package/src/ast.ts +86 -0
  52. package/src/contracts.ts +148 -0
  53. package/src/diagnostics.ts +96 -0
  54. package/src/evaluate.ts +497 -0
  55. package/src/graph.ts +347 -0
  56. package/src/hash.ts +36 -0
  57. package/src/index.ts +15 -0
  58. package/src/ir.ts +154 -0
  59. package/src/lower.ts +418 -0
  60. package/src/materialize.ts +330 -0
  61. package/src/normalize.ts +846 -0
  62. package/src/output.ts +175 -0
  63. package/src/planCache.ts +327 -0
  64. package/src/session.ts +172 -0
  65. package/src/yuku.ts +189 -0
  66. package/src/zero.ts +598 -0
  67. package/types/ast.d.ts +10 -0
  68. package/types/ast.d.ts.map +1 -0
  69. package/types/contracts.d.ts +97 -0
  70. package/types/contracts.d.ts.map +1 -0
  71. package/types/diagnostics.d.ts +31 -0
  72. package/types/diagnostics.d.ts.map +1 -0
  73. package/types/evaluate.d.ts +36 -0
  74. package/types/evaluate.d.ts.map +1 -0
  75. package/types/graph.d.ts +37 -0
  76. package/types/graph.d.ts.map +1 -0
  77. package/types/hash.d.ts +8 -0
  78. package/types/hash.d.ts.map +1 -0
  79. package/types/index.d.ts +16 -0
  80. package/types/index.d.ts.map +1 -0
  81. package/types/ir.d.ts +118 -0
  82. package/types/ir.d.ts.map +1 -0
  83. package/types/lower.d.ts +99 -0
  84. package/types/lower.d.ts.map +1 -0
  85. package/types/materialize.d.ts +104 -0
  86. package/types/materialize.d.ts.map +1 -0
  87. package/types/normalize.d.ts +8 -0
  88. package/types/normalize.d.ts.map +1 -0
  89. package/types/output.d.ts +32 -0
  90. package/types/output.d.ts.map +1 -0
  91. package/types/planCache.d.ts +113 -0
  92. package/types/planCache.d.ts.map +1 -0
  93. package/types/session.d.ts +44 -0
  94. package/types/session.d.ts.map +1 -0
  95. package/types/yuku.d.ts +4 -0
  96. package/types/yuku.d.ts.map +1 -0
  97. package/types/zero.d.ts +98 -0
  98. package/types/zero.d.ts.map +1 -0
package/src/zero.ts ADDED
@@ -0,0 +1,598 @@
1
+ import { childNode, childNodes, walkAst } from './ast'
2
+ import type { AstNode, ResolvedModuleId, SourceSpan } from './contracts'
3
+ import type { BailoutReason, ZeroRule } from './diagnostics'
4
+ import { zeroRuleForBailout } from './diagnostics'
5
+ import type { LoweredModulePlan } from './lower'
6
+ import type { SourceEdit } from './output'
7
+ import { parseModuleAst } from './yuku'
8
+
9
+ /**
10
+ * Zero-runtime reference erasure and the rule-mapped diagnostics that gate it.
11
+ *
12
+ * A passing lowering plan already proved every Tamagui use in this module became
13
+ * host markup. That fact is stronger than anything a bundler can derive, so the
14
+ * compiler removes the now-dead references before the bundler records this
15
+ * module's dependencies. Metro fixes its dependency graph at resolution time and
16
+ * does no export-level shaking, so nothing later in the pipeline can do this.
17
+ */
18
+
19
+ export type { ZeroRule }
20
+
21
+ export type ZeroViolationCode =
22
+ | 'zero/static-island-import'
23
+ | 'zero/side-effect-import'
24
+ | 'zero/live-tamagui-reference'
25
+ | 'zero/design-state-read'
26
+ | 'zero/runtime-provider'
27
+ | BailoutReason['code']
28
+
29
+ export interface ZeroViolation {
30
+ rule: ZeroRule
31
+ code: ZeroViolationCode
32
+ span: SourceSpan
33
+ message: string
34
+ component?: string
35
+ }
36
+
37
+ /** The last line of every zero-runtime failure, in both gates. */
38
+ export const ZERO_FAILURE_FOOTER =
39
+ 'Fix every site or move the owning module to a declared full-runtime island. Zero-runtime never retains one component as a fallback.'
40
+
41
+ /**
42
+ * The JavaScript design-state surface. A reference to any of these in a zero
43
+ * graph is rule 7: the value only exists once a runtime has parsed the config.
44
+ */
45
+ export const ZERO_DESIGN_STATE_APIS = new Set([
46
+ 'useMedia',
47
+ 'useTheme',
48
+ 'useThemeName',
49
+ 'useProps',
50
+ 'usePropsAndStyle',
51
+ 'getConfig',
52
+ 'getTokens',
53
+ 'useTokens',
54
+ 'getVariableValue',
55
+ 'getToken',
56
+ 'getTokenValue',
57
+ 'useConfiguration',
58
+ 'useAnimationDriver',
59
+ 'updateTheme',
60
+ 'addTheme',
61
+ 'replaceTheme',
62
+ 'forceUpdateThemes',
63
+ ])
64
+
65
+ /** Root providers. Their use is illegal in a zero graph, never erasable. */
66
+ export const ZERO_PROVIDER_EXPORTS = new Set([
67
+ 'TamaguiProvider',
68
+ 'ThemeProvider',
69
+ 'TamaguiRoot',
70
+ ])
71
+
72
+ /**
73
+ * The four public animated-number hooks. They are the one opt-in runtime a zero
74
+ * graph may keep, and only through the leaf module: the public barrel would drag
75
+ * the config-bound driver resolution in with them.
76
+ */
77
+ export const ZERO_ANIMATED_NUMBER_HOOKS = new Set([
78
+ 'useAnimatedNumber',
79
+ 'useAnimatedNumberStyle',
80
+ 'useAnimatedNumbersStyle',
81
+ 'useAnimatedNumberReaction',
82
+ ])
83
+
84
+ export const ZERO_ANIMATED_NUMBER_MODULE = '@tamagui/animations-css/animated-number'
85
+
86
+ export interface ZeroRuleParams {
87
+ component?: string
88
+ expression?: string
89
+ prop?: string
90
+ detail?: string
91
+ api?: string
92
+ /** Rule 8 only: what to do about this import, which differs per code. */
93
+ remediation?: string
94
+ }
95
+
96
+ /** The rule map's developer messages, verbatim. */
97
+ export function zeroRuleMessage(rule: ZeroRule, params: ZeroRuleParams): string {
98
+ const component = params.component ?? 'this component'
99
+ switch (rule) {
100
+ case 1:
101
+ return `Zero-runtime rule 1: ${component} cannot receive a prop spread because the compiler cannot prove it is style-free. Pass non-style props explicitly or move this module to a full-runtime island.`
102
+ case 2:
103
+ return `Zero-runtime rule 2: component expression ${params.expression ?? component} does not resolve to one literal lowerable host component. Use a literal Tamagui or html.* component, or move this module to a full-runtime island.`
104
+ case 3:
105
+ return `Zero-runtime rule 3: value for ${params.prop ?? 'a style prop'} on ${component} cannot be lowered: ${params.detail ?? 'the compiler could not evaluate it'}. Use a supported build-time value or move this module to a full-runtime island.`
106
+ case 4:
107
+ return `Zero-runtime rule 4: ${params.detail ?? component} requires runtime theme or config state. Theme names and modifier targets must be statically enumerable, Theme value props and config must be build-time data, and runtime mutation belongs in a full-runtime island.`
108
+ case 5:
109
+ return `Zero-runtime rule 5: ${params.detail ?? component} requires a component animation runtime. Use a static CSS transition or move this module to a full-runtime island.`
110
+ case 6:
111
+ return `Zero-runtime rule 6: ${component} does not lower to one host element with className and is island-only. Move this module to a declared full-runtime island.`
112
+ case 7:
113
+ return `Zero-runtime rule 7: ${params.api ?? component} reads Tamagui design state in JavaScript. Express the condition in CSS or move this module to a full-runtime island.`
114
+ case 8:
115
+ return `Zero-runtime rule 8: ${params.detail ?? 'a module-level import'} defeats the zero-runtime graph. ${params.remediation ?? ''}`.trimEnd()
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Rule 8's two codes.
121
+ *
122
+ * They are module-level imports rather than element sites, and neither one is
123
+ * fixed by moving the module to an island: a stray side-effect import wants
124
+ * deleting, and an island import wants the generated loader. Reporting them as
125
+ * rule 6 sent developers to do island work that would not have helped.
126
+ */
127
+ export function zeroSideEffectImportMessage(specifier: string): string {
128
+ return zeroRuleMessage(8, {
129
+ detail: `the bare side-effect import of "${specifier}", whose effects zero-runtime cannot prove or erase,`,
130
+ remediation: `Remove it, or import the values this module uses so the compiler can lower and erase them.`,
131
+ })
132
+ }
133
+
134
+ export function zeroStaticIslandImportMessage(
135
+ specifier: string,
136
+ islandId: string
137
+ ): string {
138
+ return zeroRuleMessage(8, {
139
+ detail: `the static import of "${specifier}", which is declared as full-runtime island "${islandId}",`,
140
+ remediation: `Import the generated island loader instead; the island is a separately built entry and the zero graph never contains it.`,
141
+ })
142
+ }
143
+
144
+ export const ZERO_PROVIDER_MESSAGE =
145
+ '[tamagui zero-runtime] Rule 4: TamaguiProvider is not used by a zero-runtime root. The bundler loads generated CSS and the compiler lowers static Theme nodes. Remove this provider or make this entry full-runtime.'
146
+
147
+ export function zeroThemeBoundaryMessage(component: string, prop: string): string {
148
+ return `[tamagui zero-runtime] Rule 4: ${component} uses ${prop}, which creates a runtime component theme boundary. Replace it with a static <Theme name="..."> wrapper (use name="inverse" for themeInverse) or move this module to a full-runtime island.`
149
+ }
150
+
151
+ export function zeroConfigDriverMessage(name: string, outputStyle: unknown): string {
152
+ return `[tamagui zero-runtime] Rule 5: createTamagui animations must resolve to the CSS driver. Driver ${name} has outputStyle=${String(outputStyle)}. Remove it from the zero entry or move its consumers to a full-runtime island.`
153
+ }
154
+
155
+ export function zeroIslandThemeMessage(entry: string): string {
156
+ return `[tamagui zero-runtime] Rule 4: island ${entry} has no statically resolved theme context at this mount. Place the island boundary under a compiler-visible static <Theme> or make this entry full-runtime.`
157
+ }
158
+
159
+ /**
160
+ * The lowering plan's own diagnostics, read as zero-runtime violations.
161
+ *
162
+ * Only the diagnostics that stopped a candidate from lowering count: one
163
+ * recorded next to a successful lowering describes a dropped prop and leaves no
164
+ * runtime behind.
165
+ */
166
+ export function zeroViolationsFromPlan(plan: LoweredModulePlan): ZeroViolation[] {
167
+ const violations: ZeroViolation[] = []
168
+ for (const diagnostic of plan.diagnostics) {
169
+ if (!diagnostic.blocking) continue
170
+ const rule = zeroRuleForBailout(diagnostic)
171
+ violations.push({
172
+ rule,
173
+ code: diagnostic.code,
174
+ span: diagnostic.span,
175
+ component: diagnostic.component,
176
+ message:
177
+ diagnostic.zeroMessage ??
178
+ zeroRuleMessage(rule, {
179
+ component: diagnostic.component,
180
+ prop: diagnostic.prop,
181
+ detail: diagnostic.message,
182
+ }),
183
+ })
184
+ }
185
+ return violations
186
+ }
187
+
188
+ export interface ZeroErasureInput {
189
+ id: ResolvedModuleId
190
+ source: string
191
+ /** Edits already committed by lowering. A reference inside one is consumed. */
192
+ loweredEdits: readonly SourceEdit[]
193
+ /** True when this import specifier names the zero-forbidden Tamagui surface. */
194
+ isTamaguiSpecifier(specifier: string): boolean
195
+ /** Returns the island id when this specifier names a declared island module. */
196
+ islandIdFor(specifier: string): string | null
197
+ }
198
+
199
+ export interface ZeroErasureResult {
200
+ edits: SourceEdit[]
201
+ removedModules: string[]
202
+ removedBindings: string[]
203
+ erasedStyledDefinitions: string[]
204
+ /**
205
+ * Erased declarators this module also exported. The build-wide gate proves
206
+ * every importer of them inside the zero entry graph was itself transformed.
207
+ */
208
+ erasedExports: string[]
209
+ /** Hooks rewritten to the animated-number leaf, by their imported name. */
210
+ rewrittenAnimatedNumberHooks: string[]
211
+ liveBindings: string[]
212
+ violations: ZeroViolation[]
213
+ }
214
+
215
+ interface Range {
216
+ start: number
217
+ end: number
218
+ }
219
+
220
+ interface Occurrence extends Range {
221
+ parent: AstNode | null
222
+ }
223
+
224
+ interface ImportBinding {
225
+ local: string
226
+ imported: string
227
+ specifierNode: AstNode
228
+ declaration: AstNode
229
+ moduleSpecifier: string
230
+ }
231
+
232
+ /**
233
+ * Every value-position occurrence of each identifier name in the module,
234
+ * excluding binding sites and non-reference positions such as member property
235
+ * names, object keys, and JSX attribute names.
236
+ */
237
+ function collectNameOccurrences(program: AstNode): Map<string, Occurrence[]> {
238
+ const occurrences = new Map<string, Occurrence[]>()
239
+ walkAst(program, (node, parent, key) => {
240
+ if (node.type !== 'Identifier' && node.type !== 'JSXIdentifier') return
241
+ const name = typeof node.name === 'string' ? node.name : null
242
+ if (!name) return
243
+ if (parent) {
244
+ // binding sites, not references
245
+ if (
246
+ parent.type === 'ImportSpecifier' ||
247
+ parent.type === 'ImportDefaultSpecifier' ||
248
+ parent.type === 'ImportNamespaceSpecifier' ||
249
+ parent.type === 'ExportSpecifier'
250
+ ) {
251
+ return
252
+ }
253
+ // property names, not references
254
+ if (
255
+ (parent.type === 'MemberExpression' ||
256
+ parent.type === 'JSXMemberExpression' ||
257
+ parent.type === 'OptionalMemberExpression') &&
258
+ key === 'property' &&
259
+ !parent.computed
260
+ ) {
261
+ return
262
+ }
263
+ if (
264
+ (parent.type === 'Property' || parent.type === 'PropertyDefinition') &&
265
+ key === 'key' &&
266
+ !parent.computed
267
+ ) {
268
+ return
269
+ }
270
+ if (parent.type === 'JSXAttribute' && key === 'name') return
271
+ // declaration binding sites, not references
272
+ if (
273
+ (parent.type === 'VariableDeclarator' ||
274
+ parent.type === 'FunctionDeclaration' ||
275
+ parent.type === 'ClassDeclaration') &&
276
+ key === 'id'
277
+ ) {
278
+ return
279
+ }
280
+ }
281
+ const entry: Occurrence = { start: node.start, end: node.end, parent: parent ?? null }
282
+ const list = occurrences.get(name)
283
+ if (list) list.push(entry)
284
+ else occurrences.set(name, [entry])
285
+ })
286
+ return occurrences
287
+ }
288
+
289
+ function coversRange(outer: Range, inner: Range): boolean {
290
+ return outer.start <= inner.start && outer.end >= inner.end
291
+ }
292
+
293
+ function stringValueOf(node: AstNode | null): string | null {
294
+ return node && typeof node.value === 'string' ? node.value : null
295
+ }
296
+
297
+ function identifierNameOf(node: AstNode | null): string | null {
298
+ return node && node.type === 'Identifier' && typeof node.name === 'string'
299
+ ? node.name
300
+ : null
301
+ }
302
+
303
+ /**
304
+ * The smallest source text that names what a surviving reference is doing. A
305
+ * bare identifier says nothing about why it could not lower, so the enclosing
306
+ * expression is reported when there is one.
307
+ */
308
+ function referenceExpression(source: string, reference: Occurrence): string {
309
+ const parent = reference.parent
310
+ const node =
311
+ parent &&
312
+ (parent.type === 'ConditionalExpression' ||
313
+ parent.type === 'LogicalExpression' ||
314
+ parent.type === 'CallExpression' ||
315
+ parent.type === 'MemberExpression' ||
316
+ parent.type === 'ArrayExpression')
317
+ ? parent
318
+ : reference
319
+ const text = source.slice(node.start, node.end).replace(/\s+/g, ' ').trim()
320
+ return text.length > 80 ? `${text.slice(0, 77)}...` : text
321
+ }
322
+
323
+ /**
324
+ * Declarators of the form `const Card = styled(Base, {...})` at module scope.
325
+ * The styled binding must come from the Tamagui surface; anything else with that
326
+ * name is an unrelated local function.
327
+ */
328
+ function collectStyledDeclarators(
329
+ program: AstNode,
330
+ styledLocals: ReadonlySet<string>
331
+ ): { name: string; declarator: AstNode; declaration: AstNode; exported: boolean }[] {
332
+ const found: {
333
+ name: string
334
+ declarator: AstNode
335
+ declaration: AstNode
336
+ exported: boolean
337
+ }[] = []
338
+ for (const statement of childNodes(program, 'body')) {
339
+ const exported = statement.type === 'ExportNamedDeclaration'
340
+ const declaration = exported ? childNode(statement, 'declaration') : statement
341
+ if (!declaration || declaration.type !== 'VariableDeclaration') continue
342
+ const declarators = childNodes(declaration, 'declarations')
343
+ if (declarators.length !== 1) continue
344
+ const declarator = declarators[0]!
345
+ const name = identifierNameOf(childNode(declarator, 'id'))
346
+ const init = childNode(declarator, 'init')
347
+ if (!name || !init || init.type !== 'CallExpression') continue
348
+ const callee = identifierNameOf(childNode(init, 'callee'))
349
+ if (!callee || !styledLocals.has(callee)) continue
350
+ // an exported declarator's whole statement carries the export keyword
351
+ found.push({
352
+ name,
353
+ declarator,
354
+ declaration: exported ? statement : declaration,
355
+ exported,
356
+ })
357
+ }
358
+ return found
359
+ }
360
+
361
+ export function planZeroErasure(input: ZeroErasureInput): ZeroErasureResult {
362
+ const { id, source, loweredEdits } = input
363
+ const program = parseModuleAst(source, id)
364
+
365
+ const violations: ZeroViolation[] = []
366
+ const span = (node: Range): SourceSpan => ({ id, start: node.start, end: node.end })
367
+
368
+ const bindings: ImportBinding[] = []
369
+ const tamaguiDeclarations: AstNode[] = []
370
+
371
+ for (const statement of childNodes(program, 'body')) {
372
+ if (statement.type !== 'ImportDeclaration') continue
373
+ if (statement.importKind === 'type') continue
374
+ const moduleSpecifier = stringValueOf(childNode(statement, 'source'))
375
+ if (!moduleSpecifier) continue
376
+
377
+ const islandId = input.islandIdFor(moduleSpecifier)
378
+ if (islandId) {
379
+ violations.push({
380
+ rule: 8,
381
+ code: 'zero/static-island-import',
382
+ span: span(statement),
383
+ message: zeroStaticIslandImportMessage(moduleSpecifier, islandId),
384
+ })
385
+ continue
386
+ }
387
+
388
+ if (!input.isTamaguiSpecifier(moduleSpecifier)) continue
389
+ tamaguiDeclarations.push(statement)
390
+
391
+ const specifiers = childNodes(statement, 'specifiers')
392
+ if (specifiers.length === 0) {
393
+ violations.push({
394
+ rule: 8,
395
+ code: 'zero/side-effect-import',
396
+ span: span(statement),
397
+ message: zeroSideEffectImportMessage(moduleSpecifier),
398
+ })
399
+ continue
400
+ }
401
+ for (const specifier of specifiers) {
402
+ if (specifier.importKind === 'type') continue
403
+ const local = identifierNameOf(childNode(specifier, 'local'))
404
+ if (!local) continue
405
+ bindings.push({
406
+ local,
407
+ imported: identifierNameOf(childNode(specifier, 'imported')) ?? local,
408
+ specifierNode: specifier,
409
+ declaration: statement,
410
+ moduleSpecifier,
411
+ })
412
+ }
413
+ }
414
+
415
+ if (bindings.length === 0) {
416
+ return {
417
+ edits: [],
418
+ removedModules: [],
419
+ removedBindings: [],
420
+ erasedStyledDefinitions: [],
421
+ erasedExports: [],
422
+ rewrittenAnimatedNumberHooks: [],
423
+ liveBindings: [],
424
+ violations,
425
+ }
426
+ }
427
+
428
+ // A reference is consumed when lowering overwrote the source range holding it.
429
+ const consumed: Range[] = loweredEdits
430
+ .filter((edit) => edit.end > edit.start)
431
+ .map((edit) => ({ start: edit.start, end: edit.end }))
432
+
433
+ // Name occurrences, collected in one walk. This deliberately over-counts a
434
+ // shadowed name: over-counting keeps an import that could have been dropped,
435
+ // while under-counting would erase a live one and break the app at runtime.
436
+ const occurrences = collectNameOccurrences(program)
437
+ const referencesOf = (local: string): Occurrence[] => occurrences.get(local) ?? []
438
+
439
+ const isConsumed = (reference: Range) =>
440
+ consumed.some((range) => coversRange(range, reference))
441
+
442
+ // Pass 1: erase app-local `styled()` declarators whose binding lowering fully
443
+ // consumed. Their spans then count as consumed for the import pass, which is
444
+ // what lets `styled` and its base component drop with them.
445
+ const styledLocals = new Set(
446
+ bindings.filter((binding) => binding.imported === 'styled').map((b) => b.local)
447
+ )
448
+ const erasedStyledDefinitions: string[] = []
449
+ const erasedExports: string[] = []
450
+ const styledEdits: SourceEdit[] = []
451
+ for (const definition of collectStyledDeclarators(program, styledLocals)) {
452
+ const references = referencesOf(definition.name)
453
+ if (references.length === 0) continue
454
+ if (!references.every(isConsumed)) continue
455
+ styledEdits.push({
456
+ start: definition.declaration.start,
457
+ end: definition.declaration.end,
458
+ content: '',
459
+ origin: span(definition.declaration),
460
+ })
461
+ consumed.push({
462
+ start: definition.declaration.start,
463
+ end: definition.declaration.end,
464
+ })
465
+ erasedStyledDefinitions.push(definition.name)
466
+ if (definition.exported) erasedExports.push(definition.name)
467
+ }
468
+
469
+ // Pass 2: the animated-number hooks are rewritten to the leaf before any dead
470
+ // specifier is dropped, so the public barrel never enters the client graph for
471
+ // them. They are the one runtime a zero entry may keep.
472
+ const animatedNumberBindings = bindings.filter(
473
+ (binding) =>
474
+ ZERO_ANIMATED_NUMBER_HOOKS.has(binding.imported) &&
475
+ referencesOf(binding.local).some((reference) => !isConsumed(reference))
476
+ )
477
+ const rewrittenAnimatedNumberHooks = animatedNumberBindings.map(
478
+ (binding) => binding.imported
479
+ )
480
+ const animatedNumberSpecifiers = new Set(
481
+ animatedNumberBindings.map((binding) => binding.specifierNode)
482
+ )
483
+ const leafImportEdits: SourceEdit[] = []
484
+ for (const declaration of tamaguiDeclarations) {
485
+ const rewritten = animatedNumberBindings.filter(
486
+ (binding) => binding.declaration === declaration
487
+ )
488
+ if (rewritten.length === 0) continue
489
+ leafImportEdits.push({
490
+ start: declaration.start,
491
+ end: declaration.start,
492
+ content: `import { ${rewritten
493
+ .map((binding) =>
494
+ binding.imported === binding.local
495
+ ? binding.imported
496
+ : `${binding.imported} as ${binding.local}`
497
+ )
498
+ .join(', ')} } from ${JSON.stringify(ZERO_ANIMATED_NUMBER_MODULE)};\n`,
499
+ origin: span(declaration),
500
+ })
501
+ }
502
+
503
+ // Pass 3: drop every Tamagui import specifier with no surviving reference, and
504
+ // the whole declaration when none survives.
505
+ const deadBindings = new Set<AstNode>(animatedNumberSpecifiers)
506
+ const removedBindings: string[] = []
507
+ const liveBindings: ImportBinding[] = []
508
+ for (const binding of bindings) {
509
+ if (animatedNumberSpecifiers.has(binding.specifierNode)) continue
510
+ const references = referencesOf(binding.local)
511
+ if (references.length > 0 && !references.every(isConsumed)) {
512
+ liveBindings.push(binding)
513
+ continue
514
+ }
515
+ deadBindings.add(binding.specifierNode)
516
+ removedBindings.push(binding.local)
517
+ }
518
+
519
+ const edits: SourceEdit[] = [...styledEdits, ...leafImportEdits]
520
+ const removedModules: string[] = []
521
+ for (const declaration of tamaguiDeclarations) {
522
+ const specifiers = childNodes(declaration, 'specifiers').filter(
523
+ (specifier) => specifier.importKind !== 'type'
524
+ )
525
+ if (specifiers.length === 0) continue
526
+ const dead = specifiers.filter((specifier) => deadBindings.has(specifier))
527
+ if (dead.length === 0) continue
528
+ if (dead.length === specifiers.length) {
529
+ edits.push({
530
+ start: declaration.start,
531
+ end: declaration.end,
532
+ content: '',
533
+ origin: span(declaration),
534
+ })
535
+ removedModules.push(stringValueOf(childNode(declaration, 'source'))!)
536
+ continue
537
+ }
538
+ // Partial removal keeps the declaration, so rewrite the specifier list once
539
+ // rather than surgically deleting commas.
540
+ const survivors = specifiers.filter((specifier) => !deadBindings.has(specifier))
541
+ const braceStart = specifiers[0]!.start
542
+ const braceEnd = specifiers[specifiers.length - 1]!.end
543
+ edits.push({
544
+ start: braceStart,
545
+ end: braceEnd,
546
+ content: survivors
547
+ .map((specifier) => source.slice(specifier.start, specifier.end))
548
+ .join(', '),
549
+ origin: span({ start: braceStart, end: braceEnd }),
550
+ })
551
+ }
552
+
553
+ // A binding lowering could not consume is a retained runtime. Which rule it is
554
+ // depends on what the binding names, so the message names the API or the
555
+ // expression rather than the import.
556
+ for (const binding of liveBindings) {
557
+ const live = referencesOf(binding.local).filter(
558
+ (reference) => !isConsumed(reference)
559
+ )[0]!
560
+ if (ZERO_DESIGN_STATE_APIS.has(binding.imported)) {
561
+ violations.push({
562
+ rule: 7,
563
+ code: 'zero/design-state-read',
564
+ span: span(live),
565
+ message: zeroRuleMessage(7, { api: binding.imported }),
566
+ })
567
+ continue
568
+ }
569
+ if (ZERO_PROVIDER_EXPORTS.has(binding.imported)) {
570
+ violations.push({
571
+ rule: 4,
572
+ code: 'zero/runtime-provider',
573
+ span: span(live),
574
+ component: binding.imported,
575
+ message: ZERO_PROVIDER_MESSAGE,
576
+ })
577
+ continue
578
+ }
579
+ violations.push({
580
+ rule: 2,
581
+ code: 'zero/live-tamagui-reference',
582
+ span: span(live),
583
+ component: binding.local,
584
+ message: zeroRuleMessage(2, { expression: referenceExpression(source, live) }),
585
+ })
586
+ }
587
+
588
+ return {
589
+ edits,
590
+ removedModules,
591
+ removedBindings,
592
+ erasedStyledDefinitions,
593
+ erasedExports,
594
+ rewrittenAnimatedNumberHooks,
595
+ liveBindings: liveBindings.map((binding) => binding.local),
596
+ violations,
597
+ }
598
+ }
package/types/ast.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { AstNode } from './contracts';
2
+ export declare function isAstNode(value: unknown): value is AstNode;
3
+ export declare function walkAst(node: AstNode, visitor: (node: AstNode, parent: AstNode | null, key: string | null) => void, parent?: AstNode | null, key?: string | null): void;
4
+ export declare function findAstNode(root: AstNode, predicate: (node: AstNode, parent: AstNode | null, key: string | null) => boolean): AstNode | null;
5
+ export declare function childNode(node: AstNode, key: string): AstNode | null;
6
+ export declare function childNodes(node: AstNode, key: string): AstNode[];
7
+ export declare function identifierName(node: AstNode | null): string | null;
8
+ export declare function literalValue(node: AstNode | null): unknown;
9
+ export declare function unwrapExpression(node: AstNode): AstNode;
10
+ //# sourceMappingURL=ast.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../src/ast.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAI1C,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,OAAO,CAQ1D;AAED,wBAAgB,OAAO,CACrB,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,EAC5E,MAAM,GAAE,OAAO,GAAG,IAAW,EAC7B,GAAG,GAAE,MAAM,GAAG,IAAW,GACxB,IAAI,CAeN;AAED,wBAAgB,WAAW,CACzB,IAAI,EAAE,OAAO,EACb,SAAS,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,OAAO,GAChF,OAAO,GAAG,IAAI,CAQhB;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAGpE;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,EAAE,CAGhE;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAGlE;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,GAAG,OAAO,CAM1D;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAavD"}