@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.
Files changed (44) hide show
  1. package/README.md +234 -1
  2. package/dist/builtInNames.mjs +14 -0
  3. package/dist/builtInNames.mjs.map +1 -0
  4. package/dist/containers.mjs +141 -0
  5. package/dist/containers.mjs.map +1 -0
  6. package/dist/convert.mjs +1233 -0
  7. package/dist/convert.mjs.map +1 -0
  8. package/dist/expressions.mjs +188 -0
  9. package/dist/expressions.mjs.map +1 -0
  10. package/dist/functionalVariants.mjs +469 -0
  11. package/dist/functionalVariants.mjs.map +1 -0
  12. package/dist/grammar.mjs +73 -0
  13. package/dist/grammar.mjs.map +1 -0
  14. package/dist/index.mjs +374 -0
  15. package/dist/index.mjs.map +1 -0
  16. package/dist/legacyConditions.mjs +293 -0
  17. package/dist/legacyConditions.mjs.map +1 -0
  18. package/dist/legacyNames.mjs +130 -0
  19. package/dist/legacyNames.mjs.map +1 -0
  20. package/dist/provenance.mjs +137 -0
  21. package/dist/provenance.mjs.map +1 -0
  22. package/dist/report.mjs +188 -0
  23. package/dist/report.mjs.map +1 -0
  24. package/dist/sheetAnatomy.mjs +200 -0
  25. package/dist/sheetAnatomy.mjs.map +1 -0
  26. package/dist/structuredNative.mjs +275 -0
  27. package/dist/structuredNative.mjs.map +1 -0
  28. package/dist/transition.mjs +257 -0
  29. package/dist/transition.mjs.map +1 -0
  30. package/package.json +35 -7
  31. package/src/builtInNames.ts +23 -0
  32. package/src/containers.ts +227 -0
  33. package/src/convert.ts +1977 -0
  34. package/src/expressions.ts +220 -0
  35. package/src/functionalVariants.ts +642 -0
  36. package/src/grammar.ts +190 -0
  37. package/src/index.ts +589 -0
  38. package/src/legacyConditions.ts +357 -0
  39. package/src/legacyNames.ts +160 -0
  40. package/src/provenance.ts +210 -0
  41. package/src/report.ts +362 -0
  42. package/src/sheetAnatomy.ts +277 -0
  43. package/src/structuredNative.ts +459 -0
  44. package/src/transition.ts +415 -0
