@tamagui/codemod-flat-values 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.
@@ -0,0 +1,346 @@
1
+ import {
2
+ transformFamilyProps,
3
+ unitlessNumberProperties,
4
+ } from '@tamagui/style-grammar/tooling'
5
+ import type { ModifierRegistryView, ParsedClause } from '@tamagui/style-grammar/tooling'
6
+
7
+ export const pseudoToModifier: Readonly<Record<string, string>> = Object.freeze({
8
+ hoverStyle: 'hover',
9
+ pressStyle: 'press',
10
+ focusStyle: 'focus',
11
+ focusVisibleStyle: 'focus-visible',
12
+ focusWithinStyle: 'focus-within',
13
+ disabledStyle: 'disabled',
14
+ enterStyle: 'enter',
15
+ exitStyle: 'exit',
16
+ })
17
+
18
+ const transformPartProperties: ReadonlySet<string> = new Set([
19
+ 'scale',
20
+ 'scaleX',
21
+ 'scaleY',
22
+ 'rotate',
23
+ 'rotateX',
24
+ 'rotateY',
25
+ 'rotateZ',
26
+ 'x',
27
+ 'y',
28
+ 'skewX',
29
+ 'skewY',
30
+ 'perspective',
31
+ ])
32
+
33
+ export interface ConvertLegacyConditionOptions {
34
+ registry: ModifierRegistryView
35
+ }
36
+
37
+ export interface LegacyConditionContribution {
38
+ prop: string
39
+ clause: ParsedClause
40
+ }
41
+
42
+ export interface LegacyConditionError {
43
+ code: string
44
+ path: string
45
+ message: string
46
+ }
47
+
48
+ export interface LegacyConditionResult {
49
+ contributions: LegacyConditionContribution[]
50
+ errors: LegacyConditionError[]
51
+ }
52
+
53
+ type ConditionResolution =
54
+ | { recognized: false }
55
+ | { recognized: true; modifiers: readonly string[] }
56
+ | { recognized: true; error: Omit<LegacyConditionError, 'path'> }
57
+
58
+ function resolveLegacyCondition(
59
+ propName: string,
60
+ registry: ModifierRegistryView
61
+ ): ConditionResolution {
62
+ const pseudoModifier = pseudoToModifier[propName]
63
+ if (pseudoModifier !== undefined) {
64
+ return registry.get(pseudoModifier) === 'state'
65
+ ? { recognized: true, modifiers: [pseudoModifier] }
66
+ : {
67
+ recognized: true,
68
+ error: {
69
+ code: 'unregistered-legacy-condition',
70
+ message: `legacy condition "${propName}" maps to unregistered state modifier "${pseudoModifier}"`,
71
+ },
72
+ }
73
+ }
74
+
75
+ if (propName.startsWith('$theme-')) {
76
+ const modifier = propName.slice('$theme-'.length)
77
+ return modifier && registry.get(modifier) === 'theme'
78
+ ? { recognized: true, modifiers: [modifier] }
79
+ : {
80
+ recognized: true,
81
+ error: {
82
+ code: 'unregistered-legacy-condition',
83
+ message: `legacy theme condition "${propName}" does not name a registered theme`,
84
+ },
85
+ }
86
+ }
87
+
88
+ if (propName.startsWith('$platform-')) {
89
+ const modifier = propName.slice('$platform-'.length)
90
+ return modifier && registry.get(modifier) === 'platform'
91
+ ? { recognized: true, modifiers: [modifier] }
92
+ : {
93
+ recognized: true,
94
+ error: {
95
+ code: 'unregistered-legacy-condition',
96
+ message: `legacy platform condition "${propName}" does not name a registered platform`,
97
+ },
98
+ }
99
+ }
100
+
101
+ if (propName.startsWith('$group-')) {
102
+ const remainder = propName.slice('$group-'.length)
103
+ const candidates: string[] = []
104
+ let start = 0
105
+ while (start < remainder.length) {
106
+ const state = remainder.slice(start)
107
+ if (registry.get(state) === 'state') candidates.push(state)
108
+ const dash = remainder.indexOf('-', start)
109
+ if (dash === -1) break
110
+ start = dash + 1
111
+ }
112
+
113
+ const longestLength = candidates.reduce(
114
+ (length, state) => Math.max(length, state.length),
115
+ 0
116
+ )
117
+ const longest = candidates.filter((state) => state.length === longestLength)
118
+ if (longest.length > 1) {
119
+ return {
120
+ recognized: true,
121
+ error: {
122
+ code: 'ambiguous-legacy-group',
123
+ message: `legacy group condition "${propName}" has more than one equally specific state suffix`,
124
+ },
125
+ }
126
+ }
127
+
128
+ const state = longest.length === 1 ? longest[0] : null
129
+ let namePart =
130
+ state === null
131
+ ? remainder
132
+ : state.length === remainder.length
133
+ ? ''
134
+ : remainder.slice(0, -(state.length + 1))
135
+ let media: string | null = null
136
+
137
+ if (namePart) {
138
+ let scanStart = 0
139
+ while (scanStart < namePart.length) {
140
+ const suffix = namePart.slice(scanStart)
141
+ if (registry.get(`@${suffix}`) === 'container') {
142
+ media = suffix
143
+ break
144
+ }
145
+ const dash = namePart.indexOf('-', scanStart)
146
+ if (dash === -1) break
147
+ scanStart = dash + 1
148
+ }
149
+ if (media !== null) {
150
+ namePart =
151
+ media.length === namePart.length ? '' : namePart.slice(0, -(media.length + 1))
152
+ } else if (state === null) {
153
+ return {
154
+ recognized: true,
155
+ error: {
156
+ code: 'unregistered-legacy-condition',
157
+ message: `legacy group condition "${propName}" has no registered state or container-size suffix`,
158
+ },
159
+ }
160
+ }
161
+ } else if (state === null) {
162
+ return {
163
+ recognized: true,
164
+ error: {
165
+ code: 'unregistered-legacy-condition',
166
+ message: `legacy group condition "${propName}" has no registered state suffix`,
167
+ },
168
+ }
169
+ }
170
+
171
+ const suffix = namePart ? `/${namePart}` : ''
172
+ const modifiers: string[] = []
173
+ if (media !== null) modifiers.push(`@${media}${suffix}`)
174
+ if (state !== null) modifiers.push(`group-${state}${suffix}`)
175
+ return { recognized: true, modifiers }
176
+ }
177
+
178
+ if (propName[0] === '$') {
179
+ const modifier = propName.slice(1)
180
+ const kind = registry.get(modifier)
181
+ if (kind === 'media' || kind === 'container') {
182
+ return { recognized: true, modifiers: [modifier] }
183
+ }
184
+ }
185
+
186
+ return { recognized: false }
187
+ }
188
+
189
+ function isConditionObject(value: unknown): value is Record<string, unknown> {
190
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return false
191
+ const prototype = Object.getPrototypeOf(value)
192
+ return prototype === Object.prototype || prototype === null
193
+ }
194
+
195
+ function convertStyleValue(
196
+ prop: string,
197
+ value: unknown,
198
+ path: string,
199
+ errors: LegacyConditionError[]
200
+ ): string | null {
201
+ if (transformPartProperties.has(prop) && !transformFamilyProps.has(prop)) {
202
+ errors.push({
203
+ code: 'legacy-transform-part',
204
+ path,
205
+ message: `legacy transform part "${prop}" has no flat spelling; author it inside a flat \`transform\` value (only x, y, scale, scaleX, scaleY, and rotate are first-class)`,
206
+ })
207
+ return null
208
+ }
209
+
210
+ if (
211
+ typeof value === 'number' &&
212
+ Number.isFinite(value) &&
213
+ transformFamilyProps.has(prop)
214
+ ) {
215
+ if (prop === 'rotate') return value === 0 ? '0deg' : `${value}deg`
216
+ if (prop === 'x' || prop === 'y') return value === 0 ? '0' : `${value}px`
217
+ return String(value)
218
+ }
219
+
220
+ if (typeof value === 'string') {
221
+ if (!value.length) {
222
+ errors.push({
223
+ code: 'unsupported-legacy-value',
224
+ path,
225
+ message: 'an empty string cannot become a condition clause payload',
226
+ })
227
+ return null
228
+ }
229
+ if (value.indexOf('$') === -1) return value
230
+ if (value.includes('"') || value.includes("'") || value.includes('url(')) {
231
+ errors.push({
232
+ code: 'unsupported-legacy-value',
233
+ path,
234
+ message: `"${value}" mixes "$" with quoted or url() content; migrate the token spelling by hand`,
235
+ })
236
+ return null
237
+ }
238
+ if (/\$[\w-]*\./.test(value)) {
239
+ errors.push({
240
+ code: 'legacy-token-dot-path',
241
+ path,
242
+ message: `legacy token in "${value}" uses dot-path naming; rename it to one configured flat token name before conversion`,
243
+ })
244
+ return null
245
+ }
246
+ if (/\$-?\d/.test(value) && !/^\$-?[\w-]+$/.test(value)) {
247
+ errors.push({
248
+ code: 'legacy-numeric-composite-token',
249
+ path,
250
+ message: `numeric token in "${value}" is embedded in a composite value; replace it with its resolved CSS value before conversion`,
251
+ })
252
+ return null
253
+ }
254
+ if (/\$(?![\w-])/.test(value)) {
255
+ errors.push({
256
+ code: 'unsupported-legacy-value',
257
+ path,
258
+ message: `"$" in "${value}" is not followed by a token name`,
259
+ })
260
+ return null
261
+ }
262
+ return value.replace(/\$([\w-]+)/g, '$1')
263
+ }
264
+
265
+ if (typeof value === 'number') {
266
+ if (Number.isFinite(value)) {
267
+ return unitlessNumberProperties.has(prop) ? String(value) : `${value}px`
268
+ }
269
+ errors.push({
270
+ code: 'unsupported-legacy-value',
271
+ path,
272
+ message: `non-finite number ${String(value)} cannot become a CSS payload`,
273
+ })
274
+ return null
275
+ }
276
+
277
+ errors.push({
278
+ code: 'unsupported-legacy-value',
279
+ path,
280
+ message: `legacy condition value for "${prop}" must be a string or finite number`,
281
+ })
282
+ return null
283
+ }
284
+
285
+ export function convertLegacyConditionProp(
286
+ propName: string,
287
+ value: unknown,
288
+ options: ConvertLegacyConditionOptions
289
+ ): LegacyConditionResult | null {
290
+ const root = resolveLegacyCondition(propName, options.registry)
291
+ if (!root.recognized) return null
292
+
293
+ const result: LegacyConditionResult = { contributions: [], errors: [] }
294
+ if ('error' in root) {
295
+ result.errors.push({ ...root.error, path: propName })
296
+ return result
297
+ }
298
+ if (!isConditionObject(value)) {
299
+ result.errors.push({
300
+ code: 'legacy-condition-object',
301
+ path: propName,
302
+ message: `legacy condition "${propName}" must contain a style object`,
303
+ })
304
+ return result
305
+ }
306
+
307
+ const modifiers = root.modifiers.slice()
308
+ const visit = (object: Record<string, unknown>, objectPath: string): void => {
309
+ for (const childProp in object) {
310
+ if (!Object.prototype.hasOwnProperty.call(object, childProp)) continue
311
+ const childValue = object[childProp]
312
+ const childPath = `${objectPath}.${childProp}`
313
+ const condition = resolveLegacyCondition(childProp, options.registry)
314
+
315
+ if (condition.recognized) {
316
+ if ('error' in condition) {
317
+ result.errors.push({ ...condition.error, path: childPath })
318
+ continue
319
+ }
320
+ if (!isConditionObject(childValue)) {
321
+ result.errors.push({
322
+ code: 'legacy-condition-object',
323
+ path: childPath,
324
+ message: `legacy condition "${childProp}" must contain a style object`,
325
+ })
326
+ continue
327
+ }
328
+ modifiers.push(...condition.modifiers)
329
+ visit(childValue, childPath)
330
+ modifiers.length -= condition.modifiers.length
331
+ continue
332
+ }
333
+
334
+ const payload = convertStyleValue(childProp, childValue, childPath, result.errors)
335
+ if (payload !== null) {
336
+ result.contributions.push({
337
+ prop: childProp,
338
+ clause: { modifiers: modifiers.slice(), payload },
339
+ })
340
+ }
341
+ }
342
+ }
343
+
344
+ visit(value, propName)
345
+ return result
346
+ }
@@ -0,0 +1,160 @@
1
+ // Legacy condition prop names, normalized to the one spelling the shared
2
+ // converter resolves.
3
+ //
4
+ // Three v1 spellings need normalizing before the shared converter sees them:
5
+ // the platform conditions are written `$web`/`$ios` rather than `$platform-web`;
6
+ // v1 media keys are camelCase where the V6 built-ins are kebab-case (decision
7
+ // 15); and a legacy group condition may carry a container size, which V3 splits
8
+ // into a container query plus a group state (`$group-card-sm-hover` becomes
9
+ // `@sm/card:group-hover/card:`).
10
+
11
+ import {
12
+ grammarPlatformNames,
13
+ pseudoToModifier,
14
+ type ModifierRegistryView,
15
+ } from './grammar'
16
+
17
+ export interface ContainerRequest {
18
+ /** the media key the container measures */
19
+ size: string
20
+ /** the group name the query is scoped to, or null for the nearest container */
21
+ group: string | null
22
+ }
23
+
24
+ export interface ResolvedLegacyName {
25
+ /** the spelling handed to the shared converter */
26
+ canonical: string
27
+ /** modifiers replacing the converter's root modifier, when the split applies */
28
+ replaceRoot: readonly string[] | null
29
+ /** the container the parent group element has to declare */
30
+ container: ContainerRequest | null
31
+ }
32
+
33
+ export type LegacyNameResolution =
34
+ | { ok: true; resolved: ResolvedLegacyName }
35
+ | { ok: false; code: string; message: string }
36
+
37
+ export function isLegacyConditionName(name: string): boolean {
38
+ return name.startsWith('$') || pseudoToModifier[name] !== undefined
39
+ }
40
+
41
+ function kebab(name: string): string {
42
+ return name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
43
+ }
44
+
45
+ /** the registered media name for a v1 media key, kebab-cased when v1 spelled it camelCase */
46
+ function mediaName(name: string, registry: ModifierRegistryView): string | null {
47
+ if (registry.get(name) === 'media') return name
48
+ const kebabbed = kebab(name)
49
+ return registry.get(kebabbed) === 'media' ? kebabbed : null
50
+ }
51
+
52
+ /** the longest suffix of `parts` that resolves through `resolve` */
53
+ function longestSuffix(
54
+ parts: readonly string[],
55
+ resolve: (candidate: string) => string | null
56
+ ): { value: string; rest: readonly string[] } | null {
57
+ for (let start = 0; start < parts.length; start++) {
58
+ const resolved = resolve(parts.slice(start).join('-'))
59
+ if (resolved !== null) return { value: resolved, rest: parts.slice(0, start) }
60
+ }
61
+ return null
62
+ }
63
+
64
+ function resolveGroupName(
65
+ name: string,
66
+ registry: ModifierRegistryView
67
+ ): LegacyNameResolution {
68
+ const parts = name.slice('$group-'.length).split('-')
69
+ const state = longestSuffix(parts, (candidate) =>
70
+ registry.get(candidate) === 'state' ? candidate : null
71
+ )
72
+ const beforeState = state ? state.rest : parts
73
+ const size = longestSuffix(beforeState, (candidate) => mediaName(candidate, registry))
74
+ const group = (size ? size.rest : beforeState).join('-') || null
75
+ const suffix = group === null ? '' : `/${group}`
76
+
77
+ if (!state && !size) {
78
+ return {
79
+ ok: false,
80
+ code: 'legacy-group-presence',
81
+ message: `"${name}" styles every descendant of the group unconditionally; V3 has no group-presence condition, so move the value to the base program or add a state`,
82
+ }
83
+ }
84
+
85
+ if (!size) {
86
+ // the shared converter already splits `$group-[name-]state`
87
+ return { ok: true, resolved: { canonical: name, replaceRoot: null, container: null } }
88
+ }
89
+
90
+ const modifiers = [`@${size.value}${suffix}`]
91
+ if (state) modifiers.push(`group-${state.value}${suffix}`)
92
+ for (const modifier of modifiers) {
93
+ if (registry.get(modifier) === undefined) {
94
+ return {
95
+ ok: false,
96
+ code: 'unregistered-legacy-condition',
97
+ message: `"${name}" needs modifier "${modifier}", which is not registered`,
98
+ }
99
+ }
100
+ }
101
+
102
+ return {
103
+ ok: true,
104
+ resolved: {
105
+ // any registered root works: `replaceRoot` substitutes the whole chain
106
+ canonical: state
107
+ ? `$group-${group === null ? '' : `${group}-`}${state.value}`
108
+ : `$${size.value}`,
109
+ replaceRoot: modifiers,
110
+ container: { size: size.value, group },
111
+ },
112
+ }
113
+ }
114
+
115
+ export function resolveLegacyName(
116
+ name: string,
117
+ registry: ModifierRegistryView
118
+ ): LegacyNameResolution {
119
+ if (pseudoToModifier[name] !== undefined || name.startsWith('$theme-')) {
120
+ return { ok: true, resolved: { canonical: name, replaceRoot: null, container: null } }
121
+ }
122
+
123
+ if (name.startsWith('$platform-')) {
124
+ return { ok: true, resolved: { canonical: name, replaceRoot: null, container: null } }
125
+ }
126
+
127
+ if (name.startsWith('$group-')) return resolveGroupName(name, registry)
128
+
129
+ if (name.startsWith('$')) {
130
+ const bare = name.slice(1)
131
+ if (grammarPlatformNames.has(bare)) {
132
+ return {
133
+ ok: true,
134
+ resolved: {
135
+ canonical: `$platform-${bare}`,
136
+ replaceRoot: null,
137
+ container: null,
138
+ },
139
+ }
140
+ }
141
+ const media = mediaName(bare, registry)
142
+ if (media !== null) {
143
+ return {
144
+ ok: true,
145
+ resolved: { canonical: `$${media}`, replaceRoot: null, container: null },
146
+ }
147
+ }
148
+ return {
149
+ ok: false,
150
+ code: 'unknown-legacy-condition',
151
+ message: `"${name}" is not a registered legacy condition spelling`,
152
+ }
153
+ }
154
+
155
+ return {
156
+ ok: false,
157
+ code: 'unknown-legacy-condition',
158
+ message: `"${name}" is not a registered legacy condition spelling`,
159
+ }
160
+ }