@yahoo/uds 0.4.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. package/cli/README.md +10 -0
  2. package/cli/codemods/propsToClass.test.tsx +97 -0
  3. package/cli/codemods/propsToClass.ts +221 -0
  4. package/cli/codemods/utils/sizingPropToClassMap.ts +89 -0
  5. package/cli/commands/codemod/codemod.ts +87 -0
  6. package/cli/commands/codemod/sizingProps.ts +17 -0
  7. package/cli/utils/getCommandHelp.ts +42 -3
  8. package/cli/utils/getDirChoices.ts +22 -0
  9. package/dist/{Image.native-DUAFJodS.d.ts → Image.native-BJfkUDnq.d.ts} +2 -2
  10. package/dist/{Image.native-B3I4JoH3.d.cts → Image.native-CddvOupj.d.cts} +2 -2
  11. package/dist/{VStack-BHlRUsOR.d.cts → VStack-CMGd5AX1.d.cts} +1 -1
  12. package/dist/{VStack-DMb_RGRS.d.ts → VStack-CjOrFgxd.d.ts} +1 -1
  13. package/dist/experimental/index.cjs +1 -1
  14. package/dist/experimental/index.d.cts +2 -2
  15. package/dist/experimental/index.d.ts +2 -2
  16. package/dist/experimental/index.js +1 -1
  17. package/dist/experimental/index.native.cjs +1 -1
  18. package/dist/experimental/index.native.d.cts +3 -3
  19. package/dist/experimental/index.native.d.ts +3 -3
  20. package/dist/experimental/index.native.js +1 -1
  21. package/dist/index.cjs +1 -1
  22. package/dist/index.d.cts +6 -12
  23. package/dist/index.d.ts +6 -12
  24. package/dist/index.js +1 -1
  25. package/dist/{index.native-Bm-r2Dpa.d.cts → index.native-DG8RsS88.d.cts} +1 -1
  26. package/dist/{index.native-BTfOSmUx.d.ts → index.native-LymI5azd.d.ts} +1 -1
  27. package/dist/index.native.cjs +1 -1
  28. package/dist/index.native.d.cts +7 -7
  29. package/dist/index.native.d.ts +7 -7
  30. package/dist/index.native.js +1 -1
  31. package/dist/tailwindPlugin.d.cts +1 -1
  32. package/dist/tailwindPlugin.d.ts +1 -1
  33. package/dist/tailwindPurge.cjs +1 -1
  34. package/dist/tailwindPurge.js +1 -1
  35. package/dist/tokens/index.d.cts +2 -2
  36. package/dist/tokens/index.d.ts +2 -2
  37. package/dist/tokens/index.native.d.cts +2 -2
  38. package/dist/tokens/index.native.d.ts +2 -2
  39. package/dist/tokens/parseTokens.d.cts +1 -1
  40. package/dist/tokens/parseTokens.d.ts +1 -1
  41. package/dist/tokens/parseTokens.native.d.cts +1 -1
  42. package/dist/tokens/parseTokens.native.d.ts +1 -1
  43. package/dist/{types-COiuE8XK.d.cts → types-D-ttCAsE.d.cts} +3 -23
  44. package/dist/{types-COiuE8XK.d.ts → types-D-ttCAsE.d.ts} +3 -23
  45. package/package.json +3 -1
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
+ };
@@ -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
+ };
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { i as UniversalPressableProps, j as UniversalIconButtonProps, k as UniversalImageProps } from './types-COiuE8XK.js';
2
+ import { i as UniversalPressableProps, j as UniversalIconButtonProps, k as UniversalImageProps } from './types-D-ttCAsE.js';
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, minHeight, maxHeight, minWidth, maxWidth, ...props }: ImageProps): react_jsx_runtime.JSX.Element;
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 };
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { i as UniversalPressableProps, j as UniversalIconButtonProps, k as UniversalImageProps } from './types-COiuE8XK.cjs';
2
+ import { i as UniversalPressableProps, j as UniversalIconButtonProps, k as UniversalImageProps } from './types-D-ttCAsE.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, minHeight, maxHeight, minWidth, maxWidth, ...props }: ImageProps): react_jsx_runtime.JSX.Element;
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 };
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { Ref } from 'react';
3
- import { aS as UniversalBoxProps, i as UniversalPressableProps, aU as UniversalTextProps, l as UniversalStackProps } from './types-COiuE8XK.cjs';
3
+ import { aM as UniversalBoxProps, i as UniversalPressableProps, aO as UniversalTextProps, l as UniversalStackProps } from './types-D-ttCAsE.cjs';
4
4
 
5
5
  type DivProps = React.HTMLAttributes<HTMLDivElement>;
6
6
  interface BoxProps extends UniversalBoxProps, DivProps {
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { Ref } from 'react';
3
- import { aS as UniversalBoxProps, i as UniversalPressableProps, aU as UniversalTextProps, l as UniversalStackProps } from './types-COiuE8XK.js';
3
+ import { aM as UniversalBoxProps, i as UniversalPressableProps, aO as UniversalTextProps, l as UniversalStackProps } from './types-D-ttCAsE.js';
4
4
 
5
5
  type DivProps = React.HTMLAttributes<HTMLDivElement>;
6
6
  interface BoxProps extends UniversalBoxProps, DivProps {