@@ -0,0 +1,357 @@
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
+ // V5 spells a half-step token with a dot ($2.5); V3 spells the same token
222
+ // with a dash (2-5) and resolves it to the identical value, so a NUMERIC
223
+ // dot-path is a pure rename rather than a value change. Measured against
224
+ // both config packs: $0.5/0-5 = 1, $1.5/1-5 = 4, $2.5/2-5 = 10,
225
+ // $3.5/3-5 = 16.
226
+ //
227
+ // Worth converting rather than flagging because it is the single largest
228
+ // flag category on real corpora: 272 of 497 flagged sites in one mid-size
229
+ // app. A non-numeric dot-path is a rename nobody can derive, so it stays
230
+ // flagged below.
231
+ const text = value.replace(/\$(-?\d+)\.(\d+)\b/g, '$$$1-$2')
232
+
233
+ if (!text.length) {
234
+ errors.push({
235
+ code: 'unsupported-legacy-value',
236
+ path,
237
+ message: 'an empty string cannot become a condition clause payload',
238
+ })
239
+ return null
240
+ }
241
+ if (text.indexOf('$') === -1) return text
242
+ if (text.includes('"') || text.includes("'") || text.includes('url(')) {
243
+ errors.push({
244
+ code: 'unsupported-legacy-value',
245
+ path,
246
+ message: `"${text}" mixes "$" with quoted or url() content; migrate the token spelling by hand`,
247
+ })
248
+ return null
249
+ }
250
+ if (/\$[\w-]*\./.test(text)) {
251
+ errors.push({
252
+ code: 'legacy-token-dot-path',
253
+ path,
254
+ message: `legacy token in "${text}" uses dot-path naming; rename it to one configured flat token name before conversion`,
255
+ })
256
+ return null
257
+ }
258
+ if (/\$-?\d/.test(text) && !/^\$-?[\w-]+$/.test(text)) {
259
+ errors.push({
260
+ code: 'legacy-numeric-composite-token',
261
+ path,
262
+ message: `numeric token in "${text}" is embedded in a composite value; replace it with its resolved CSS value before conversion`,
263
+ })
264
+ return null
265
+ }
266
+ if (/\$(?![\w-])/.test(text)) {
267
+ errors.push({
268
+ code: 'unsupported-legacy-value',
269
+ path,
270
+ message: `"$" in "${text}" is not followed by a token name`,
271
+ })
272
+ return null
273
+ }
274
+ return text.replace(/\$([\w-]+)/g, '$1')
275
+ }
276
+
277
+ if (typeof value === 'number') {
278
+ if (Number.isFinite(value)) {
279
+ return unitlessNumberProperties.has(prop) ? String(value) : `${value}px`
280
+ }
281
+ errors.push({
282
+ code: 'unsupported-legacy-value',
283
+ path,
284
+ message: `non-finite number ${String(value)} cannot become a CSS payload`,
285
+ })
286
+ return null
287
+ }
288
+
289
+ errors.push({
290
+ code: 'unsupported-legacy-value',
291
+ path,
292
+ message: `legacy condition value for "${prop}" must be a string or finite number`,
293
+ })
294
+ return null
295
+ }
296
+
297
+ export function convertLegacyConditionProp(
298
+ propName: string,
299
+ value: unknown,
300
+ options: ConvertLegacyConditionOptions
301
+ ): LegacyConditionResult | null {
302
+ const root = resolveLegacyCondition(propName, options.registry)
303
+ if (!root.recognized) return null
304
+
305
+ const result: LegacyConditionResult = { contributions: [], errors: [] }
306
+ if ('error' in root) {
307
+ result.errors.push({ ...root.error, path: propName })
308
+ return result
309
+ }
310
+ if (!isConditionObject(value)) {
311
+ result.errors.push({
312
+ code: 'legacy-condition-object',
313
+ path: propName,
314
+ message: `legacy condition "${propName}" must contain a style object`,
315
+ })
316
+ return result
317
+ }
318
+
319
+ const modifiers = root.modifiers.slice()
320
+ const visit = (object: Record<string, unknown>, objectPath: string): void => {
321
+ for (const childProp in object) {
322
+ const childValue = object[childProp]
323
+ const childPath = `${objectPath}.${childProp}`
324
+ const condition = resolveLegacyCondition(childProp, options.registry)
325
+
326
+ if (condition.recognized) {
327
+ if ('error' in condition) {
328
+ result.errors.push({ ...condition.error, path: childPath })
329
+ continue
330
+ }
331
+ if (!isConditionObject(childValue)) {
332
+ result.errors.push({
333
+ code: 'legacy-condition-object',
334
+ path: childPath,
335
+ message: `legacy condition "${childProp}" must contain a style object`,
336
+ })
337
+ continue
338
+ }
339
+ modifiers.push(...condition.modifiers)
340
+ visit(childValue, childPath)
341
+ modifiers.length -= condition.modifiers.length
342
+ continue
343
+ }
344
+
345
+ const payload = convertStyleValue(childProp, childValue, childPath, result.errors)
346
+ if (payload !== null) {
347
+ result.contributions.push({
348
+ prop: childProp,
349
+ clause: { modifiers: modifiers.slice(), payload },
350
+ })
351
+ }
352
+ }
353
+ }
354
+
355
+ visit(value, propName)
356
+ return result
357
+ }
@@ -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
+ }