@tamagui/compiler-core 0.0.0-bootstrap.0 → 3.0.0-beta.637.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.
- package/LICENSE +21 -0
- package/README.md +57 -1
- package/dist/cjs/ast.cjs +95 -0
- package/dist/cjs/contracts.cjs +62 -0
- package/dist/cjs/diagnostics.cjs +69 -0
- package/dist/cjs/evaluate.cjs +351 -0
- package/dist/cjs/graph.cjs +279 -0
- package/dist/cjs/hash.cjs +49 -0
- package/dist/cjs/index.cjs +44 -0
- package/dist/cjs/ir.cjs +33 -0
- package/dist/cjs/lower.cjs +225 -0
- package/dist/cjs/materialize.cjs +200 -0
- package/dist/cjs/normalize.cjs +622 -0
- package/dist/cjs/output.cjs +149 -0
- package/dist/cjs/planCache.cjs +224 -0
- package/dist/cjs/session.cjs +119 -0
- package/dist/cjs/yuku.cjs +159 -0
- package/dist/cjs/zero.cjs +387 -0
- package/dist/esm/ast.mjs +65 -0
- package/dist/esm/ast.mjs.map +1 -0
- package/dist/esm/contracts.mjs +34 -0
- package/dist/esm/contracts.mjs.map +1 -0
- package/dist/esm/diagnostics.mjs +44 -0
- package/dist/esm/diagnostics.mjs.map +1 -0
- package/dist/esm/evaluate.mjs +327 -0
- package/dist/esm/evaluate.mjs.map +1 -0
- package/dist/esm/graph.mjs +256 -0
- package/dist/esm/graph.mjs.map +1 -0
- package/dist/esm/hash.mjs +26 -0
- package/dist/esm/hash.mjs.map +1 -0
- package/dist/esm/index.mjs +30 -0
- package/dist/esm/ir.mjs +12 -0
- package/dist/esm/ir.mjs.map +1 -0
- package/dist/esm/lower.mjs +202 -0
- package/dist/esm/lower.mjs.map +1 -0
- package/dist/esm/materialize.mjs +179 -0
- package/dist/esm/materialize.mjs.map +1 -0
- package/dist/esm/normalize.mjs +596 -0
- package/dist/esm/normalize.mjs.map +1 -0
- package/dist/esm/output.mjs +119 -0
- package/dist/esm/output.mjs.map +1 -0
- package/dist/esm/planCache.mjs +194 -0
- package/dist/esm/planCache.mjs.map +1 -0
- package/dist/esm/session.mjs +99 -0
- package/dist/esm/session.mjs.map +1 -0
- package/dist/esm/yuku.mjs +136 -0
- package/dist/esm/yuku.mjs.map +1 -0
- package/dist/esm/zero.mjs +352 -0
- package/dist/esm/zero.mjs.map +1 -0
- package/package.json +42 -4
- package/src/ast.ts +86 -0
- package/src/contracts.ts +148 -0
- package/src/diagnostics.ts +96 -0
- package/src/evaluate.ts +497 -0
- package/src/graph.ts +347 -0
- package/src/hash.ts +36 -0
- package/src/index.ts +15 -0
- package/src/ir.ts +154 -0
- package/src/lower.ts +418 -0
- package/src/materialize.ts +330 -0
- package/src/normalize.ts +846 -0
- package/src/output.ts +175 -0
- package/src/planCache.ts +327 -0
- package/src/session.ts +172 -0
- package/src/yuku.ts +189 -0
- package/src/zero.ts +598 -0
- package/types/ast.d.ts +10 -0
- package/types/ast.d.ts.map +1 -0
- package/types/contracts.d.ts +97 -0
- package/types/contracts.d.ts.map +1 -0
- package/types/diagnostics.d.ts +31 -0
- package/types/diagnostics.d.ts.map +1 -0
- package/types/evaluate.d.ts +36 -0
- package/types/evaluate.d.ts.map +1 -0
- package/types/graph.d.ts +37 -0
- package/types/graph.d.ts.map +1 -0
- package/types/hash.d.ts +8 -0
- package/types/hash.d.ts.map +1 -0
- package/types/index.d.ts +16 -0
- package/types/index.d.ts.map +1 -0
- package/types/ir.d.ts +118 -0
- package/types/ir.d.ts.map +1 -0
- package/types/lower.d.ts +99 -0
- package/types/lower.d.ts.map +1 -0
- package/types/materialize.d.ts +104 -0
- package/types/materialize.d.ts.map +1 -0
- package/types/normalize.d.ts +8 -0
- package/types/normalize.d.ts.map +1 -0
- package/types/output.d.ts +32 -0
- package/types/output.d.ts.map +1 -0
- package/types/planCache.d.ts +113 -0
- package/types/planCache.d.ts.map +1 -0
- package/types/session.d.ts +44 -0
- package/types/session.d.ts.map +1 -0
- package/types/yuku.d.ts +4 -0
- package/types/yuku.d.ts.map +1 -0
- package/types/zero.d.ts +98 -0
- 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"}
|