@tamagui/codemod-flat-values 0.0.0-bootstrap.0 → 3.0.0-beta.1093.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/README.md +234 -1
- package/dist/builtInNames.mjs +14 -0
- package/dist/builtInNames.mjs.map +1 -0
- package/dist/containers.mjs +141 -0
- package/dist/containers.mjs.map +1 -0
- package/dist/convert.mjs +1233 -0
- package/dist/convert.mjs.map +1 -0
- package/dist/expressions.mjs +188 -0
- package/dist/expressions.mjs.map +1 -0
- package/dist/functionalVariants.mjs +469 -0
- package/dist/functionalVariants.mjs.map +1 -0
- package/dist/grammar.mjs +73 -0
- package/dist/grammar.mjs.map +1 -0
- package/dist/index.mjs +374 -0
- package/dist/index.mjs.map +1 -0
- package/dist/legacyConditions.mjs +293 -0
- package/dist/legacyConditions.mjs.map +1 -0
- package/dist/legacyNames.mjs +130 -0
- package/dist/legacyNames.mjs.map +1 -0
- package/dist/provenance.mjs +137 -0
- package/dist/provenance.mjs.map +1 -0
- package/dist/report.mjs +188 -0
- package/dist/report.mjs.map +1 -0
- package/dist/sheetAnatomy.mjs +200 -0
- package/dist/sheetAnatomy.mjs.map +1 -0
- package/dist/structuredNative.mjs +275 -0
- package/dist/structuredNative.mjs.map +1 -0
- package/dist/transition.mjs +257 -0
- package/dist/transition.mjs.map +1 -0
- package/package.json +35 -7
- package/src/builtInNames.ts +23 -0
- package/src/containers.ts +227 -0
- package/src/convert.ts +1977 -0
- package/src/expressions.ts +220 -0
- package/src/functionalVariants.ts +642 -0
- package/src/grammar.ts +190 -0
- package/src/index.ts +589 -0
- package/src/legacyConditions.ts +357 -0
- package/src/legacyNames.ts +160 -0
- package/src/provenance.ts +210 -0
- package/src/report.ts +362 -0
- package/src/sheetAnatomy.ts +277 -0
- package/src/structuredNative.ts +459 -0
- package/src/transition.ts +415 -0
package/package.json
CHANGED
|
@@ -1,16 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tamagui/codemod-flat-values",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Tamagui v3 package bootstrap",
|
|
5
|
-
"repository": {
|
|
6
|
-
"type": "git",
|
|
7
|
-
"url": "git+https://github.com/tamagui/tamagui.git"
|
|
8
|
-
},
|
|
3
|
+
"version": "3.0.0-beta.1093.1",
|
|
9
4
|
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"tamagui-codemod-flat-values": "dist/index.mjs"
|
|
8
|
+
},
|
|
9
|
+
"source": "src/index.ts",
|
|
10
10
|
"files": [
|
|
11
|
-
"
|
|
11
|
+
"src",
|
|
12
|
+
"dist"
|
|
12
13
|
],
|
|
13
14
|
"publishConfig": {
|
|
14
15
|
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tamagui-build --skip-native --skip-types",
|
|
19
|
+
"watch": "bun run build --watch",
|
|
20
|
+
"clean": "tamagui-build clean",
|
|
21
|
+
"clean:build": "tamagui-build clean:build",
|
|
22
|
+
"dry-run": "cd ../../.. && bun code/core/codemod-flat-values/src/index.ts --report code/core/codemod-flat-values/dry-run-report.md code/kitchen-sink/src/usecases code/ui/tamagui/src/components/Button.tsx",
|
|
23
|
+
"write": "cd ../../.. && bun code/core/codemod-flat-values/src/index.ts --write --report code/core/codemod-flat-values/dry-run-report.md code/kitchen-sink/src/usecases code/ui/tamagui/src/components/Button.tsx",
|
|
24
|
+
"test": "bun test --timeout 20000 test",
|
|
25
|
+
"test:web": "bun test --timeout 20000 test",
|
|
26
|
+
"typecheck": "tsc --noEmit"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@tamagui/helpers": "3.0.0-beta.1093.1",
|
|
30
|
+
"@tamagui/language-service": "3.0.0-beta.1093.1",
|
|
31
|
+
"@tamagui/shorthands": "3.0.0-beta.1093.1",
|
|
32
|
+
"@tamagui/style-grammar": "3.0.0-beta.1093.1",
|
|
33
|
+
"ts-morph": "^28.0.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@tamagui/build": "3.0.0-beta.1093.1",
|
|
37
|
+
"typescript": "~6.0.3"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/tamagui/tamagui.git",
|
|
42
|
+
"directory": "code/core/codemod-flat-values"
|
|
15
43
|
}
|
|
16
44
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { v6ThemeNameReplacements } from '@tamagui/style-grammar/tooling'
|
|
2
|
+
|
|
3
|
+
export const v6CodemodBuiltInNameReplacements = {
|
|
4
|
+
...v6ThemeNameReplacements,
|
|
5
|
+
// v3 removed backgroundActive after its component defaults had already
|
|
6
|
+
// stopped resolving; press is the corrected active-state default
|
|
7
|
+
backgroundActive: 'background-press',
|
|
8
|
+
// v3 configs no longer alias `true`; the default config pointed it at `4`
|
|
9
|
+
true: '4',
|
|
10
|
+
} as const
|
|
11
|
+
|
|
12
|
+
const builtInTokenPattern = new RegExp(
|
|
13
|
+
`\\$(${Object.keys(v6CodemodBuiltInNameReplacements).join('|')})(?![\\w-])`,
|
|
14
|
+
'g'
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
export function replaceV6BuiltInTokens(value: string): string {
|
|
18
|
+
return value.replace(
|
|
19
|
+
builtInTokenPattern,
|
|
20
|
+
(_, name: keyof typeof v6CodemodBuiltInNameReplacements) =>
|
|
21
|
+
`$${v6CodemodBuiltInNameReplacements[name]}`
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// V3 splits the group from the query container, so a legacy group condition that
|
|
2
|
+
// carries a container size (`$group-card-maxMd`) needs the element declaring the
|
|
3
|
+
// group to declare a query container too.
|
|
4
|
+
//
|
|
5
|
+
// Which element that is has to be proven, never inferred. Declaring a container
|
|
6
|
+
// changes containment and layout, so adding one to a `group="card"` in an
|
|
7
|
+
// unrelated tree is a behavior change with no v1 counterpart. Only two answers
|
|
8
|
+
// are allowed: the ancestor is visible in this file's JSX, or the site is flagged
|
|
9
|
+
// for a human.
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
Node,
|
|
13
|
+
SyntaxKind,
|
|
14
|
+
type JsxAttribute,
|
|
15
|
+
type JsxOpeningElement,
|
|
16
|
+
type JsxSelfClosingElement,
|
|
17
|
+
type PropertyAssignment,
|
|
18
|
+
type SourceFile,
|
|
19
|
+
} from 'ts-morph'
|
|
20
|
+
import type { Flag } from './convert'
|
|
21
|
+
import type { ModifierRegistryView } from './grammar'
|
|
22
|
+
import { isLegacyConditionName, resolveLegacyName } from './legacyNames'
|
|
23
|
+
|
|
24
|
+
/** the element or styled config that declares `group` */
|
|
25
|
+
type Declaration = JsxAttribute | PropertyAssignment
|
|
26
|
+
|
|
27
|
+
export interface ContainerTarget {
|
|
28
|
+
/** the group name the declaration carries, or the empty string for the unnamed group */
|
|
29
|
+
group: string
|
|
30
|
+
/** a consumer named this group, so the container needs a name to match */
|
|
31
|
+
named: boolean
|
|
32
|
+
/** set when the render-tree relationship could not be proven from the source */
|
|
33
|
+
flag: Flag | null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ContainerPlan {
|
|
37
|
+
/** keyed by the node declaring `group`, for the declarations that get a container */
|
|
38
|
+
targets: ReadonlyMap<Node, ContainerTarget>
|
|
39
|
+
/** keyed by the node holding the legacy container-size condition */
|
|
40
|
+
unresolved: ReadonlyMap<Node, Flag>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const emptyPlan: ContainerPlan = {
|
|
44
|
+
targets: new Map(),
|
|
45
|
+
unresolved: new Map(),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** the group name a declaration carries, or null when it is not statically known */
|
|
49
|
+
function declaredGroup(declaration: Declaration): string | null {
|
|
50
|
+
if (Node.isJsxAttribute(declaration)) {
|
|
51
|
+
const initializer = declaration.getInitializer()
|
|
52
|
+
// a bare `group` prop declares the unnamed group
|
|
53
|
+
if (initializer === undefined) return ''
|
|
54
|
+
if (Node.isStringLiteral(initializer)) return initializer.getLiteralValue()
|
|
55
|
+
if (!Node.isJsxExpression(initializer)) return null
|
|
56
|
+
const expression = initializer.getExpression()
|
|
57
|
+
if (expression === undefined) return null
|
|
58
|
+
if (
|
|
59
|
+
Node.isStringLiteral(expression) ||
|
|
60
|
+
Node.isNoSubstitutionTemplateLiteral(expression)
|
|
61
|
+
) {
|
|
62
|
+
return expression.getLiteralValue()
|
|
63
|
+
}
|
|
64
|
+
return expression.getKind() === SyntaxKind.TrueKeyword ? '' : null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const initializer = declaration.getInitializer()
|
|
68
|
+
if (initializer === undefined) return null
|
|
69
|
+
if (
|
|
70
|
+
Node.isStringLiteral(initializer) ||
|
|
71
|
+
Node.isNoSubstitutionTemplateLiteral(initializer)
|
|
72
|
+
) {
|
|
73
|
+
return initializer.getLiteralValue()
|
|
74
|
+
}
|
|
75
|
+
return initializer.getKind() === SyntaxKind.TrueKeyword ? '' : null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function groupAttribute(
|
|
79
|
+
opening: JsxOpeningElement | JsxSelfClosingElement
|
|
80
|
+
): JsxAttribute | null {
|
|
81
|
+
for (const attribute of opening.getAttributes()) {
|
|
82
|
+
if (!Node.isJsxAttribute(attribute)) continue
|
|
83
|
+
const name = attribute.getNameNode()
|
|
84
|
+
if (Node.isIdentifier(name) && name.getText() === 'group') return attribute
|
|
85
|
+
}
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** an unnamed condition takes the nearest group; a named one takes its own */
|
|
90
|
+
function matches(consumerGroup: string, declaration: string | null): boolean {
|
|
91
|
+
if (declaration === null) return true
|
|
92
|
+
return consumerGroup === '' || consumerGroup === declaration
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface Consumer {
|
|
96
|
+
/** the JSX attribute or object property holding the legacy condition */
|
|
97
|
+
node: Declaration
|
|
98
|
+
group: string
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function containerConsumers(
|
|
102
|
+
sourceFile: SourceFile,
|
|
103
|
+
registry: ModifierRegistryView
|
|
104
|
+
): Consumer[] {
|
|
105
|
+
const consumers: Consumer[] = []
|
|
106
|
+
const add = (node: Declaration, name: string): void => {
|
|
107
|
+
if (!isLegacyConditionName(name)) return
|
|
108
|
+
const resolution = resolveLegacyName(name, registry)
|
|
109
|
+
if (!resolution.ok || resolution.resolved.container === null) return
|
|
110
|
+
consumers.push({ node, group: resolution.resolved.container.group ?? '' })
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const attribute of sourceFile.getDescendantsOfKind(SyntaxKind.JsxAttribute)) {
|
|
114
|
+
const name = attribute.getNameNode()
|
|
115
|
+
if (Node.isIdentifier(name)) add(attribute, name.getText())
|
|
116
|
+
}
|
|
117
|
+
for (const property of sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAssignment)) {
|
|
118
|
+
const name = property.getNameNode()
|
|
119
|
+
if (Node.isComputedPropertyName(name)) continue
|
|
120
|
+
add(property, name.getText().replace(/^['"]|['"]$/g, ''))
|
|
121
|
+
}
|
|
122
|
+
return consumers
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function declarations(sourceFile: SourceFile): Declaration[] {
|
|
126
|
+
const found: Declaration[] = []
|
|
127
|
+
for (const attribute of sourceFile.getDescendantsOfKind(SyntaxKind.JsxAttribute)) {
|
|
128
|
+
const name = attribute.getNameNode()
|
|
129
|
+
if (Node.isIdentifier(name) && name.getText() === 'group') found.push(attribute)
|
|
130
|
+
}
|
|
131
|
+
for (const property of sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAssignment)) {
|
|
132
|
+
const name = property.getNameNode()
|
|
133
|
+
if (Node.isComputedPropertyName(name)) continue
|
|
134
|
+
if (name.getText().replace(/^['"]|['"]$/g, '') === 'group') found.push(property)
|
|
135
|
+
}
|
|
136
|
+
return found
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The nearest JSX ancestor declaring the group, `'ambiguous'` when an ancestor
|
|
141
|
+
* declares a group name this pass cannot read, or null when no ancestor declares
|
|
142
|
+
* one at all.
|
|
143
|
+
*/
|
|
144
|
+
function provenAncestor(
|
|
145
|
+
consumer: Consumer
|
|
146
|
+
): { declaration: JsxAttribute } | 'ambiguous' | null {
|
|
147
|
+
const owner = consumer.node.getFirstAncestor(
|
|
148
|
+
(node): node is JsxOpeningElement | JsxSelfClosingElement =>
|
|
149
|
+
Node.isJsxOpeningElement(node) || Node.isJsxSelfClosingElement(node)
|
|
150
|
+
)
|
|
151
|
+
if (owner === undefined) return null
|
|
152
|
+
|
|
153
|
+
// an element's own group never applies to itself, so the walk starts above it
|
|
154
|
+
for (const ancestor of owner.getAncestors()) {
|
|
155
|
+
if (!Node.isJsxElement(ancestor)) continue
|
|
156
|
+
const declaration = groupAttribute(ancestor.getOpeningElement())
|
|
157
|
+
if (declaration === null) continue
|
|
158
|
+
const group = declaredGroup(declaration)
|
|
159
|
+
if (group === null) return 'ambiguous'
|
|
160
|
+
if (matches(consumer.group, group)) return { declaration }
|
|
161
|
+
}
|
|
162
|
+
return null
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function planContainers(
|
|
166
|
+
sourceFile: SourceFile,
|
|
167
|
+
registry: ModifierRegistryView
|
|
168
|
+
): ContainerPlan {
|
|
169
|
+
const consumers = containerConsumers(sourceFile, registry)
|
|
170
|
+
if (!consumers.length) return emptyPlan
|
|
171
|
+
|
|
172
|
+
const targets = new Map<Node, ContainerTarget>()
|
|
173
|
+
const unresolved = new Map<Node, Flag>()
|
|
174
|
+
const all = declarations(sourceFile)
|
|
175
|
+
|
|
176
|
+
const target = (declaration: Declaration, consumer: Consumer, flag: Flag | null) => {
|
|
177
|
+
const existing = targets.get(declaration)
|
|
178
|
+
if (existing) {
|
|
179
|
+
existing.named ||= consumer.group !== ''
|
|
180
|
+
existing.flag ??= flag
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
targets.set(declaration, {
|
|
184
|
+
group: declaredGroup(declaration) ?? consumer.group,
|
|
185
|
+
named: consumer.group !== '',
|
|
186
|
+
flag,
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (const consumer of consumers) {
|
|
191
|
+
const label =
|
|
192
|
+
consumer.group === '' ? 'the nearest group' : `group "${consumer.group}"`
|
|
193
|
+
const proven = provenAncestor(consumer)
|
|
194
|
+
if (proven === 'ambiguous') {
|
|
195
|
+
unresolved.set(consumer.node, {
|
|
196
|
+
code: 'ambiguous-container-group',
|
|
197
|
+
detail: `a JSX ancestor declares a group name this pass cannot read, so the element that has to declare the container for ${label} is not provable; add "container" by hand`,
|
|
198
|
+
})
|
|
199
|
+
continue
|
|
200
|
+
}
|
|
201
|
+
if (proven !== null) {
|
|
202
|
+
target(proven.declaration, consumer, null)
|
|
203
|
+
continue
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const candidates = all.filter((declaration) =>
|
|
207
|
+
matches(consumer.group, declaredGroup(declaration))
|
|
208
|
+
)
|
|
209
|
+
if (candidates.length === 1) {
|
|
210
|
+
target(candidates[0], consumer, {
|
|
211
|
+
code: 'unproven-container-group',
|
|
212
|
+
detail: `a legacy container-size condition targets ${label} and this is the only declaration of it in the file, but no JSX ancestry proves it wraps that element; confirm the container belongs here`,
|
|
213
|
+
})
|
|
214
|
+
continue
|
|
215
|
+
}
|
|
216
|
+
unresolved.set(consumer.node, {
|
|
217
|
+
code: candidates.length
|
|
218
|
+
? 'ambiguous-container-group'
|
|
219
|
+
: 'container-group-not-declared',
|
|
220
|
+
detail: candidates.length
|
|
221
|
+
? `${candidates.length} declarations of ${label} are in this file and no JSX ancestry picks one, so the element that has to declare the container is not provable; add "container" by hand`
|
|
222
|
+
: `${label} is not declared in this file, so the "@…" query this condition becomes has no container to match; add "container" to the element declaring the group`,
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return { targets, unresolved }
|
|
227
|
+
}
|