@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,275 @@
1
+ import { Node, SyntaxKind } from "ts-morph";
2
+ import { numericValue, unwrapExpression } from "./expressions.mjs";
3
+ import { parseTransformString, sharedPayload } from "./grammar.mjs";
4
+
5
+ const staticTransformOperations = /* @__PURE__ */ new Set([
6
+ "perspective",
7
+ "rotate",
8
+ "rotateX",
9
+ "rotateY",
10
+ "rotateZ",
11
+ "scale",
12
+ "scaleX",
13
+ "scaleY",
14
+ "skewX",
15
+ "skewY",
16
+ "translateX",
17
+ "translateY"
18
+ ]);
19
+ function blocked(code, detail) {
20
+ return {
21
+ payload: null,
22
+ blocked: {
23
+ code,
24
+ detail
25
+ }
26
+ };
27
+ }
28
+ function staticString(expression) {
29
+ const current = unwrapExpression(expression);
30
+ return Node.isStringLiteral(current) || Node.isNoSubstitutionTemplateLiteral(current) ? current.getLiteralValue() : null;
31
+ }
32
+ function hasOnlyStringValues(expression) {
33
+ const current = unwrapExpression(expression);
34
+ if (Node.isStringLiteral(current) || Node.isNoSubstitutionTemplateLiteral(current) || Node.isTemplateExpression(current)) {
35
+ return true;
36
+ }
37
+ const type = current.getType();
38
+ const parts = type.isUnion() ? type.getUnionTypes() : [type];
39
+ let strings = 0;
40
+ for (const part of parts) {
41
+ if (part.isNull() || part.isUndefined()) continue;
42
+ if (!part.isString() && !part.isStringLiteral()) return false;
43
+ strings++;
44
+ }
45
+ return strings > 0;
46
+ }
47
+ function staticTransformArray(expression, source) {
48
+ const array = unwrapExpression(expression);
49
+ if (!Node.isArrayLiteralExpression(array)) {
50
+ return blocked("structured-transform-dynamic", `transform value "${source}" is not an inline transform array; keep dynamic and Animated arrays authored`);
51
+ }
52
+ const functions = [];
53
+ for (const element of array.getElements()) {
54
+ if (!Node.isObjectLiteralExpression(element)) {
55
+ return blocked("structured-transform-dynamic", `transform value "${source}" contains a spread or computed entry; keep dynamic and Animated arrays authored`);
56
+ }
57
+ const properties = element.getProperties();
58
+ if (properties.length !== 1 || !Node.isPropertyAssignment(properties[0])) {
59
+ return blocked("structured-transform-entry", `transform value "${source}" must have exactly one static operation per array entry`);
60
+ }
61
+ const property = properties[0];
62
+ const nameNode = property.getNameNode();
63
+ if (!Node.isIdentifier(nameNode) && !Node.isStringLiteral(nameNode)) {
64
+ return blocked("structured-transform-entry", `transform value "${source}" contains a computed operation name`);
65
+ }
66
+ const operation = Node.isStringLiteral(nameNode) ? nameNode.getLiteralValue() : nameNode.getText();
67
+ const initializer = unwrapExpression(property.getInitializerOrThrow());
68
+ if (operation === "matrix" || operation === "matrix3d") {
69
+ return blocked("structured-transform-matrix", `transform value "${source}" contains "${operation}", whose React Native array and portable CSS function shapes do not match; keep it authored`);
70
+ }
71
+ if (!staticTransformOperations.has(operation)) {
72
+ return blocked("structured-transform-operation", `transform value "${source}" uses unsupported operation "${operation}"`);
73
+ }
74
+ const number = numericValue(initializer);
75
+ let value;
76
+ if (number !== null) {
77
+ if (operation === "rotate" || operation === "rotateX" || operation === "rotateY" || operation === "rotateZ" || operation === "skewX" || operation === "skewY") {
78
+ return blocked("structured-transform-unit", `transform operation "${operation}" in "${source}" needs an explicit deg or rad string`);
79
+ }
80
+ value = operation === "scale" || operation === "scaleX" || operation === "scaleY" ? String(number) : `${number}px`;
81
+ } else if (Node.isStringLiteral(initializer) || Node.isNoSubstitutionTemplateLiteral(initializer)) {
82
+ value = initializer.getLiteralValue();
83
+ } else {
84
+ return blocked("structured-transform-dynamic", `transform operation "${operation}" in "${source}" is computed; keep dynamic and Animated arrays authored`);
85
+ }
86
+ functions.push(`${operation}(${value})`);
87
+ }
88
+ if (functions.length === 0) {
89
+ return blocked("structured-transform-empty", `transform value "${source}" is empty; no portable flat base can preserve its reset semantics`);
90
+ }
91
+ const payload = functions.join(" ");
92
+ const parsed = parseTransformString(payload);
93
+ if (parsed.errors.length) {
94
+ return blocked(`structured-transform-${parsed.errors[0].code}`, `${parsed.errors[0].message}; keep "${source}" authored`);
95
+ }
96
+ return {
97
+ payload,
98
+ blocked: null
99
+ };
100
+ }
101
+ function staticFontVariantArray(expression, source) {
102
+ const array = unwrapExpression(expression);
103
+ if (!Node.isArrayLiteralExpression(array)) {
104
+ return blocked("structured-font-variant-dynamic", `fontVariant value "${source}" is not an inline string array`);
105
+ }
106
+ const variants = [];
107
+ for (const element of array.getElements()) {
108
+ const variant = staticString(element);
109
+ if (variant === null) {
110
+ return blocked("structured-font-variant-dynamic", `fontVariant value "${source}" contains a computed entry; keep it authored`);
111
+ }
112
+ variants.push(variant);
113
+ }
114
+ return variants.length ? {
115
+ payload: variants.join(" "),
116
+ blocked: null
117
+ } : blocked("structured-font-variant-empty", `fontVariant value "${source}" is empty; no CSS token list preserves that reset`);
118
+ }
119
+ function staticBackgroundImageArray(expression, source) {
120
+ const array = unwrapExpression(expression);
121
+ if (!Node.isArrayLiteralExpression(array)) {
122
+ return blocked("structured-background-image-dynamic", `backgroundImage value "${source}" is not an inline gradient array`);
123
+ }
124
+ if (array.getElements().length !== 1) {
125
+ return blocked("structured-background-image-layers", `backgroundImage value "${source}" must contain exactly one gradient; native conditional evaluation does not accept multiple layers`);
126
+ }
127
+ const gradient = array.getElements()[0];
128
+ if (!Node.isObjectLiteralExpression(gradient)) {
129
+ return blocked("structured-background-image-dynamic", `backgroundImage value "${source}" contains a spread or computed gradient`);
130
+ }
131
+ const fields = /* @__PURE__ */ new Map();
132
+ for (const member of gradient.getProperties()) {
133
+ if (!Node.isPropertyAssignment(member)) {
134
+ return blocked("structured-background-image-dynamic", `backgroundImage value "${source}" contains a spread or computed gradient field`);
135
+ }
136
+ const nameNode = member.getNameNode();
137
+ if (!Node.isIdentifier(nameNode) && !Node.isStringLiteral(nameNode)) {
138
+ return blocked("structured-background-image-dynamic", `backgroundImage value "${source}" contains a computed gradient field`);
139
+ }
140
+ const name = Node.isStringLiteral(nameNode) ? nameNode.getLiteralValue() : nameNode.getText();
141
+ if (fields.has(name) || name !== "type" && name !== "direction" && name !== "colorStops") {
142
+ return blocked("structured-background-image-shape", `backgroundImage value "${source}" contains unsupported field "${name}"`);
143
+ }
144
+ fields.set(name, member.getInitializerOrThrow());
145
+ }
146
+ const type = fields.get("type");
147
+ const typeName = type ? staticString(type) : null;
148
+ if (typeName !== "linear-gradient") {
149
+ return blocked("structured-background-image-kind", `backgroundImage value "${source}" is not a static linear-gradient object`);
150
+ }
151
+ const parts = [];
152
+ const direction = fields.get("direction");
153
+ if (direction) {
154
+ const value = staticString(direction);
155
+ if (value === null) {
156
+ return blocked("structured-background-image-dynamic", `backgroundImage value "${source}" has a computed direction`);
157
+ }
158
+ parts.push(value);
159
+ }
160
+ const colorStopsExpression = fields.get("colorStops");
161
+ const colorStops = colorStopsExpression ? unwrapExpression(colorStopsExpression) : void 0;
162
+ if (!colorStops || !Node.isArrayLiteralExpression(colorStops)) {
163
+ return blocked("structured-background-image-dynamic", `backgroundImage value "${source}" does not have an inline colorStops array`);
164
+ }
165
+ if (colorStops.getElements().length < 2) {
166
+ return blocked("structured-background-image-stops", `backgroundImage value "${source}" needs at least two color stops`);
167
+ }
168
+ const stopElements = colorStops.getElements();
169
+ for (const [stopIndex, stop] of stopElements.entries()) {
170
+ if (!Node.isObjectLiteralExpression(stop)) {
171
+ return blocked("structured-background-image-dynamic", `backgroundImage value "${source}" contains a spread or computed color stop`);
172
+ }
173
+ let color;
174
+ let hasColor = false;
175
+ let positions = [];
176
+ let hasPositions = false;
177
+ for (const member of stop.getProperties()) {
178
+ if (!Node.isPropertyAssignment(member)) {
179
+ return blocked("structured-background-image-dynamic", `backgroundImage value "${source}" contains a spread or computed color-stop field`);
180
+ }
181
+ const nameNode = member.getNameNode();
182
+ if (!Node.isIdentifier(nameNode) && !Node.isStringLiteral(nameNode)) {
183
+ return blocked("structured-background-image-dynamic", `backgroundImage value "${source}" contains a computed color-stop field`);
184
+ }
185
+ const name = Node.isStringLiteral(nameNode) ? nameNode.getLiteralValue() : nameNode.getText();
186
+ if (name === "color" && !hasColor) {
187
+ hasColor = true;
188
+ const value = unwrapExpression(member.getInitializerOrThrow());
189
+ if (value.getKind() === SyntaxKind.NullKeyword) {
190
+ color = null;
191
+ continue;
192
+ }
193
+ const staticColor = staticString(value);
194
+ if (staticColor === null) {
195
+ return blocked("structured-background-image-dynamic", `backgroundImage value "${source}" contains a computed color`);
196
+ }
197
+ color = staticColor;
198
+ continue;
199
+ }
200
+ if (name === "positions" && !hasPositions) {
201
+ hasPositions = true;
202
+ const value = unwrapExpression(member.getInitializerOrThrow());
203
+ if (!Node.isArrayLiteralExpression(value)) {
204
+ return blocked("structured-background-image-dynamic", `backgroundImage value "${source}" contains computed color-stop positions`);
205
+ }
206
+ positions = [];
207
+ for (const positionExpression of value.getElements()) {
208
+ const number = numericValue(positionExpression);
209
+ if (number !== null) {
210
+ positions.push(`${number}px`);
211
+ continue;
212
+ }
213
+ const position = staticString(positionExpression);
214
+ if (position === null) {
215
+ return blocked("structured-background-image-dynamic", `backgroundImage value "${source}" contains a computed color-stop position`);
216
+ }
217
+ if (!position.endsWith("%")) {
218
+ return blocked("structured-background-image-position", `backgroundImage value "${source}" has position "${position}"; React Native gradient objects accept numeric points or percentage strings`);
219
+ }
220
+ positions.push(position);
221
+ }
222
+ continue;
223
+ }
224
+ return blocked("structured-background-image-shape", `backgroundImage value "${source}" contains unsupported or repeated color-stop field "${name}"`);
225
+ }
226
+ if (!hasColor || color === void 0) {
227
+ return blocked("structured-background-image-shape", `backgroundImage value "${source}" contains a color stop without a color`);
228
+ }
229
+ if (color === null) {
230
+ if (positions.length !== 1 || stopIndex === 0 || stopIndex === stopElements.length - 1) {
231
+ return blocked("structured-background-image-hint", `backgroundImage value "${source}" has an invalid transition hint; it needs one position between two colored stops`);
232
+ }
233
+ parts.push(positions[0]);
234
+ } else if (positions.length > 2) {
235
+ for (const position of positions) parts.push(`${color} ${position}`);
236
+ } else {
237
+ parts.push([color, ...positions].join(" "));
238
+ }
239
+ }
240
+ return {
241
+ payload: `linear-gradient(${parts.join(", ")})`,
242
+ blocked: null
243
+ };
244
+ }
245
+ const structuredNativeSerializers = Object.freeze({
246
+ backgroundImage: staticBackgroundImageArray,
247
+ fontVariant: staticFontVariantArray,
248
+ transform: staticTransformArray
249
+ });
250
+ function classifyStructuredNativeValue(property, expression, source, registry) {
251
+ const serializer = structuredNativeSerializers[property];
252
+ if (serializer) {
253
+ if (hasOnlyStringValues(expression)) return null;
254
+ const result = serializer(expression, source);
255
+ if (result.payload === null) return result;
256
+ const flattened = sharedPayload(property, result.payload, registry);
257
+ const error = flattened.errors[0];
258
+ if (error || flattened.payload === null) {
259
+ return blocked(error?.code ?? "unsupported-structured-value", `${property}: ${error?.message ?? `"${result.payload}" has no flat spelling`}`);
260
+ }
261
+ return {
262
+ payload: flattened.payload,
263
+ blocked: null
264
+ };
265
+ }
266
+ const current = unwrapExpression(expression);
267
+ if (!Node.isObjectLiteralExpression(current) && !Node.isArrayLiteralExpression(current)) {
268
+ return null;
269
+ }
270
+ const code = property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
271
+ return blocked(`structured-${code}`, `${property} value "${source}" has no verified CSS-shaped migration rule; keep it authored`);
272
+ }
273
+
274
+ export { classifyStructuredNativeValue };
275
+ //# sourceMappingURL=structuredNative.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structuredNative.js","names":[],"sources":["structuredNative.js"],"sourcesContent":["import { Node, SyntaxKind } from \"ts-morph\";\nimport { numericValue, unwrapExpression } from \"./expressions\";\nimport { parseTransformString, sharedPayload } from \"./grammar\";\nconst staticTransformOperations = /* @__PURE__ */ new Set([\n \"perspective\",\n \"rotate\",\n \"rotateX\",\n \"rotateY\",\n \"rotateZ\",\n \"scale\",\n \"scaleX\",\n \"scaleY\",\n \"skewX\",\n \"skewY\",\n \"translateX\",\n \"translateY\"\n]);\nfunction blocked(code, detail) {\n return { payload: null, blocked: { code, detail } };\n}\nfunction staticString(expression) {\n const current = unwrapExpression(expression);\n return Node.isStringLiteral(current) || Node.isNoSubstitutionTemplateLiteral(current) ? current.getLiteralValue() : null;\n}\nfunction hasOnlyStringValues(expression) {\n const current = unwrapExpression(expression);\n if (Node.isStringLiteral(current) || Node.isNoSubstitutionTemplateLiteral(current) || Node.isTemplateExpression(current)) {\n return true;\n }\n const type = current.getType();\n const parts = type.isUnion() ? type.getUnionTypes() : [type];\n let strings = 0;\n for (const part of parts) {\n if (part.isNull() || part.isUndefined()) continue;\n if (!part.isString() && !part.isStringLiteral()) return false;\n strings++;\n }\n return strings > 0;\n}\nfunction staticTransformArray(expression, source) {\n const array = unwrapExpression(expression);\n if (!Node.isArrayLiteralExpression(array)) {\n return blocked(\n \"structured-transform-dynamic\",\n `transform value \"${source}\" is not an inline transform array; keep dynamic and Animated arrays authored`\n );\n }\n const functions = [];\n for (const element of array.getElements()) {\n if (!Node.isObjectLiteralExpression(element)) {\n return blocked(\n \"structured-transform-dynamic\",\n `transform value \"${source}\" contains a spread or computed entry; keep dynamic and Animated arrays authored`\n );\n }\n const properties = element.getProperties();\n if (properties.length !== 1 || !Node.isPropertyAssignment(properties[0])) {\n return blocked(\n \"structured-transform-entry\",\n `transform value \"${source}\" must have exactly one static operation per array entry`\n );\n }\n const property = properties[0];\n const nameNode = property.getNameNode();\n if (!Node.isIdentifier(nameNode) && !Node.isStringLiteral(nameNode)) {\n return blocked(\n \"structured-transform-entry\",\n `transform value \"${source}\" contains a computed operation name`\n );\n }\n const operation = Node.isStringLiteral(nameNode) ? nameNode.getLiteralValue() : nameNode.getText();\n const initializer = unwrapExpression(property.getInitializerOrThrow());\n if (operation === \"matrix\" || operation === \"matrix3d\") {\n return blocked(\n \"structured-transform-matrix\",\n `transform value \"${source}\" contains \"${operation}\", whose React Native array and portable CSS function shapes do not match; keep it authored`\n );\n }\n if (!staticTransformOperations.has(operation)) {\n return blocked(\n \"structured-transform-operation\",\n `transform value \"${source}\" uses unsupported operation \"${operation}\"`\n );\n }\n const number = numericValue(initializer);\n let value;\n if (number !== null) {\n if (operation === \"rotate\" || operation === \"rotateX\" || operation === \"rotateY\" || operation === \"rotateZ\" || operation === \"skewX\" || operation === \"skewY\") {\n return blocked(\n \"structured-transform-unit\",\n `transform operation \"${operation}\" in \"${source}\" needs an explicit deg or rad string`\n );\n }\n value = operation === \"scale\" || operation === \"scaleX\" || operation === \"scaleY\" ? String(number) : `${number}px`;\n } else if (Node.isStringLiteral(initializer) || Node.isNoSubstitutionTemplateLiteral(initializer)) {\n value = initializer.getLiteralValue();\n } else {\n return blocked(\n \"structured-transform-dynamic\",\n `transform operation \"${operation}\" in \"${source}\" is computed; keep dynamic and Animated arrays authored`\n );\n }\n functions.push(`${operation}(${value})`);\n }\n if (functions.length === 0) {\n return blocked(\n \"structured-transform-empty\",\n `transform value \"${source}\" is empty; no portable flat base can preserve its reset semantics`\n );\n }\n const payload = functions.join(\" \");\n const parsed = parseTransformString(payload);\n if (parsed.errors.length) {\n return blocked(\n `structured-transform-${parsed.errors[0].code}`,\n `${parsed.errors[0].message}; keep \"${source}\" authored`\n );\n }\n return { payload, blocked: null };\n}\nfunction staticFontVariantArray(expression, source) {\n const array = unwrapExpression(expression);\n if (!Node.isArrayLiteralExpression(array)) {\n return blocked(\n \"structured-font-variant-dynamic\",\n `fontVariant value \"${source}\" is not an inline string array`\n );\n }\n const variants = [];\n for (const element of array.getElements()) {\n const variant = staticString(element);\n if (variant === null) {\n return blocked(\n \"structured-font-variant-dynamic\",\n `fontVariant value \"${source}\" contains a computed entry; keep it authored`\n );\n }\n variants.push(variant);\n }\n return variants.length ? { payload: variants.join(\" \"), blocked: null } : blocked(\n \"structured-font-variant-empty\",\n `fontVariant value \"${source}\" is empty; no CSS token list preserves that reset`\n );\n}\nfunction staticBackgroundImageArray(expression, source) {\n const array = unwrapExpression(expression);\n if (!Node.isArrayLiteralExpression(array)) {\n return blocked(\n \"structured-background-image-dynamic\",\n `backgroundImage value \"${source}\" is not an inline gradient array`\n );\n }\n if (array.getElements().length !== 1) {\n return blocked(\n \"structured-background-image-layers\",\n `backgroundImage value \"${source}\" must contain exactly one gradient; native conditional evaluation does not accept multiple layers`\n );\n }\n const gradient = array.getElements()[0];\n if (!Node.isObjectLiteralExpression(gradient)) {\n return blocked(\n \"structured-background-image-dynamic\",\n `backgroundImage value \"${source}\" contains a spread or computed gradient`\n );\n }\n const fields = /* @__PURE__ */ new Map();\n for (const member of gradient.getProperties()) {\n if (!Node.isPropertyAssignment(member)) {\n return blocked(\n \"structured-background-image-dynamic\",\n `backgroundImage value \"${source}\" contains a spread or computed gradient field`\n );\n }\n const nameNode = member.getNameNode();\n if (!Node.isIdentifier(nameNode) && !Node.isStringLiteral(nameNode)) {\n return blocked(\n \"structured-background-image-dynamic\",\n `backgroundImage value \"${source}\" contains a computed gradient field`\n );\n }\n const name = Node.isStringLiteral(nameNode) ? nameNode.getLiteralValue() : nameNode.getText();\n if (fields.has(name) || name !== \"type\" && name !== \"direction\" && name !== \"colorStops\") {\n return blocked(\n \"structured-background-image-shape\",\n `backgroundImage value \"${source}\" contains unsupported field \"${name}\"`\n );\n }\n fields.set(name, member.getInitializerOrThrow());\n }\n const type = fields.get(\"type\");\n const typeName = type ? staticString(type) : null;\n if (typeName !== \"linear-gradient\") {\n return blocked(\n \"structured-background-image-kind\",\n `backgroundImage value \"${source}\" is not a static linear-gradient object`\n );\n }\n const parts = [];\n const direction = fields.get(\"direction\");\n if (direction) {\n const value = staticString(direction);\n if (value === null) {\n return blocked(\n \"structured-background-image-dynamic\",\n `backgroundImage value \"${source}\" has a computed direction`\n );\n }\n parts.push(value);\n }\n const colorStopsExpression = fields.get(\"colorStops\");\n const colorStops = colorStopsExpression ? unwrapExpression(colorStopsExpression) : void 0;\n if (!colorStops || !Node.isArrayLiteralExpression(colorStops)) {\n return blocked(\n \"structured-background-image-dynamic\",\n `backgroundImage value \"${source}\" does not have an inline colorStops array`\n );\n }\n if (colorStops.getElements().length < 2) {\n return blocked(\n \"structured-background-image-stops\",\n `backgroundImage value \"${source}\" needs at least two color stops`\n );\n }\n const stopElements = colorStops.getElements();\n for (const [stopIndex, stop] of stopElements.entries()) {\n if (!Node.isObjectLiteralExpression(stop)) {\n return blocked(\n \"structured-background-image-dynamic\",\n `backgroundImage value \"${source}\" contains a spread or computed color stop`\n );\n }\n let color;\n let hasColor = false;\n let positions = [];\n let hasPositions = false;\n for (const member of stop.getProperties()) {\n if (!Node.isPropertyAssignment(member)) {\n return blocked(\n \"structured-background-image-dynamic\",\n `backgroundImage value \"${source}\" contains a spread or computed color-stop field`\n );\n }\n const nameNode = member.getNameNode();\n if (!Node.isIdentifier(nameNode) && !Node.isStringLiteral(nameNode)) {\n return blocked(\n \"structured-background-image-dynamic\",\n `backgroundImage value \"${source}\" contains a computed color-stop field`\n );\n }\n const name = Node.isStringLiteral(nameNode) ? nameNode.getLiteralValue() : nameNode.getText();\n if (name === \"color\" && !hasColor) {\n hasColor = true;\n const value = unwrapExpression(member.getInitializerOrThrow());\n if (value.getKind() === SyntaxKind.NullKeyword) {\n color = null;\n continue;\n }\n const staticColor = staticString(value);\n if (staticColor === null) {\n return blocked(\n \"structured-background-image-dynamic\",\n `backgroundImage value \"${source}\" contains a computed color`\n );\n }\n color = staticColor;\n continue;\n }\n if (name === \"positions\" && !hasPositions) {\n hasPositions = true;\n const value = unwrapExpression(member.getInitializerOrThrow());\n if (!Node.isArrayLiteralExpression(value)) {\n return blocked(\n \"structured-background-image-dynamic\",\n `backgroundImage value \"${source}\" contains computed color-stop positions`\n );\n }\n positions = [];\n for (const positionExpression of value.getElements()) {\n const number = numericValue(positionExpression);\n if (number !== null) {\n positions.push(`${number}px`);\n continue;\n }\n const position = staticString(positionExpression);\n if (position === null) {\n return blocked(\n \"structured-background-image-dynamic\",\n `backgroundImage value \"${source}\" contains a computed color-stop position`\n );\n }\n if (!position.endsWith(\"%\")) {\n return blocked(\n \"structured-background-image-position\",\n `backgroundImage value \"${source}\" has position \"${position}\"; React Native gradient objects accept numeric points or percentage strings`\n );\n }\n positions.push(position);\n }\n continue;\n }\n return blocked(\n \"structured-background-image-shape\",\n `backgroundImage value \"${source}\" contains unsupported or repeated color-stop field \"${name}\"`\n );\n }\n if (!hasColor || color === void 0) {\n return blocked(\n \"structured-background-image-shape\",\n `backgroundImage value \"${source}\" contains a color stop without a color`\n );\n }\n if (color === null) {\n if (positions.length !== 1 || stopIndex === 0 || stopIndex === stopElements.length - 1) {\n return blocked(\n \"structured-background-image-hint\",\n `backgroundImage value \"${source}\" has an invalid transition hint; it needs one position between two colored stops`\n );\n }\n parts.push(positions[0]);\n } else if (positions.length > 2) {\n for (const position of positions) parts.push(`${color} ${position}`);\n } else {\n parts.push([color, ...positions].join(\" \"));\n }\n }\n return { payload: `linear-gradient(${parts.join(\", \")})`, blocked: null };\n}\nconst structuredNativeSerializers = Object.freeze({\n backgroundImage: staticBackgroundImageArray,\n fontVariant: staticFontVariantArray,\n transform: staticTransformArray\n});\nfunction classifyStructuredNativeValue(property, expression, source, registry) {\n const serializer = structuredNativeSerializers[property];\n if (serializer) {\n if (hasOnlyStringValues(expression)) return null;\n const result = serializer(expression, source);\n if (result.payload === null) return result;\n const flattened = sharedPayload(property, result.payload, registry);\n const error = flattened.errors[0];\n if (error || flattened.payload === null) {\n return blocked(\n error?.code ?? \"unsupported-structured-value\",\n `${property}: ${error?.message ?? `\"${result.payload}\" has no flat spelling`}`\n );\n }\n return { payload: flattened.payload, blocked: null };\n }\n const current = unwrapExpression(expression);\n if (!Node.isObjectLiteralExpression(current) && !Node.isArrayLiteralExpression(current)) {\n return null;\n }\n const code = property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);\n return blocked(\n `structured-${code}`,\n `${property} value \"${source}\" has no verified CSS-shaped migration rule; keep it authored`\n );\n}\nexport {\n classifyStructuredNativeValue\n};\n//# sourceMappingURL=structuredNative.js.map\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,16 +1,44 @@
1
1
  {
2
2
  "name": "@tamagui/codemod-flat-values",
3
- "version": "0.0.0-bootstrap.0",
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.643.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
- "README.md"
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.643.1",
30
+ "@tamagui/language-service": "3.0.0-beta.643.1",
31
+ "@tamagui/shorthands": "3.0.0-beta.643.1",
32
+ "@tamagui/style-grammar": "3.0.0-beta.643.1",
33
+ "ts-morph": "^28.0.0"
34
+ },
35
+ "devDependencies": {
36
+ "@tamagui/build": "3.0.0-beta.643.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,21 @@
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
+ } as const
9
+
10
+ const builtInTokenPattern = new RegExp(
11
+ `\\$(${Object.keys(v6CodemodBuiltInNameReplacements).join('|')})(?![\\w-])`,
12
+ 'g'
13
+ )
14
+
15
+ export function replaceV6BuiltInTokens(value: string): string {
16
+ return value.replace(
17
+ builtInTokenPattern,
18
+ (_, name: keyof typeof v6CodemodBuiltInNameReplacements) =>
19
+ `$${v6CodemodBuiltInNameReplacements[name]}`
20
+ )
21
+ }
@@ -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
+ }