@yahoo/uds 0.4.1 → 0.4.3
Sign up to get free protection for your applications and to get access to all the features.
- package/cli/README.md +10 -0
- package/cli/codemods/propsToClass.test.tsx +97 -0
- package/cli/codemods/propsToClass.ts +221 -0
- package/cli/codemods/utils/sizingPropToClassMap.ts +89 -0
- package/cli/commands/codemod/codemod.ts +87 -0
- package/cli/commands/codemod/sizingProps.ts +17 -0
- package/cli/preload.ts +1 -1
- package/cli/utils/getCommandHelp.ts +42 -3
- package/cli/utils/getDirChoices.ts +22 -0
- package/cli/utils/purgeCSS.ts +2 -2
- package/dist/{Image.native-DUAFJodS.d.ts → Image.native-CyJCMvgr.d.cts} +2 -2
- package/dist/{Image.native-B3I4JoH3.d.cts → Image.native-M50LC-q7.d.ts} +2 -2
- package/dist/{VStack-DMb_RGRS.d.ts → VStack-R8SACz7K.d.cts} +1 -1
- package/dist/{VStack-BHlRUsOR.d.cts → VStack-fSE_ityd.d.ts} +1 -1
- package/dist/experimental/index.cjs +1 -1
- package/dist/experimental/index.d.cts +2 -2
- package/dist/experimental/index.d.ts +2 -2
- package/dist/experimental/index.js +1 -1
- package/dist/experimental/index.native.cjs +1 -1
- package/dist/experimental/index.native.d.cts +3 -3
- package/dist/experimental/index.native.d.ts +3 -3
- package/dist/experimental/index.native.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +43 -16
- package/dist/index.d.ts +43 -16
- package/dist/index.js +1 -1
- package/dist/{index.native-Bm-r2Dpa.d.cts → index.native-BstClKbg.d.cts} +1 -1
- package/dist/{index.native-BTfOSmUx.d.ts → index.native-DtdUtCNu.d.ts} +1 -1
- package/dist/index.native.cjs +1 -1
- package/dist/index.native.d.cts +75 -9
- package/dist/index.native.d.ts +75 -9
- package/dist/index.native.js +1 -1
- package/dist/styles/globals.css +1 -0
- package/dist/{tailwindPlugin.d.ts → tailwind/plugin.d.cts} +1 -1
- package/dist/{tailwindPlugin.d.cts → tailwind/plugin.d.ts} +1 -1
- package/dist/tokens/index.d.cts +15 -3
- package/dist/tokens/index.d.ts +15 -3
- package/dist/tokens/index.native.d.cts +2 -2
- package/dist/tokens/index.native.d.ts +2 -2
- package/dist/tokens/parseTokens.d.cts +1 -1
- package/dist/tokens/parseTokens.d.ts +1 -1
- package/dist/{types-COiuE8XK.d.ts → types-BN1X8QcB.d.cts} +3 -23
- package/dist/{types-COiuE8XK.d.cts → types-BN1X8QcB.d.ts} +3 -23
- package/dist/types-DRP5tWZG.d.cts +375 -0
- package/dist/types-DRP5tWZG.d.ts +375 -0
- package/package.json +21 -39
- package/dist/styles/fonts.css +0 -1
- package/dist/styles/fonts.d.cts +0 -2
- package/dist/styles/fonts.d.ts +0 -2
- package/dist/styles/globals.d.cts +0 -2
- package/dist/styles/globals.d.ts +0 -2
- package/dist/styles/toast.d.cts +0 -2
- package/dist/styles/toast.d.ts +0 -2
- package/dist/tailwindPurge/utils.cjs +0 -1
- package/dist/tailwindPurge/utils.d.cts +0 -25
- package/dist/tailwindPurge/utils.d.ts +0 -25
- package/dist/tailwindPurge/utils.js +0 -1
- package/dist/tailwindPurge.cjs +0 -4
- package/dist/tailwindPurge.d.cts +0 -20
- package/dist/tailwindPurge.d.ts +0 -20
- package/dist/tailwindPurge.js +0 -4
- package/dist/tokens/parseTokens.native.cjs +0 -1
- package/dist/tokens/parseTokens.native.d.cts +0 -399
- package/dist/tokens/parseTokens.native.d.ts +0 -399
- package/dist/tokens/parseTokens.native.js +0 -1
- package/dist/types-FO65RM-W.d.cts +0 -38
- package/dist/types-FO65RM-W.d.ts +0 -38
- /package/dist/{tailwindPlugin.cjs → tailwind/plugin.cjs} +0 -0
- /package/dist/{tailwindPlugin.js → tailwind/plugin.js} +0 -0
package/cli/README.md
CHANGED
@@ -98,6 +98,16 @@ uds purge
|
|
98
98
|
ENABLED_SCALE_AND_COLOR_MODES="dark,large" uds purge
|
99
99
|
```
|
100
100
|
|
101
|
+
### Codemod
|
102
|
+
|
103
|
+
The `uds codemod` command is here to help you run one-off codemods. In the future, we'll likely apply codemodes via `uds migrate` for you, but this will provide more fine grained control over applying codemods to help you deal with breaking changes to our system. We're flying fast right now, and investing too much energy in full blown migrations is silly as things will just keep changing, but this is a step into an _easy_ migration future!
|
104
|
+
|
105
|
+
Any file added to commands/codemod will be available in prompt land.
|
106
|
+
|
107
|
+
```shell
|
108
|
+
uds codemod
|
109
|
+
```
|
110
|
+
|
101
111
|
### Expo (WIP)
|
102
112
|
|
103
113
|
The `uds expo` command is for building and launching React Native apps using Expo.
|
@@ -0,0 +1,97 @@
|
|
1
|
+
import { unlink } from 'node:fs';
|
2
|
+
import path from 'node:path';
|
3
|
+
|
4
|
+
import * as bluebun from 'bluebun';
|
5
|
+
import { afterEach, beforeAll, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
|
6
|
+
import { IndentationText, Project } from 'ts-morph';
|
7
|
+
|
8
|
+
import { propsToClass } from './propsToClass';
|
9
|
+
import { sizingPropToClassMap } from './utils/sizingPropToClassMap';
|
10
|
+
|
11
|
+
const FILE_NAME = 'PropsToClass.mock.tsx';
|
12
|
+
const FILE_BEFORE = `
|
13
|
+
import { Button, cx, HStack, Text } from '@yahoo/uds';
|
14
|
+
const textClassName = cx('text-primary');
|
15
|
+
const otherProps = { height: 'fit' };
|
16
|
+
|
17
|
+
export const PageA = () => {
|
18
|
+
return (
|
19
|
+
<HStack width="full" maxHeight="screen">
|
20
|
+
<Button minWidth="full">Click me</Button>
|
21
|
+
<Text width="10/12" className={textClassName}>Some text</Text>
|
22
|
+
<Text {...otherProps}>Some text</Text>
|
23
|
+
</HStack>
|
24
|
+
);
|
25
|
+
}
|
26
|
+
`.trim();
|
27
|
+
const FILE_AFTER = `
|
28
|
+
import { Button, cx, HStack, Text } from '@yahoo/uds';
|
29
|
+
const textClassName = cx('text-primary');
|
30
|
+
const otherProps = { height: 'fit' };
|
31
|
+
|
32
|
+
export const PageA = () => {
|
33
|
+
return (
|
34
|
+
<HStack className="w-full max-h-screen">
|
35
|
+
<Button className="min-w-full">Click me</Button>
|
36
|
+
{/* 🙏 TODO: Add w-10/12 to your className attribute */}
|
37
|
+
<Text className={textClassName}>Some text</Text>
|
38
|
+
<Text {...otherProps}>Some text</Text>
|
39
|
+
</HStack>
|
40
|
+
);
|
41
|
+
}
|
42
|
+
`.trim();
|
43
|
+
|
44
|
+
describe('propsToClass', () => {
|
45
|
+
const workspaceDir = Bun.env.PWD;
|
46
|
+
const srcDir = path.join(workspaceDir, 'tsconfig.json');
|
47
|
+
const project = new Project({
|
48
|
+
tsConfigFilePath: srcDir,
|
49
|
+
manipulationSettings: { indentationText: IndentationText.TwoSpaces },
|
50
|
+
});
|
51
|
+
|
52
|
+
beforeAll(async () => {
|
53
|
+
// setup mocks
|
54
|
+
mock.module('bluebun', () => ({ print: () => 'mocked' }));
|
55
|
+
});
|
56
|
+
|
57
|
+
beforeEach(async () => {
|
58
|
+
// Setup files
|
59
|
+
project.createSourceFile(FILE_NAME, FILE_BEFORE, {
|
60
|
+
overwrite: true,
|
61
|
+
});
|
62
|
+
});
|
63
|
+
afterEach(async () => {
|
64
|
+
// teardown files
|
65
|
+
unlink(FILE_NAME, (err) => {
|
66
|
+
if (err) {
|
67
|
+
throw err;
|
68
|
+
}
|
69
|
+
});
|
70
|
+
});
|
71
|
+
|
72
|
+
it('converts props to classNames', async () => {
|
73
|
+
// Sanity check
|
74
|
+
const fileBefore = project.getSourceFile(FILE_NAME)?.getText();
|
75
|
+
expect(fileBefore).toEqual(FILE_BEFORE);
|
76
|
+
|
77
|
+
// Apply the codemod
|
78
|
+
await propsToClass({ propToClassMap: sizingPropToClassMap, project });
|
79
|
+
|
80
|
+
// Confirm it's been transformed
|
81
|
+
const fileAfter = project.getSourceFile(FILE_NAME)?.getText();
|
82
|
+
expect(fileAfter).toEqual(FILE_AFTER);
|
83
|
+
});
|
84
|
+
it('logs a warning when a complex expression is found', async () => {
|
85
|
+
// Spy on the bun logger
|
86
|
+
const spy = spyOn(bluebun, 'print');
|
87
|
+
|
88
|
+
// Apply the codemod
|
89
|
+
await propsToClass({
|
90
|
+
propToClassMap: sizingPropToClassMap,
|
91
|
+
project,
|
92
|
+
});
|
93
|
+
|
94
|
+
// We print 3 times for each complex expression, and 2 times for each spread expression
|
95
|
+
expect(spy).toHaveBeenCalledTimes(5);
|
96
|
+
});
|
97
|
+
});
|
@@ -0,0 +1,221 @@
|
|
1
|
+
import path from 'node:path';
|
2
|
+
|
3
|
+
import { blue, green, print, red, yellow } from 'bluebun';
|
4
|
+
import {
|
5
|
+
IndentationText,
|
6
|
+
JsxAttributeLike,
|
7
|
+
JsxElement,
|
8
|
+
JsxSpreadAttribute,
|
9
|
+
Node,
|
10
|
+
Project,
|
11
|
+
SyntaxKind,
|
12
|
+
} from 'ts-morph';
|
13
|
+
|
14
|
+
const throwComplexExpressionWarning = ({
|
15
|
+
attr,
|
16
|
+
element,
|
17
|
+
classNamesToAdd = [],
|
18
|
+
kind,
|
19
|
+
}: {
|
20
|
+
attr: JsxAttributeLike | JsxSpreadAttribute;
|
21
|
+
element: JsxElement;
|
22
|
+
classNamesToAdd?: string[];
|
23
|
+
kind?: 'className' | 'other';
|
24
|
+
}) => {
|
25
|
+
const sourceFile = element.getSourceFile();
|
26
|
+
const tagName = element.getFirstChildByKind(SyntaxKind.Identifier)?.getText();
|
27
|
+
const spreadExpression = Node.isJsxSpreadAttribute(attr) ? attr.getExpression() : undefined;
|
28
|
+
const initializerText = Node.isJsxAttribute(attr)
|
29
|
+
? (attr.getInitializer()?.getText() as string)
|
30
|
+
: 'UNKNOWN';
|
31
|
+
|
32
|
+
// Get the line number of the attribute and the element
|
33
|
+
const attrLineNumber = attr.getStartLineNumber();
|
34
|
+
const elementLineNumber = element.getStartLineNumber();
|
35
|
+
const isOneLiner = elementLineNumber === attrLineNumber;
|
36
|
+
|
37
|
+
// Get the position of the attribute and the indentation level of the element
|
38
|
+
const attrLinePos = attr.getStartLinePos();
|
39
|
+
const indentAmount = isOneLiner ? element.getIndentationLevel() : attr.getIndentationLevel();
|
40
|
+
const classNameComment = isOneLiner
|
41
|
+
? `{/* 🙏 TODO: Add ${classNamesToAdd} to your className attribute */}\n`
|
42
|
+
: `// 🙏 TODO: Add ${classNamesToAdd} to your className attribute\n`;
|
43
|
+
|
44
|
+
// Add a comment to the line above the attribute if this is a className issue
|
45
|
+
if (kind === 'className') {
|
46
|
+
sourceFile.insertText(attrLinePos, classNameComment);
|
47
|
+
sourceFile.indent(attrLinePos, indentAmount);
|
48
|
+
|
49
|
+
// Warn the user about the complex expression
|
50
|
+
print(red(`Yuh-oh.. we found a complex className: ${yellow(initializerText)}`));
|
51
|
+
print(
|
52
|
+
red(
|
53
|
+
`Please take a look at the ${blue(String(tagName))} component in ${blue(`${sourceFile.getBaseName()}:${elementLineNumber}`)} ${red('and update the className manually')}`,
|
54
|
+
),
|
55
|
+
);
|
56
|
+
print(
|
57
|
+
`${blue('Classes to add:\n' + classNamesToAdd.map((className) => `${green(`+ ${className}`)}`).join('\n'))}\n`,
|
58
|
+
);
|
59
|
+
}
|
60
|
+
|
61
|
+
if (spreadExpression) {
|
62
|
+
print(red(`Yuh-oh.. we found a spread expression: ${yellow(spreadExpression.getText())}`));
|
63
|
+
print(
|
64
|
+
red(
|
65
|
+
`Please take a look at the ${blue(String(tagName))} component in ${blue(`${sourceFile.getBaseName()}:${elementLineNumber}`)} ${red('and confirm the spread expression is correct')}\n`,
|
66
|
+
),
|
67
|
+
);
|
68
|
+
}
|
69
|
+
};
|
70
|
+
|
71
|
+
export const getProject = () => {
|
72
|
+
const workspaceDir = Bun.env.PWD;
|
73
|
+
const srcDir = path.join(workspaceDir, 'tsconfig.json');
|
74
|
+
return new Project({
|
75
|
+
tsConfigFilePath: srcDir,
|
76
|
+
manipulationSettings: { indentationText: IndentationText.TwoSpaces },
|
77
|
+
});
|
78
|
+
};
|
79
|
+
|
80
|
+
const getGlobPattern = (selectedDirs?: string[]) => {
|
81
|
+
let glob = './';
|
82
|
+
if (selectedDirs && selectedDirs.length === 1) {
|
83
|
+
glob += selectedDirs[0];
|
84
|
+
} else if (selectedDirs) {
|
85
|
+
glob += `{${selectedDirs.join(',')}}`;
|
86
|
+
}
|
87
|
+
glob += '/**/*.+(ts|tsx)';
|
88
|
+
|
89
|
+
return glob;
|
90
|
+
};
|
91
|
+
|
92
|
+
interface PropToClassMap {
|
93
|
+
selectedDirs?: string[];
|
94
|
+
project?: Project;
|
95
|
+
propToClassMap: {
|
96
|
+
[key: string]: {
|
97
|
+
[key: string]: string;
|
98
|
+
};
|
99
|
+
};
|
100
|
+
}
|
101
|
+
|
102
|
+
// Throw a warning for spread attributes
|
103
|
+
const throwSpreadAttributeWarning = ({ jsxElement }: { jsxElement: JsxElement }) => {
|
104
|
+
const spreadAttributes = jsxElement.getDescendantsOfKind(SyntaxKind.JsxSpreadAttribute);
|
105
|
+
spreadAttributes.forEach((attr) => {
|
106
|
+
throwComplexExpressionWarning({ attr, element: jsxElement, kind: 'other' });
|
107
|
+
});
|
108
|
+
};
|
109
|
+
|
110
|
+
export const propsToClass = async ({
|
111
|
+
propToClassMap,
|
112
|
+
selectedDirs,
|
113
|
+
project = getProject(),
|
114
|
+
}: PropToClassMap) => {
|
115
|
+
const glob = getGlobPattern(selectedDirs);
|
116
|
+
project.getSourceFiles(glob).forEach((sourceFile) => {
|
117
|
+
const importedComponents = new Set<string>();
|
118
|
+
|
119
|
+
// Collect imports from '@yahoo/uds'
|
120
|
+
sourceFile.getImportDeclarations().forEach((importDeclaration) => {
|
121
|
+
if (importDeclaration.getModuleSpecifierValue() === '@yahoo/uds') {
|
122
|
+
importDeclaration.getNamedImports().forEach((namedImport) => {
|
123
|
+
importedComponents.add(namedImport.getName());
|
124
|
+
});
|
125
|
+
}
|
126
|
+
});
|
127
|
+
|
128
|
+
// Handle JSX elements from the imported components
|
129
|
+
sourceFile.forEachDescendant((node) => {
|
130
|
+
// Spread attributes are not supported
|
131
|
+
if (node.getKind() === SyntaxKind.JsxElement) {
|
132
|
+
const jsxElement = node as JsxElement;
|
133
|
+
// TODO handle spread attributes more elegant
|
134
|
+
throwSpreadAttributeWarning({ jsxElement });
|
135
|
+
}
|
136
|
+
|
137
|
+
// All other JSX elements we want to handle
|
138
|
+
if (
|
139
|
+
node.getKind() === SyntaxKind.JsxOpeningElement ||
|
140
|
+
node.getKind() === SyntaxKind.JsxSelfClosingElement
|
141
|
+
) {
|
142
|
+
const jsxElement = node as JsxElement;
|
143
|
+
|
144
|
+
// If this is a UDS components
|
145
|
+
const tagName = jsxElement.getFirstChildByKind(SyntaxKind.Identifier)?.getText();
|
146
|
+
if (tagName && importedComponents.has(tagName)) {
|
147
|
+
// Attributes
|
148
|
+
const attributes =
|
149
|
+
Node.isJsxOpeningElement(jsxElement) || Node.isJsxSelfClosingElement(jsxElement)
|
150
|
+
? jsxElement.getAttributes()
|
151
|
+
: [];
|
152
|
+
const existingClassNameAttribute = attributes.find(
|
153
|
+
(attr) => Node.isJsxAttribute(attr) && attr.getNameNode().getText() === 'className',
|
154
|
+
);
|
155
|
+
let classNameIsComplex = false;
|
156
|
+
const existingClassNameValues: string[] = [];
|
157
|
+
const classNamesToAdd: string[] = [];
|
158
|
+
|
159
|
+
// Iterate over the attributes and handle the className attribute
|
160
|
+
attributes.forEach((attribute) => {
|
161
|
+
if (Node.isJsxAttribute(attribute)) {
|
162
|
+
const attributeName = attribute.getNameNode().getText() as
|
163
|
+
| keyof typeof propToClassMap
|
164
|
+
| 'className';
|
165
|
+
if (attributeName === 'className') {
|
166
|
+
const initializer = attribute.getInitializer();
|
167
|
+
if (initializer && Node.isStringLiteral(initializer)) {
|
168
|
+
// If the className is a string literal, add it to the existingClassNameValues
|
169
|
+
existingClassNameValues.push(initializer.getLiteralText());
|
170
|
+
} else if (initializer) {
|
171
|
+
// If the className is a complex expression, throw a warning
|
172
|
+
classNameIsComplex = true;
|
173
|
+
}
|
174
|
+
} else {
|
175
|
+
const initializer = attribute.getInitializer();
|
176
|
+
const attributeValue = initializer
|
177
|
+
?.getText()
|
178
|
+
.replace(
|
179
|
+
/^["']|["']$/g,
|
180
|
+
'',
|
181
|
+
) as keyof (typeof propToClassMap)[keyof typeof propToClassMap];
|
182
|
+
if (
|
183
|
+
propToClassMap[attributeName] &&
|
184
|
+
propToClassMap[attributeName][attributeValue]
|
185
|
+
) {
|
186
|
+
classNamesToAdd.push(propToClassMap[attributeName][attributeValue]);
|
187
|
+
attribute.remove();
|
188
|
+
}
|
189
|
+
}
|
190
|
+
}
|
191
|
+
});
|
192
|
+
|
193
|
+
// Combine the existing className values with the new classNamesToAdd
|
194
|
+
const allClassNames = [...existingClassNameValues, ...classNamesToAdd].join(' ').trim();
|
195
|
+
if (existingClassNameAttribute && allClassNames.length > 0) {
|
196
|
+
// Warn for complex expressions or variables
|
197
|
+
if (classNameIsComplex) {
|
198
|
+
throwComplexExpressionWarning({
|
199
|
+
kind: classNameIsComplex ? 'className' : 'other',
|
200
|
+
attr: existingClassNameAttribute,
|
201
|
+
element: jsxElement,
|
202
|
+
classNamesToAdd,
|
203
|
+
});
|
204
|
+
} else {
|
205
|
+
// @ts-expect-error - setInitializer is not in the types
|
206
|
+
existingClassNameAttribute.setInitializer(`"${allClassNames}"`);
|
207
|
+
}
|
208
|
+
} else if (classNamesToAdd.length > 0 && !existingClassNameAttribute) {
|
209
|
+
// @ts-expect-error - addAttribute is not in the types
|
210
|
+
jsxElement.addAttribute({
|
211
|
+
name: 'className',
|
212
|
+
initializer: `"${classNamesToAdd.join(' ')}"`,
|
213
|
+
});
|
214
|
+
}
|
215
|
+
}
|
216
|
+
}
|
217
|
+
});
|
218
|
+
});
|
219
|
+
|
220
|
+
await project.save();
|
221
|
+
};
|
@@ -0,0 +1,89 @@
|
|
1
|
+
/** This object represents the props we want to convert into classNames */
|
2
|
+
export const sizingPropToClassMap = {
|
3
|
+
height: {
|
4
|
+
auto: 'h-auto',
|
5
|
+
full: 'h-full',
|
6
|
+
screen: 'h-screen',
|
7
|
+
min: 'h-min',
|
8
|
+
max: 'h-max',
|
9
|
+
fit: 'h-fit',
|
10
|
+
'1/2': 'h-1/2',
|
11
|
+
'1/3': 'h-1/3',
|
12
|
+
'2/3': 'h-2/3',
|
13
|
+
'1/4': 'h-1/4',
|
14
|
+
'2/4': 'h-2/4',
|
15
|
+
'3/4': 'h-3/4',
|
16
|
+
'1/5': 'h-1/5',
|
17
|
+
'2/5': 'h-2/5',
|
18
|
+
'3/5': 'h-3/5',
|
19
|
+
'4/5': 'h-4/5',
|
20
|
+
'1/6': 'h-1/6',
|
21
|
+
'2/6': 'h-2/6',
|
22
|
+
'3/6': 'h-3/6',
|
23
|
+
'4/6': 'h-4/6',
|
24
|
+
'5/6': 'h-5/6',
|
25
|
+
},
|
26
|
+
minHeight: {
|
27
|
+
full: 'min-h-full',
|
28
|
+
min: 'min-h-min',
|
29
|
+
max: 'min-h-max',
|
30
|
+
fit: 'min-h-fit',
|
31
|
+
screen: 'min-h-screen',
|
32
|
+
},
|
33
|
+
maxHeight: {
|
34
|
+
full: 'max-h-full',
|
35
|
+
min: 'max-h-min',
|
36
|
+
max: 'max-h-max',
|
37
|
+
fit: 'max-h-fit',
|
38
|
+
screen: 'max-h-screen',
|
39
|
+
none: 'max-h-[0px]',
|
40
|
+
},
|
41
|
+
width: {
|
42
|
+
auto: 'w-auto',
|
43
|
+
full: 'w-full',
|
44
|
+
screen: 'w-screen',
|
45
|
+
min: 'w-min',
|
46
|
+
max: 'w-max',
|
47
|
+
fit: 'w-fit',
|
48
|
+
'1/2': 'w-1/2',
|
49
|
+
'1/3': 'w-1/3',
|
50
|
+
'2/3': 'w-2/3',
|
51
|
+
'1/4': 'w-1/4',
|
52
|
+
'2/4': 'w-2/4',
|
53
|
+
'3/4': 'w-3/4',
|
54
|
+
'1/5': 'w-1/5',
|
55
|
+
'2/5': 'w-2/5',
|
56
|
+
'3/5': 'w-3/5',
|
57
|
+
'4/5': 'w-4/5',
|
58
|
+
'1/6': 'w-1/6',
|
59
|
+
'2/6': 'w-2/6',
|
60
|
+
'3/6': 'w-3/6',
|
61
|
+
'4/6': 'w-4/6',
|
62
|
+
'5/6': 'w-5/6',
|
63
|
+
'1/12': 'w-1/12',
|
64
|
+
'2/12': 'w-2/12',
|
65
|
+
'3/12': 'w-3/12',
|
66
|
+
'4/12': 'w-4/12',
|
67
|
+
'5/12': 'w-5/12',
|
68
|
+
'6/12': 'w-6/12',
|
69
|
+
'7/12': 'w-7/12',
|
70
|
+
'8/12': 'w-8/12',
|
71
|
+
'9/12': 'w-9/12',
|
72
|
+
'10/12': 'w-10/12',
|
73
|
+
'11/12': 'w-11/12',
|
74
|
+
},
|
75
|
+
minWidth: {
|
76
|
+
full: 'min-w-full',
|
77
|
+
min: 'min-w-min',
|
78
|
+
max: 'min-w-max',
|
79
|
+
fit: 'min-w-fit',
|
80
|
+
screen: 'min-w-screen',
|
81
|
+
},
|
82
|
+
maxWidth: {
|
83
|
+
none: 'max-w-[0px]',
|
84
|
+
full: 'max-w-full',
|
85
|
+
min: 'max-w-min',
|
86
|
+
max: 'max-w-max',
|
87
|
+
fit: 'max-w-fit',
|
88
|
+
},
|
89
|
+
} as const;
|
@@ -0,0 +1,87 @@
|
|
1
|
+
import { type Props } from 'bluebun';
|
2
|
+
import prompts from 'prompts';
|
3
|
+
|
4
|
+
import { getCommandHelp, getSubCommandsChoices } from '../../utils/getCommandHelp';
|
5
|
+
import { getDirChoices } from '../../utils/getDirChoices';
|
6
|
+
|
7
|
+
export default {
|
8
|
+
name: 'codemod',
|
9
|
+
description: `Apply a codemod`,
|
10
|
+
run: async (props: Props) => {
|
11
|
+
const subCommands = await getSubCommandsChoices(props);
|
12
|
+
const subCommandIsValid = subCommands.some(({ value }) => props?.first === value);
|
13
|
+
const isRootCommand = Boolean(!props?.first);
|
14
|
+
const dirChoices = getDirChoices();
|
15
|
+
|
16
|
+
if (isRootCommand) {
|
17
|
+
// Prompt the user to setup the codemod runner
|
18
|
+
const { selectedDirs, selectedCodemods, didConfirm } = await prompts([
|
19
|
+
{
|
20
|
+
type: 'multiselect',
|
21
|
+
name: 'selectedDirs',
|
22
|
+
instructions: false,
|
23
|
+
message: 'Where are your sourcefiles?',
|
24
|
+
hint: '(Space to select. Return to submit)',
|
25
|
+
choices: dirChoices,
|
26
|
+
},
|
27
|
+
{
|
28
|
+
type: 'multiselect',
|
29
|
+
name: 'selectedCodemods',
|
30
|
+
instructions: false,
|
31
|
+
message: 'Select the codemods you want to run',
|
32
|
+
hint: '(Space to select. Return to submit)',
|
33
|
+
choices: subCommands,
|
34
|
+
},
|
35
|
+
{
|
36
|
+
type: 'toggle',
|
37
|
+
name: 'didConfirm',
|
38
|
+
message: 'Are you ready?',
|
39
|
+
initial: true,
|
40
|
+
active: 'yes',
|
41
|
+
inactive: 'no',
|
42
|
+
},
|
43
|
+
]);
|
44
|
+
|
45
|
+
// If we bail at anypoint, just exit
|
46
|
+
if (!selectedDirs?.length || !selectedCodemods?.length || !didConfirm) {
|
47
|
+
process.exit(1);
|
48
|
+
}
|
49
|
+
|
50
|
+
// Run each codemod and provide the selectedDirs
|
51
|
+
return Promise.all(
|
52
|
+
selectedCodemods.map(async (selectedCodemod: string[]) => {
|
53
|
+
return await import(`./${selectedCodemod}`).then((codemod) =>
|
54
|
+
codemod.default.run({ ...props, selectedDirs }),
|
55
|
+
);
|
56
|
+
}),
|
57
|
+
);
|
58
|
+
} else if (subCommandIsValid) {
|
59
|
+
// Prompt the user to select a directory
|
60
|
+
const { selectedDirs } = await prompts({
|
61
|
+
type: 'multiselect',
|
62
|
+
name: 'selectedDirs',
|
63
|
+
instructions: false,
|
64
|
+
message: 'Where are your sourcefiles?',
|
65
|
+
hint: '(Space to select. Return to submit)',
|
66
|
+
choices: dirChoices,
|
67
|
+
});
|
68
|
+
|
69
|
+
// If no directory is selected, exit
|
70
|
+
if (!selectedDirs?.length) {
|
71
|
+
process.exit(1);
|
72
|
+
}
|
73
|
+
|
74
|
+
// Run the codemod
|
75
|
+
return (await import(`./${props.first}`)).default.run({
|
76
|
+
...props,
|
77
|
+
selectedDirs,
|
78
|
+
});
|
79
|
+
} else {
|
80
|
+
// Throw the help message
|
81
|
+
await getCommandHelp({
|
82
|
+
...props,
|
83
|
+
notes: `That codemod does not exist. Try one of the codemods listed above!`,
|
84
|
+
});
|
85
|
+
}
|
86
|
+
},
|
87
|
+
};
|
@@ -0,0 +1,17 @@
|
|
1
|
+
import { type Props, spinStart, spinStop } from 'bluebun';
|
2
|
+
|
3
|
+
import { propsToClass } from '../../codemods/propsToClass';
|
4
|
+
import { sizingPropToClassMap } from '../../codemods/utils/sizingPropToClassMap';
|
5
|
+
|
6
|
+
export default {
|
7
|
+
name: 'sizingProps',
|
8
|
+
description: `Convert sizing props to classNames`,
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
10
|
+
run: async (props: Props & { selectedDirs?: string[] }) => {
|
11
|
+
spinStart('Running codemod...');
|
12
|
+
|
13
|
+
await propsToClass({ propToClassMap: sizingPropToClassMap, selectedDirs: props.selectedDirs });
|
14
|
+
|
15
|
+
spinStop('Codemod complete! Peek the diff and commit your changes!');
|
16
|
+
},
|
17
|
+
};
|
package/cli/preload.ts
CHANGED
@@ -4,7 +4,7 @@ const mockFastGlob = jest.fn().mockResolvedValue(['/pages/PageA.tsx', '/pages/Pa
|
|
4
4
|
|
5
5
|
mock.module('fast-glob', () => ({ __esModule: true, default: mockFastGlob }));
|
6
6
|
|
7
|
-
mock.module('@yahoo/uds/
|
7
|
+
mock.module('@yahoo/uds/tailwind/purger', () => ({
|
8
8
|
componentsDependencies: {
|
9
9
|
Button: ['Icon', 'Pressable', 'Text'],
|
10
10
|
Spinner: [],
|
@@ -10,6 +10,7 @@ import {
|
|
10
10
|
type Props,
|
11
11
|
white,
|
12
12
|
} from 'bluebun';
|
13
|
+
import { type Choice } from 'prompts';
|
13
14
|
|
14
15
|
/**
|
15
16
|
* The formatting from bluebun for the help command is not great.
|
@@ -21,8 +22,8 @@ import {
|
|
21
22
|
* - https://github.com/wobsoriano/blipgloss
|
22
23
|
* - https://github.com/wobsoriano/bun-promptx
|
23
24
|
*/
|
24
|
-
async function formatHelp(initialProps: Props) {
|
25
|
-
const { name, commandPath } = initialProps;
|
25
|
+
async function formatHelp(initialProps: Props & { notes?: string }) {
|
26
|
+
const { name, commandPath, notes } = initialProps;
|
26
27
|
|
27
28
|
const categoryToFilter = commandPath?.length ? commandPath[0] : undefined;
|
28
29
|
|
@@ -63,11 +64,49 @@ ${magenta(`Usage: ${white(`${name} <command>`)}`)}
|
|
63
64
|
|
64
65
|
${magenta('Commands:')}
|
65
66
|
${helpLines.join('\n')}
|
67
|
+
${notes ? `\n${magenta('Notes:')} ${notes}\n` : ''}
|
66
68
|
`;
|
67
69
|
|
68
70
|
return help;
|
69
71
|
}
|
70
72
|
|
71
|
-
export async function getCommandHelp(props: Props) {
|
73
|
+
export async function getCommandHelp(props: Props & { notes?: string }) {
|
72
74
|
print(await formatHelp(props));
|
73
75
|
}
|
76
|
+
|
77
|
+
// This function is used to get the choices for subcommands of a command
|
78
|
+
export async function getSubCommandsChoices(initialProps: Props) {
|
79
|
+
// Get the command tree for the initialProps
|
80
|
+
const categoryToFilter = initialProps.commandPath?.length
|
81
|
+
? initialProps.commandPath[0]
|
82
|
+
: undefined;
|
83
|
+
const _tree = await commandTree(initialProps);
|
84
|
+
const tree = categoryToFilter ? { [categoryToFilter]: _tree[categoryToFilter] } : _tree;
|
85
|
+
|
86
|
+
// This function is used to generate a list of choices from a command tree
|
87
|
+
function generateCommandList(cmdTree: CommandTree): Choice[] {
|
88
|
+
// For each key in the command tree, generate a choice
|
89
|
+
return Object.keys(cmdTree).flatMap((key) => {
|
90
|
+
const command = cmdTree[key];
|
91
|
+
const lines: Choice[] = [];
|
92
|
+
|
93
|
+
// Add a choice for the command
|
94
|
+
lines.push({
|
95
|
+
title: `${command.name} (${command.description})`,
|
96
|
+
value: command.name,
|
97
|
+
selected: true,
|
98
|
+
});
|
99
|
+
|
100
|
+
// If the command has subcommands, recursively generate choices for the subcommands and add them to the list
|
101
|
+
if (command.subcommands) {
|
102
|
+
lines.push(...generateCommandList(command.subcommands));
|
103
|
+
}
|
104
|
+
|
105
|
+
// Return the list of choices
|
106
|
+
return lines;
|
107
|
+
});
|
108
|
+
}
|
109
|
+
|
110
|
+
// Return a list of choices for all subcommands
|
111
|
+
return generateCommandList(tree).filter((command) => command.value !== categoryToFilter);
|
112
|
+
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import fs from 'node:fs';
|
2
|
+
|
3
|
+
import { Choice } from 'prompts';
|
4
|
+
|
5
|
+
const lessCommonDirs = [
|
6
|
+
'node_modules',
|
7
|
+
'.git',
|
8
|
+
'.github',
|
9
|
+
'.husky',
|
10
|
+
'.vscode',
|
11
|
+
'dist',
|
12
|
+
'build',
|
13
|
+
'public',
|
14
|
+
];
|
15
|
+
export const getDirChoices = (rootDir = '.'): Choice[] => {
|
16
|
+
return fs
|
17
|
+
.readdirSync(rootDir)
|
18
|
+
.filter(
|
19
|
+
(dir) => fs.statSync(dir).isDirectory() && dir !== 'node_modules' && !dir.startsWith('.'),
|
20
|
+
)
|
21
|
+
.map((dir) => ({ title: dir, value: dir, selected: !lessCommonDirs.includes(dir) }));
|
22
|
+
};
|
package/cli/utils/purgeCSS.ts
CHANGED
@@ -6,11 +6,11 @@ import {
|
|
6
6
|
componentToVariants,
|
7
7
|
variantsList,
|
8
8
|
variantToTailwindClass,
|
9
|
-
} from '@yahoo/uds/
|
9
|
+
} from '@yahoo/uds/tailwind/purger';
|
10
10
|
import {
|
11
11
|
findReferencesAsJsxElements,
|
12
12
|
getUsedPropsInReference,
|
13
|
-
} from '@yahoo/uds/
|
13
|
+
} from '@yahoo/uds/tailwind/purger/utils';
|
14
14
|
import {
|
15
15
|
ColorModeConfig,
|
16
16
|
DARK_COLOR_MODE_CLASSNAME,
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import * as react from 'react';
|
2
|
-
import {
|
2
|
+
import { b as UniversalPressableProps, c as UniversalIconButtonProps, d as UniversalImageProps } from './types-DRP5tWZG.cjs';
|
3
3
|
import { View, PressableProps as PressableProps$1, StyleProp, ViewStyle } from 'react-native';
|
4
4
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
5
5
|
import { ImageProps as ImageProps$1 } from 'expo-image';
|
@@ -33,6 +33,6 @@ interface ImageProps extends Omit<ImageProps$1, 'alt' | 'source'>, UniversalImag
|
|
33
33
|
/**
|
34
34
|
* An image element
|
35
35
|
*/
|
36
|
-
declare function Image({ width: imageWidth, height: imageHeight, src, alt, contentFit, backgroundColor, borderRadius, borderTopStartRadius, borderTopEndRadius, borderBottomStartRadius, borderBottomEndRadius, borderColor, borderColorOnActive, borderColorOnFocus, borderColorOnChecked, borderColorOnHover, borderStartColor, borderEndColor, borderTopColor, borderBottomColor, borderWidth, borderVerticalWidth, borderHorizontalWidth, borderStartWidth, borderEndWidth, borderTopWidth, borderBottomWidth, alignContent, alignItems, alignSelf, flex, flexDirection, flexGrow, flexShrink, flexWrap, justifyContent, flexBasis, display, overflow, overflowX, overflowY, position, spacing, spacingHorizontal, spacingVertical, spacingBottom, spacingEnd, spacingStart, spacingTop, offset, offsetVertical, offsetHorizontal, offsetBottom, offsetEnd, offsetStart, offsetTop, columnGap, rowGap,
|
36
|
+
declare function Image({ width: imageWidth, height: imageHeight, src, alt, contentFit, backgroundColor, borderRadius, borderTopStartRadius, borderTopEndRadius, borderBottomStartRadius, borderBottomEndRadius, borderColor, borderColorOnActive, borderColorOnFocus, borderColorOnChecked, borderColorOnHover, borderStartColor, borderEndColor, borderTopColor, borderBottomColor, borderWidth, borderVerticalWidth, borderHorizontalWidth, borderStartWidth, borderEndWidth, borderTopWidth, borderBottomWidth, alignContent, alignItems, alignSelf, flex, flexDirection, flexGrow, flexShrink, flexWrap, justifyContent, flexBasis, display, overflow, overflowX, overflowY, position, spacing, spacingHorizontal, spacingVertical, spacingBottom, spacingEnd, spacingStart, spacingTop, offset, offsetVertical, offsetHorizontal, offsetBottom, offsetEnd, offsetStart, offsetTop, columnGap, rowGap, ...props }: ImageProps): react_jsx_runtime.JSX.Element;
|
37
37
|
|
38
38
|
export { type ImageProps as I, type PressableProps as P, IconButton as a, type IconButtonProps as b, Image as c, Pressable as d };
|