@tamagui/codemod-flat-values 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/README.md +211 -1
- package/dist/builtInNames.mjs +13 -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 +1139 -0
- package/dist/convert.mjs.map +1 -0
- package/dist/expressions.mjs +188 -0
- package/dist/expressions.mjs.map +1 -0
- package/dist/grammar.mjs +68 -0
- package/dist/grammar.mjs.map +1 -0
- package/dist/index.mjs +342 -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 +129 -0
- package/dist/report.mjs.map +1 -0
- package/dist/structuredNative.mjs +275 -0
- package/dist/structuredNative.mjs.map +1 -0
- package/package.json +35 -7
- package/src/builtInNames.ts +21 -0
- package/src/containers.ts +227 -0
- package/src/convert.ts +1784 -0
- package/src/expressions.ts +220 -0
- package/src/grammar.ts +180 -0
- package/src/index.ts +517 -0
- package/src/legacyConditions.ts +346 -0
- package/src/legacyNames.ts +160 -0
- package/src/provenance.ts +210 -0
- package/src/report.ts +235 -0
- package/src/structuredNative.ts +459 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// Two different questions get asked about a dynamic style value, each answered
|
|
2
|
+
// exactly one way.
|
|
3
|
+
//
|
|
4
|
+
// 1. Can the expression be rewritten? Only a tree of literals can: its `$token`
|
|
5
|
+
// spellings are right there in the source, so `active ? '$red10' : '$blue10'`
|
|
6
|
+
// becomes `active ? 'red10' : 'blue10'`. `literalTree` answers this from the
|
|
7
|
+
// AST.
|
|
8
|
+
// 2. Can the expression be left alone? Only if its runtime type cannot carry a
|
|
9
|
+
// legacy `$token` string. `runtimeType` answers this from the type checker, so
|
|
10
|
+
// `const GREY = 'rgb(217, 215, 210)'` is provably safe while an untyped
|
|
11
|
+
// identifier is not.
|
|
12
|
+
|
|
13
|
+
import { Node, SyntaxKind, type Expression, type TemplateExpression } from 'ts-morph'
|
|
14
|
+
import { flatStringValue, type ModifierRegistryView } from './grammar'
|
|
15
|
+
|
|
16
|
+
export function unwrapExpression(expression: Expression): Expression {
|
|
17
|
+
let current = expression
|
|
18
|
+
while (
|
|
19
|
+
Node.isParenthesizedExpression(current) ||
|
|
20
|
+
Node.isAsExpression(current) ||
|
|
21
|
+
Node.isTypeAssertion(current) ||
|
|
22
|
+
Node.isNonNullExpression(current)
|
|
23
|
+
) {
|
|
24
|
+
current = current.getExpression()
|
|
25
|
+
}
|
|
26
|
+
return current
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function numericValue(expression: Expression): number | null {
|
|
30
|
+
const current = unwrapExpression(expression)
|
|
31
|
+
if (Node.isNumericLiteral(current)) return Number(current.getText())
|
|
32
|
+
if (Node.isPrefixUnaryExpression(current)) {
|
|
33
|
+
const operand = current.getOperand()
|
|
34
|
+
if (!Node.isNumericLiteral(operand)) return null
|
|
35
|
+
const value = Number(operand.getText())
|
|
36
|
+
const operator = current.getOperatorToken()
|
|
37
|
+
if (operator === SyntaxKind.MinusToken) return -value
|
|
38
|
+
if (operator === SyntaxKind.PlusToken) return value
|
|
39
|
+
}
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** the statically known value of a legacy condition object leaf */
|
|
44
|
+
export function staticLeafValue(
|
|
45
|
+
expression: Expression
|
|
46
|
+
): { found: true; value: unknown } | null {
|
|
47
|
+
const current = unwrapExpression(expression)
|
|
48
|
+
if (Node.isStringLiteral(current) || Node.isNoSubstitutionTemplateLiteral(current)) {
|
|
49
|
+
return { found: true, value: current.getLiteralValue() }
|
|
50
|
+
}
|
|
51
|
+
const number = numericValue(current)
|
|
52
|
+
if (number !== null) return { found: true, value: number }
|
|
53
|
+
if (current.getKind() === SyntaxKind.TrueKeyword) return { found: true, value: true }
|
|
54
|
+
if (current.getKind() === SyntaxKind.FalseKeyword) return { found: true, value: false }
|
|
55
|
+
if (current.getKind() === SyntaxKind.NullKeyword) return { found: true, value: null }
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type LiteralKind = 'number' | 'string' | 'nullish'
|
|
60
|
+
|
|
61
|
+
export interface TreeError {
|
|
62
|
+
code: string
|
|
63
|
+
message: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface LiteralTree {
|
|
67
|
+
kind: LiteralKind
|
|
68
|
+
/** the expression source with legacy token spellings rewritten */
|
|
69
|
+
text: string
|
|
70
|
+
/** original string literal chunks, for migrations that need authored names */
|
|
71
|
+
strings: readonly string[]
|
|
72
|
+
/** set when a token spelling in the tree has no flat name */
|
|
73
|
+
error: TreeError | null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* One literal chunk's flat spelling. A chunk with no `$` is already flat, and one
|
|
78
|
+
* that has a `$` goes through the shared converter, so the same value refused as
|
|
79
|
+
* a clause payload (`url($asset)`) is refused here instead of being rewritten.
|
|
80
|
+
*/
|
|
81
|
+
function literalText(
|
|
82
|
+
value: string,
|
|
83
|
+
registry: ModifierRegistryView
|
|
84
|
+
): { text: string; error: TreeError | null } {
|
|
85
|
+
if (!value.includes('$')) return { text: value, error: null }
|
|
86
|
+
const flat = flatStringValue(value, registry)
|
|
87
|
+
if (flat.text === null) {
|
|
88
|
+
return {
|
|
89
|
+
text: value,
|
|
90
|
+
error: {
|
|
91
|
+
code: flat.error?.code ?? 'unsupported-legacy-value',
|
|
92
|
+
message: flat.error?.message ?? `"${value}" has no flat spelling`,
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { text: flat.text, error: null }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function templateTree(
|
|
100
|
+
template: TemplateExpression,
|
|
101
|
+
registry: ModifierRegistryView
|
|
102
|
+
): LiteralTree {
|
|
103
|
+
// every literal chunk converts on its own: `\`$accent${i}\`` names one token
|
|
104
|
+
// whose tail is computed, and a chunk holding quoted or url() content is refused
|
|
105
|
+
const head = literalText(template.getHead().getLiteralText(), registry)
|
|
106
|
+
let error = head.error
|
|
107
|
+
let text = `\`${head.text}`
|
|
108
|
+
const strings = [template.getHead().getLiteralText()]
|
|
109
|
+
for (const span of template.getTemplateSpans()) {
|
|
110
|
+
const literal = span.getLiteral().getLiteralText()
|
|
111
|
+
const tail = literalText(literal, registry)
|
|
112
|
+
error ??= tail.error
|
|
113
|
+
strings.push(literal)
|
|
114
|
+
text += `\${${span.getExpression().getText().trim()}}${tail.text}`
|
|
115
|
+
}
|
|
116
|
+
return { kind: 'string', text: `${text}\``, strings, error }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The expression as a tree of literals, or null when any leaf is computed. Every
|
|
121
|
+
* literal is reprinted, which is what strips `$` from token spellings.
|
|
122
|
+
*/
|
|
123
|
+
export function literalTree(
|
|
124
|
+
expression: Expression,
|
|
125
|
+
registry: ModifierRegistryView
|
|
126
|
+
): LiteralTree | null {
|
|
127
|
+
const current = unwrapExpression(expression)
|
|
128
|
+
|
|
129
|
+
if (Node.isStringLiteral(current) || Node.isNoSubstitutionTemplateLiteral(current)) {
|
|
130
|
+
const value = current.getLiteralValue()
|
|
131
|
+
const literal = literalText(value, registry)
|
|
132
|
+
return {
|
|
133
|
+
kind: 'string',
|
|
134
|
+
text: JSON.stringify(literal.text),
|
|
135
|
+
strings: [value],
|
|
136
|
+
error: literal.error,
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (Node.isTemplateExpression(current)) return templateTree(current, registry)
|
|
140
|
+
|
|
141
|
+
const number = numericValue(current)
|
|
142
|
+
if (number !== null) {
|
|
143
|
+
return { kind: 'number', text: String(number), strings: [], error: null }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (
|
|
147
|
+
current.getKind() === SyntaxKind.UndefinedKeyword ||
|
|
148
|
+
current.getKind() === SyntaxKind.NullKeyword ||
|
|
149
|
+
(Node.isIdentifier(current) && current.getText() === 'undefined')
|
|
150
|
+
) {
|
|
151
|
+
return { kind: 'nullish', text: current.getText(), strings: [], error: null }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (Node.isConditionalExpression(current)) {
|
|
155
|
+
const whenTrue = literalTree(current.getWhenTrue(), registry)
|
|
156
|
+
const whenFalse = literalTree(current.getWhenFalse(), registry)
|
|
157
|
+
if (!whenTrue || !whenFalse) return null
|
|
158
|
+
if (
|
|
159
|
+
whenTrue.kind !== whenFalse.kind &&
|
|
160
|
+
whenTrue.kind !== 'nullish' &&
|
|
161
|
+
whenFalse.kind !== 'nullish'
|
|
162
|
+
) {
|
|
163
|
+
return null
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
kind: whenTrue.kind === 'nullish' ? whenFalse.kind : whenTrue.kind,
|
|
167
|
+
text: `${current.getCondition().getText().trim()} ? ${whenTrue.text} : ${whenFalse.text}`,
|
|
168
|
+
strings: [...whenTrue.strings, ...whenFalse.strings],
|
|
169
|
+
error: whenTrue.error ?? whenFalse.error,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return null
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface RuntimeType {
|
|
177
|
+
kind: 'number' | 'string' | 'unknown'
|
|
178
|
+
/** every string literal the type can be, or null when the set is open */
|
|
179
|
+
literals: readonly string[] | null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** what the type checker can prove the expression evaluates to */
|
|
183
|
+
export function runtimeType(expression: Expression): RuntimeType {
|
|
184
|
+
const type = unwrapExpression(expression).getType()
|
|
185
|
+
const parts = type.isUnion() ? type.getUnionTypes() : [type]
|
|
186
|
+
const literals: string[] = []
|
|
187
|
+
let numbers = 0
|
|
188
|
+
let strings = 0
|
|
189
|
+
let known = 0
|
|
190
|
+
|
|
191
|
+
for (const part of parts) {
|
|
192
|
+
if (part.isUndefined() || part.isNull()) continue
|
|
193
|
+
known++
|
|
194
|
+
if (part.isNumber() || part.isNumberLiteral()) {
|
|
195
|
+
numbers++
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
if (part.isStringLiteral()) {
|
|
199
|
+
strings++
|
|
200
|
+
literals.push(String(part.getLiteralValue()))
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
if (part.isString()) {
|
|
204
|
+
strings++
|
|
205
|
+
continue
|
|
206
|
+
}
|
|
207
|
+
return { kind: 'unknown', literals: null }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!known) return { kind: 'unknown', literals: null }
|
|
211
|
+
if (numbers === known) return { kind: 'number', literals: null }
|
|
212
|
+
if (strings === known) {
|
|
213
|
+
return { kind: 'string', literals: literals.length === known ? literals : null }
|
|
214
|
+
}
|
|
215
|
+
return { kind: 'unknown', literals: null }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function compact(text: string): string {
|
|
219
|
+
return text.replace(/\s+/g, ' ').trim()
|
|
220
|
+
}
|
package/src/grammar.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// Every piece of style knowledge the codemod needs comes from
|
|
2
|
+
// `@tamagui/style-grammar`: the value parser, the legacy condition converter, the
|
|
3
|
+
// clause merge, and the property/unit tables. Nothing here re-derives grammar
|
|
4
|
+
// behavior, so a converted program is spelled exactly the way the runtime and the
|
|
5
|
+
// compiler will read it back.
|
|
6
|
+
|
|
7
|
+
import { stylePropsAll } from '@tamagui/helpers'
|
|
8
|
+
import { shorthands } from '@tamagui/shorthands/v6'
|
|
9
|
+
import * as styleGrammarTooling from '@tamagui/style-grammar/tooling'
|
|
10
|
+
import type {
|
|
11
|
+
ConvertLegacyConditionOptions,
|
|
12
|
+
LegacyConditionError,
|
|
13
|
+
LegacyConditionResult,
|
|
14
|
+
} from './legacyConditions'
|
|
15
|
+
import type {
|
|
16
|
+
ConversionReason,
|
|
17
|
+
ConversionTargets,
|
|
18
|
+
HostView,
|
|
19
|
+
ModifierRegistryView,
|
|
20
|
+
ParsedClause,
|
|
21
|
+
ParsedValue,
|
|
22
|
+
} from '@tamagui/style-grammar/tooling'
|
|
23
|
+
import { replaceV6BuiltInTokens } from './builtInNames'
|
|
24
|
+
import {
|
|
25
|
+
convertLegacyConditionProp as convertLegacyConditionPropLocal,
|
|
26
|
+
pseudoToModifier,
|
|
27
|
+
} from './legacyConditions'
|
|
28
|
+
|
|
29
|
+
const grammar = styleGrammarTooling
|
|
30
|
+
|
|
31
|
+
export const {
|
|
32
|
+
assessFlatConversion,
|
|
33
|
+
createModifierRegistry,
|
|
34
|
+
defaultMediaKeys,
|
|
35
|
+
evaluateProgram,
|
|
36
|
+
expandToLonghands,
|
|
37
|
+
grammarEntries,
|
|
38
|
+
grammarPlatformNames,
|
|
39
|
+
legacyPartComposite,
|
|
40
|
+
mergeProgramValues,
|
|
41
|
+
parseValue,
|
|
42
|
+
programEligibility,
|
|
43
|
+
standaloneValueProps,
|
|
44
|
+
parseTransformString,
|
|
45
|
+
} = grammar
|
|
46
|
+
|
|
47
|
+
export { pseudoToModifier }
|
|
48
|
+
|
|
49
|
+
export type {
|
|
50
|
+
ConversionReason,
|
|
51
|
+
ConversionTargets,
|
|
52
|
+
HostView,
|
|
53
|
+
LegacyConditionError,
|
|
54
|
+
ModifierRegistryView,
|
|
55
|
+
ParsedClause,
|
|
56
|
+
ParsedValue,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { shorthands }
|
|
60
|
+
|
|
61
|
+
function renameBuiltInTokens(value: unknown): unknown {
|
|
62
|
+
if (typeof value === 'string') return replaceV6BuiltInTokens(value)
|
|
63
|
+
if (Array.isArray(value)) return value.map(renameBuiltInTokens)
|
|
64
|
+
if (value === null || typeof value !== 'object') return value
|
|
65
|
+
|
|
66
|
+
const renamed: Record<string, unknown> = {}
|
|
67
|
+
for (const key in value) {
|
|
68
|
+
renamed[key] = renameBuiltInTokens((value as Record<string, unknown>)[key])
|
|
69
|
+
}
|
|
70
|
+
return renamed
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function convertLegacyConditionProp(
|
|
74
|
+
propName: string,
|
|
75
|
+
value: unknown,
|
|
76
|
+
options: ConvertLegacyConditionOptions
|
|
77
|
+
): LegacyConditionResult | null {
|
|
78
|
+
return convertLegacyConditionPropLocal(propName, renameBuiltInTokens(value), options)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** every prop spelling the codemod treats as carrying a style value */
|
|
82
|
+
export const styleProps: ReadonlySet<string> = new Set<string>([
|
|
83
|
+
...grammarEntries.map((entry) => entry.prop),
|
|
84
|
+
...Object.keys(standaloneValueProps),
|
|
85
|
+
...Object.keys(stylePropsAll),
|
|
86
|
+
...Object.keys(shorthands),
|
|
87
|
+
...Object.values(shorthands),
|
|
88
|
+
])
|
|
89
|
+
|
|
90
|
+
/** the media keys of the V6 default config, plus the two motion queries */
|
|
91
|
+
export const codemodMediaNames: readonly string[] = [
|
|
92
|
+
...defaultMediaKeys,
|
|
93
|
+
'motionReduce',
|
|
94
|
+
'motionSafe',
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
export function resolveProp(prop: string): string {
|
|
98
|
+
return shorthands[prop] ?? prop
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The one registered condition the codemod uses to reach the shared value
|
|
103
|
+
* converter for a single (prop, value) pair. Base values and clause payloads then
|
|
104
|
+
* spell identically by construction.
|
|
105
|
+
*/
|
|
106
|
+
const payloadProbeCondition = '$platform-web'
|
|
107
|
+
|
|
108
|
+
export interface SharedPayload {
|
|
109
|
+
payload: string | null
|
|
110
|
+
errors: readonly LegacyConditionError[]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function sharedPayload(
|
|
114
|
+
prop: string,
|
|
115
|
+
value: unknown,
|
|
116
|
+
registry: ModifierRegistryView
|
|
117
|
+
): SharedPayload {
|
|
118
|
+
const converted = convertLegacyConditionProp(
|
|
119
|
+
payloadProbeCondition,
|
|
120
|
+
{ [prop]: value },
|
|
121
|
+
{ registry }
|
|
122
|
+
)
|
|
123
|
+
if (converted === null) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`the payload probe condition "${payloadProbeCondition}" is unregistered`
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
payload: converted.contributions[0]?.clause.payload ?? null,
|
|
130
|
+
errors: converted.errors,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* A property with no family split, no unit table entry, and no transform part, so
|
|
136
|
+
* the shared converter's answer for a string value is exactly its token rule and
|
|
137
|
+
* nothing else.
|
|
138
|
+
*/
|
|
139
|
+
const stringProbeProp = 'color'
|
|
140
|
+
|
|
141
|
+
export interface FlatString {
|
|
142
|
+
/** the flat spelling, or null when the shared converter refused the value */
|
|
143
|
+
text: string | null
|
|
144
|
+
error: LegacyConditionError | null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The flat spelling of a legacy string, taken from the shared converter rather
|
|
149
|
+
* than re-derived. That converter drops `$` from token spellings anywhere in a
|
|
150
|
+
* composite value and refuses a value that mixes `$` with quoted or unquoted
|
|
151
|
+
* `url()` content, where a `$` is literal CSS the resolver never reads as a
|
|
152
|
+
* token candidate.
|
|
153
|
+
*/
|
|
154
|
+
export function flatStringValue(
|
|
155
|
+
value: string,
|
|
156
|
+
registry: ModifierRegistryView
|
|
157
|
+
): FlatString {
|
|
158
|
+
const probe = sharedPayload(stringProbeProp, value, registry)
|
|
159
|
+
return { text: probe.payload, error: probe.errors[0] ?? null }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const unitSuffixes = new Map<string, string>()
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The unit a raw number carries for this property, taken from the shared
|
|
166
|
+
* converter itself: `px` for lengths, `deg` for `rotate`, nothing for the
|
|
167
|
+
* unitless table. A dynamic numeric expression interpolates with the same
|
|
168
|
+
* suffix a literal would have received.
|
|
169
|
+
*/
|
|
170
|
+
export function unitSuffix(prop: string, registry: ModifierRegistryView): string {
|
|
171
|
+
const cached = unitSuffixes.get(prop)
|
|
172
|
+
if (cached !== undefined) return cached
|
|
173
|
+
const probe = sharedPayload(prop, 1, registry)
|
|
174
|
+
const suffix =
|
|
175
|
+
probe.payload !== null && probe.payload.startsWith('1') ? probe.payload.slice(1) : ''
|
|
176
|
+
unitSuffixes.set(prop, suffix)
|
|
177
|
+
return suffix
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export const printProgram = grammar.formatParsedValue
|