ui-thing 0.1.56 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/.husky/pre-commit +1 -0
  2. package/CHANGELOG.md +76 -0
  3. package/README.md +4 -3
  4. package/dist/index.js +1175 -15776
  5. package/dist/index.js.map +1 -1
  6. package/package.json +34 -22
  7. package/src/commands/add.ts +218 -274
  8. package/src/commands/init.ts +107 -58
  9. package/src/commands/prettier.ts +6 -8
  10. package/src/commands/shortcuts.ts +13 -13
  11. package/src/commands/theme.ts +9 -6
  12. package/src/index.ts +2 -2
  13. package/src/templates/css.ts +958 -773
  14. package/src/templates/prettier.ts +14 -16
  15. package/src/templates/shortcuts.ts +225 -126
  16. package/src/templates/tw-helper.ts +8 -0
  17. package/src/templates/vs-code.ts +24 -0
  18. package/src/types.ts +74 -3
  19. package/src/utils/addPrettierConfig.ts +49 -6
  20. package/src/utils/addShortcutFiles.ts +5 -4
  21. package/src/utils/addTailwindVitePlugin.ts +35 -0
  22. package/src/utils/addVSCodeFiles.ts +13 -0
  23. package/src/utils/compareUIConfig.ts +1 -2
  24. package/src/utils/config.ts +59 -86
  25. package/src/utils/constants.ts +67 -13
  26. package/src/utils/detectNuxtVersion.ts +20 -0
  27. package/src/utils/fetchComponents.ts +14 -1
  28. package/src/utils/installPackages.ts +3 -27
  29. package/src/utils/mergeJsonFile.ts +28 -0
  30. package/src/utils/printFancyBoxMessage.ts +62 -16
  31. package/src/utils/promptForComponents.ts +8 -6
  32. package/src/utils/uiConfigPrompt.ts +12 -37
  33. package/tsconfig.json +2 -1
  34. package/dist/chunk-FW4363Y4.js +0 -2
  35. package/dist/chunk-FW4363Y4.js.map +0 -1
  36. package/dist/prompt-4NXDAQWE.js +0 -46
  37. package/dist/prompt-4NXDAQWE.js.map +0 -1
  38. package/src/comps.ts +0 -3237
  39. package/src/templates/tailwind.ts +0 -142
@@ -1,16 +1,14 @@
1
- export const PRETTIER_CONFIG = `{
2
- "arrowParens": "always",
3
- "endOfLine": "lf",
4
- "plugins": ["@ianvs/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"],
5
- "printWidth": 100,
6
- "semi": true,
7
- "singleQuote": false,
8
- "tabWidth": 2,
9
- "trailingComma": "es5",
10
- "useTabs": false,
11
- "vueIndentScriptAndStyle": true,
12
- "tailwindFunctions": ["tv"],
13
- "importOrder": ["<BUILTIN_MODULES>", "<THIRD_PARTY_MODULES>", "<TYPES>", "", "^[.]"]
14
- }
15
-
16
- `;
1
+ export const PRETTIER_CONFIG = {
2
+ arrowParens: "always",
3
+ endOfLine: "lf",
4
+ plugins: ["@ianvs/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"],
5
+ printWidth: 100,
6
+ semi: true,
7
+ singleQuote: false,
8
+ tabWidth: 2,
9
+ trailingComma: "es5",
10
+ useTabs: false,
11
+ vueIndentScriptAndStyle: true,
12
+ tailwindFunctions: ["tv"],
13
+ importOrder: ["<BUILTIN_MODULES>", "<THIRD_PARTY_MODULES>", "<TYPES>", "", "^[.]"],
14
+ };
@@ -1,15 +1,95 @@
1
- export const DEFINE_SHORTCUT = `/* eslint-disable @typescript-eslint/no-unsafe-function-type */
2
- import { logicAnd, logicNot } from "@vueuse/math";
3
- import type { ComputedRef, WatchSource } from "vue";
1
+ export const DEFINE_SHORTCUT = `import {
2
+ createSharedComposable,
3
+ useActiveElement,
4
+ useDebounceFn,
5
+ useEventListener,
6
+ } from "@vueuse/core";
7
+ import type { MaybeRef } from "vue";
8
+
9
+ type KbdKeysSpecificMap = {
10
+ meta: string;
11
+ alt: string;
12
+ ctrl: string;
13
+ };
14
+
15
+ export const kbdKeysMap = {
16
+ meta: "",
17
+ ctrl: "",
18
+ alt: "",
19
+ win: "⊞",
20
+ command: "⌘",
21
+ shift: "⇧",
22
+ control: "⌃",
23
+ option: "⌥",
24
+ enter: "↵",
25
+ delete: "⌦",
26
+ backspace: "⌫",
27
+ escape: "⎋",
28
+ tab: "⇥",
29
+ capslock: "⇪",
30
+ arrowup: "↑",
31
+ arrowright: "→",
32
+ arrowdown: "↓",
33
+ arrowleft: "←",
34
+ pageup: "⇞",
35
+ pagedown: "⇟",
36
+ home: "↖",
37
+ end: "↘",
38
+ };
39
+
40
+ export type KbdKey = keyof typeof kbdKeysMap;
41
+ export type KbdKeySpecific = keyof KbdKeysSpecificMap;
42
+
43
+ const _useKbd = () => {
44
+ const macOS = computed(
45
+ () =>
46
+ import.meta.client &&
47
+ navigator &&
48
+ navigator.userAgent &&
49
+ navigator.userAgent.match(/Macintosh;/)
50
+ );
51
+
52
+ const kbdKeysSpecificMap = reactive({
53
+ meta: " ",
54
+ alt: " ",
55
+ ctrl: " ",
56
+ });
57
+
58
+ onMounted(() => {
59
+ kbdKeysSpecificMap.meta = macOS.value ? kbdKeysMap.command : "Ctrl";
60
+ kbdKeysSpecificMap.ctrl = macOS.value ? kbdKeysMap.control : "Ctrl";
61
+ kbdKeysSpecificMap.alt = macOS.value ? kbdKeysMap.option : "Alt";
62
+ });
63
+
64
+ function getKbdKey(value?: KbdKey | string) {
65
+ if (!value) {
66
+ return;
67
+ }
68
+
69
+ if (["meta", "alt", "ctrl"].includes(value)) {
70
+ return kbdKeysSpecificMap[value as KbdKeySpecific];
71
+ }
72
+
73
+ return kbdKeysMap[value as KbdKey] || value.toUpperCase();
74
+ }
75
+
76
+ return {
77
+ macOS,
78
+ getKbdKey,
79
+ };
80
+ };
81
+
82
+ export const useKbd = /* @__PURE__ */ createSharedComposable(_useKbd);
83
+
84
+ type Handler = (e?: any) => void;
4
85
 
5
86
  export interface ShortcutConfig {
6
- handler: Function;
87
+ handler: Handler;
7
88
  usingInput?: string | boolean;
8
- whenever?: WatchSource<boolean>[];
9
89
  }
10
90
 
11
91
  export interface ShortcutsConfig {
12
- [key: string]: ShortcutConfig | Function;
92
+ [key: string]: ShortcutConfig | Handler | false | null | undefined;
13
93
  }
14
94
 
15
95
  export interface ShortcutsOptions {
@@ -17,8 +97,8 @@ export interface ShortcutsOptions {
17
97
  }
18
98
 
19
99
  interface Shortcut {
20
- handler: Function;
21
- condition: ComputedRef<boolean>;
100
+ handler: Handler;
101
+ enabled: boolean;
22
102
  chained: boolean;
23
103
  // KeyboardEvent attributes
24
104
  key: string;
@@ -30,24 +110,61 @@ interface Shortcut {
30
110
  // keyCode?: number
31
111
  }
32
112
 
33
- export const defineShortcuts = (config: ShortcutsConfig, options: ShortcutsOptions = {}) => {
34
- const { macOS, usingInput } = useShortcuts();
113
+ const chainedShortcutRegex = /^[^-]+.*-.*[^-]+$/;
114
+ const combinedShortcutRegex = /^[^_]+.*_.*[^_]+$/;
115
+ // keyboard keys which can be combined with Shift modifier (in addition to alphabet keys)
116
+ const shiftableKeys = [
117
+ "arrowleft",
118
+ "arrowright",
119
+ "arrowup",
120
+ "arrowright",
121
+ "tab",
122
+ "escape",
123
+ "enter",
124
+ "backspace",
125
+ ];
126
+
127
+ export function extractShortcuts(items: any[] | any[][]) {
128
+ const shortcuts: Record<string, Handler> = {};
129
+
130
+ function traverse(items: any[]) {
131
+ items.forEach((item) => {
132
+ if (item.kbds?.length && (item.onSelect || item.onClick)) {
133
+ const shortcutKey = item.kbds.join("_");
134
+ shortcuts[shortcutKey] = item.onSelect || item.onClick;
135
+ }
136
+ if (item.children) {
137
+ traverse(item.children.flat());
138
+ }
139
+ if (item.items) {
140
+ traverse(item.items.flat());
141
+ }
142
+ });
143
+ }
144
+
145
+ traverse(items.flat());
35
146
 
36
- let shortcuts: Shortcut[] = [];
147
+ return shortcuts;
148
+ }
37
149
 
38
- const chainedInputs = ref<any[]>([]);
150
+ export function defineShortcuts(config: MaybeRef<ShortcutsConfig>, options: ShortcutsOptions = {}) {
151
+ const chainedInputs = ref<string[]>([]);
39
152
  const clearChainedInput = () => {
40
153
  chainedInputs.value.splice(0, chainedInputs.value.length);
41
154
  };
42
155
  const debouncedClearChainedInput = useDebounceFn(clearChainedInput, options.chainDelay ?? 800);
43
156
 
157
+ const { macOS } = useKbd();
158
+ const activeElement = useActiveElement();
159
+
44
160
  const onKeyDown = (e: KeyboardEvent) => {
45
161
  // Input autocomplete triggers a keydown event
46
162
  if (!e.key) {
47
163
  return;
48
164
  }
49
165
 
50
- const alphabeticalKey = /^[a-z]{1}$/i.test(e.key);
166
+ const alphabetKey = /^[a-z]{1}$/i.test(e.key);
167
+ const shiftableKey = shiftableKeys.includes(e.key.toLowerCase());
51
168
 
52
169
  let chainedKey;
53
170
  chainedInputs.value.push(e.key);
@@ -55,14 +172,14 @@ export const defineShortcuts = (config: ShortcutsConfig, options: ShortcutsOptio
55
172
  if (chainedInputs.value.length >= 2) {
56
173
  chainedKey = chainedInputs.value.slice(-2).join("-");
57
174
 
58
- for (const shortcut of shortcuts.filter((s) => s.chained)) {
175
+ for (const shortcut of shortcuts.value.filter((s) => s.chained)) {
59
176
  if (shortcut.key !== chainedKey) {
60
177
  continue;
61
178
  }
62
179
 
63
- if (shortcut.condition.value) {
180
+ if (shortcut.enabled) {
64
181
  e.preventDefault();
65
- shortcut.handler();
182
+ shortcut.handler(e);
66
183
  }
67
184
  clearChainedInput();
68
185
  return;
@@ -70,7 +187,7 @@ export const defineShortcuts = (config: ShortcutsConfig, options: ShortcutsOptio
70
187
  }
71
188
 
72
189
  // try matching a standard shortcut
73
- for (const shortcut of shortcuts.filter((s) => !s.chained)) {
190
+ for (const shortcut of shortcuts.value.filter((s) => !s.chained)) {
74
191
  if (e.key.toLowerCase() !== shortcut.key) {
75
192
  continue;
76
193
  }
@@ -80,17 +197,17 @@ export const defineShortcuts = (config: ShortcutsConfig, options: ShortcutsOptio
80
197
  if (e.ctrlKey !== shortcut.ctrlKey) {
81
198
  continue;
82
199
  }
83
- // shift modifier is only checked in combination with alphabetical keys
84
- // (shift with non-alphabetical keys would change the key)
85
- if (alphabeticalKey && e.shiftKey !== shortcut.shiftKey) {
200
+ // shift modifier is only checked in combination with alphabet keys and some extra keys
201
+ // (shift with special characters would change the key)
202
+ if ((alphabetKey || shiftableKey) && e.shiftKey !== shortcut.shiftKey) {
86
203
  continue;
87
204
  }
88
205
  // alt modifier changes the combined key anyways
89
206
  // if (e.altKey !== shortcut.altKey) { continue }
90
207
 
91
- if (shortcut.condition.value) {
208
+ if (shortcut.enabled) {
92
209
  e.preventDefault();
93
- shortcut.handler();
210
+ shortcut.handler(e);
94
211
  }
95
212
  clearChainedInput();
96
213
  return;
@@ -99,124 +216,106 @@ export const defineShortcuts = (config: ShortcutsConfig, options: ShortcutsOptio
99
216
  debouncedClearChainedInput();
100
217
  };
101
218
 
102
- // Map config to full detailled shortcuts
103
- shortcuts = Object.entries(config)
104
- .map(([key, shortcutConfig]) => {
105
- if (!shortcutConfig) {
106
- return null;
107
- }
108
-
109
- // Parse key and modifiers
110
- let shortcut: Partial<Shortcut>;
111
-
112
- if (key.includes("-") && key.includes("_")) {
113
- console.trace("[Shortcut] Invalid key");
114
- return null;
115
- }
116
-
117
- const chained = key.includes("-");
118
- if (chained) {
119
- shortcut = {
120
- key: key.toLowerCase(),
121
- metaKey: false,
122
- ctrlKey: false,
123
- shiftKey: false,
124
- altKey: false,
125
- };
126
- } else {
127
- const keySplit = key
128
- .toLowerCase()
129
- .split("_")
130
- .map((k) => k);
131
- shortcut = {
132
- key: keySplit.filter((k) => !["meta", "ctrl", "shift", "alt"].includes(k)).join("_"),
133
- metaKey: keySplit.includes("meta"),
134
- ctrlKey: keySplit.includes("ctrl"),
135
- shiftKey: keySplit.includes("shift"),
136
- altKey: keySplit.includes("alt"),
137
- };
138
- }
139
- shortcut.chained = chained;
219
+ const usingInput = computed(() => {
220
+ const tagName = activeElement.value?.tagName;
221
+ const contentEditable = activeElement.value?.contentEditable;
140
222
 
141
- // Convert Meta to Ctrl for non-MacOS
142
- if (!macOS.value && shortcut.metaKey && !shortcut.ctrlKey) {
143
- shortcut.metaKey = false;
144
- shortcut.ctrlKey = true;
145
- }
223
+ const usingInput = !!(
224
+ tagName === "INPUT" ||
225
+ tagName === "TEXTAREA" ||
226
+ contentEditable === "true" ||
227
+ contentEditable === "plaintext-only"
228
+ );
146
229
 
147
- // Retrieve handler function
148
- if (typeof shortcutConfig === "function") {
149
- shortcut.handler = shortcutConfig;
150
- } else if (typeof shortcutConfig === "object") {
151
- shortcut = { ...shortcut, handler: shortcutConfig.handler };
152
- }
230
+ if (usingInput) {
231
+ return ((activeElement.value as any)?.name as string) || true;
232
+ }
153
233
 
154
- if (!shortcut.handler) {
155
- console.trace("[Shortcut] Invalid value");
156
- return null;
157
- }
234
+ return false;
235
+ });
158
236
 
159
- // Create shortcut computed
160
- const conditions: ComputedRef<boolean>[] = [];
161
- if (!(shortcutConfig as ShortcutConfig).usingInput) {
162
- conditions.push(logicNot(usingInput));
163
- } else if (typeof (shortcutConfig as ShortcutConfig).usingInput === "string") {
164
- conditions.push(
165
- computed(() => usingInput.value === (shortcutConfig as ShortcutConfig).usingInput)
166
- );
167
- }
168
- shortcut.condition = logicAnd(
169
- ...conditions,
170
- ...((shortcutConfig as ShortcutConfig).whenever || [])
171
- );
237
+ // Map config to full detailed shortcuts
238
+ const shortcuts = computed<Shortcut[]>(() => {
239
+ return Object.entries(toValue(config))
240
+ .map(([key, shortcutConfig]) => {
241
+ if (!shortcutConfig) {
242
+ return null;
243
+ }
172
244
 
173
- return shortcut as Shortcut;
174
- })
175
- .filter(Boolean) as Shortcut[];
245
+ // Parse key and modifiers
246
+ let shortcut: Partial<Shortcut>;
176
247
 
177
- useEventListener("keydown", onKeyDown);
178
- };
248
+ if (
249
+ key.includes("-") &&
250
+ key !== "-" &&
251
+ !key.includes("_") &&
252
+ !key.match(chainedShortcutRegex)?.length
253
+ ) {
254
+ console.trace(\`[Shortcut] Invalid key: "\${key}"\`);
255
+ }
179
256
 
180
- `;
257
+ if (key.includes("_") && key !== "_" && !key.match(combinedShortcutRegex)?.length) {
258
+ console.trace(\`[Shortcut] Invalid key: "\${key}"\`);
259
+ }
181
260
 
182
- export const USE_SHORTCUTS = `export const _useShortcuts = () => {
183
- const macOS = computed(
184
- () =>
185
- import.meta.client &&
186
- navigator &&
187
- navigator.userAgent &&
188
- navigator.userAgent.match(/Macintosh;/)
189
- );
261
+ const chained = key.includes("-") && key !== "-" && !key.includes("_");
262
+ if (chained) {
263
+ shortcut = {
264
+ key: key.toLowerCase(),
265
+ metaKey: false,
266
+ ctrlKey: false,
267
+ shiftKey: false,
268
+ altKey: false,
269
+ };
270
+ } else {
271
+ const keySplit = key
272
+ .toLowerCase()
273
+ .split("_")
274
+ .map((k) => k);
275
+ shortcut = {
276
+ key: keySplit
277
+ .filter((k) => !["meta", "command", "ctrl", "shift", "alt", "option"].includes(k))
278
+ .join("_"),
279
+ metaKey: keySplit.includes("meta") || keySplit.includes("command"),
280
+ ctrlKey: keySplit.includes("ctrl"),
281
+ shiftKey: keySplit.includes("shift"),
282
+ altKey: keySplit.includes("alt") || keySplit.includes("option"),
283
+ };
284
+ }
285
+ shortcut.chained = chained;
190
286
 
191
- const metaSymbol = ref(" ");
287
+ // Convert Meta to Ctrl for non-MacOS
288
+ if (!macOS.value && shortcut.metaKey && !shortcut.ctrlKey) {
289
+ shortcut.metaKey = false;
290
+ shortcut.ctrlKey = true;
291
+ }
192
292
 
193
- const activeElement = useActiveElement();
194
- const usingInput = computed(() => {
195
- const usingInput = !!(
196
- activeElement.value?.tagName === "INPUT" ||
197
- activeElement.value?.tagName === "TEXTAREA" ||
198
- activeElement.value?.contentEditable === "true"
199
- );
293
+ // Retrieve handler function
294
+ if (typeof shortcutConfig === "function") {
295
+ shortcut.handler = shortcutConfig;
296
+ } else if (typeof shortcutConfig === "object") {
297
+ shortcut = { ...shortcut, handler: shortcutConfig.handler };
298
+ }
200
299
 
201
- if (usingInput) {
202
- return ((activeElement.value as any)?.name as string) || true;
203
- }
300
+ if (!shortcut.handler) {
301
+ console.trace("[Shortcut] Invalid value");
302
+ return null;
303
+ }
204
304
 
205
- return false;
206
- });
305
+ let enabled = true;
306
+ if (!(shortcutConfig as ShortcutConfig).usingInput) {
307
+ enabled = !usingInput.value;
308
+ } else if (typeof (shortcutConfig as ShortcutConfig).usingInput === "string") {
309
+ enabled = usingInput.value === (shortcutConfig as ShortcutConfig).usingInput;
310
+ }
311
+ shortcut.enabled = enabled;
207
312
 
208
- onMounted(() => {
209
- metaSymbol.value = macOS.value ? "⌘" : "Ctrl";
313
+ return shortcut;
314
+ })
315
+ .filter(Boolean) as Shortcut[];
210
316
  });
211
317
 
212
- return {
213
- macOS,
214
- metaSymbol,
215
- activeElement,
216
- usingInput,
217
- };
218
- };
219
-
220
- export const useShortcuts = createSharedComposable(_useShortcuts);
318
+ return useEventListener("keydown", onKeyDown);
319
+ }
221
320
 
222
321
  `;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Helper function used to get auto complete suggestions for Tailwind CSS classes.
3
+ */
4
+ export const TW_HELPER = `/**
5
+ * Utility function to return Tailwind CSS classes.
6
+ */
7
+ export const tw = <T extends TemplateStringsArray | string>(tailwindClasses: T) => tailwindClasses;
8
+ `;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * VS Code extensions recommendations for Tailwind CSS development.
3
+ */
4
+ export const VS_CODE_RECOMMENDATIONS = {
5
+ recommendations: [
6
+ "vue.volar",
7
+ "bradlc.vscode-tailwindcss",
8
+ "esbenp.prettier-vscode",
9
+ "antfu.iconify",
10
+ "formulahendry.auto-close-tag",
11
+ "formulahendry.auto-rename-tag",
12
+ ],
13
+ };
14
+
15
+ /**
16
+ * VS Code settings for Tailwind CSS development.
17
+ */
18
+ export const VS_CODE_SETTINGS = {
19
+ "editor.formatOnSave": true,
20
+ "editor.quickSuggestions": { strings: "on" },
21
+ "files.associations": { "*.css": "tailwindcss" },
22
+ "tailwindCSS.classFunctions": ["tw", "clsx", "tw\\.[a-z-]+"],
23
+ "tailwindCSS.experimental.classRegex": [["([\"'`][^\"'`]*.*?[\"'`])", "[\"'`]([^\"'`]*)[\"'`]"]],
24
+ };
package/src/types.ts CHANGED
@@ -1,18 +1,89 @@
1
+ /**
2
+ * Represents the UI Thing configuration options.
3
+ */
1
4
  export type UIConfig = {
5
+ /**
6
+ * The Nuxt version being used
7
+ *
8
+ * @default 4
9
+ *
10
+ * @deprecated This is now auto detected based on package.json.
11
+ */
2
12
  nuxtVersion?: number;
13
+ /**
14
+ * The theme to use
15
+ *
16
+ * @default "zinc"
17
+ */
3
18
  theme: string;
19
+ /**
20
+ * Path to the Tailwind CSS file
21
+ */
4
22
  tailwindCSSLocation: string;
5
- tailwindConfigLocation: string;
23
+ /**
24
+ * Path to the components directory
25
+ */
6
26
  componentsLocation: string;
27
+ /**
28
+ * Path to the composables directory
29
+ */
7
30
  composablesLocation: string;
31
+ /**
32
+ * Path to the plugins directory
33
+ */
8
34
  pluginsLocation?: string;
35
+ /**
36
+ * Path to the utils directory
37
+ */
9
38
  utilsLocation: string;
39
+ /**
40
+ * Whether to overwrite existing files
41
+ */
10
42
  force: boolean;
43
+ /**
44
+ * Whether to use the default filename when adding components.
45
+ *
46
+ * @default true
47
+ */
11
48
  useDefaultFilename: boolean;
49
+ /**
50
+ * The default package manager to use for installing dependencies.
51
+ */
12
52
  packageManager: string;
13
53
  };
14
54
 
15
- export type InitOptions = { force?: boolean; yes?: boolean; nuxtVersion?: number };
55
+ /**
56
+ * CLI options passed to the `init` command.
57
+ */
58
+ export type InitOptions = {
59
+ /**
60
+ * Whether to overwrite existing configuration files.
61
+ *
62
+ * @default false
63
+ */
64
+ force?: boolean;
65
+ /**
66
+ * Whether to skip prompts and use default values.
67
+ *
68
+ * @default false
69
+ */
70
+ yes?: boolean;
71
+ /**
72
+ * The Nuxt version to use.
73
+ *
74
+ * @default 4
75
+ */
76
+ nuxtVersion?: number;
77
+ };
78
+
79
+ export type AddCommand = {
80
+ /**
81
+ * Whether to add all components.
82
+ *
83
+ * @default false
84
+ */
85
+ all?: boolean;
86
+ };
16
87
 
17
88
  export type Component = {
18
89
  name: string;
@@ -30,7 +101,7 @@ export type Component = {
30
101
  overrides?: Record<string, any>;
31
102
  };
32
103
 
33
- export type Composable = {
104
+ type Composable = {
34
105
  fileName: string;
35
106
  dirPath: string;
36
107
  fileContent: string;
@@ -6,21 +6,64 @@ import prompts from "prompts";
6
6
 
7
7
  import { PRETTIER_CONFIG } from "../templates/prettier";
8
8
 
9
+ /**
10
+ * Adds or merges a Prettier configuration into the project.
11
+ *
12
+ * - If `.prettierrc` doesn't exist, creates it with `PRETTIER_CONFIG`.
13
+ * - If it exists, merges keys instead of overwriting, unless user chooses overwrite.
14
+ *
15
+ * @param cwd - The working directory of the project.
16
+ * @param format - Whether to run `prettier --write .` after adding/merging.
17
+ */
9
18
  export const addPrettierConfig = async (cwd = process.cwd(), format: boolean = true) => {
10
19
  const prettierLocation = join(cwd, ".prettierrc");
20
+ let finalConfig = PRETTIER_CONFIG;
21
+
11
22
  if (fse.existsSync(prettierLocation)) {
23
+ // Read existing config
24
+ const existingRaw = await fse.readFile(prettierLocation, "utf-8");
25
+ let existingConfig: Record<string, any> = {};
26
+
27
+ try {
28
+ existingConfig = JSON.parse(existingRaw);
29
+ } catch {
30
+ console.warn("⚠️ Existing .prettierrc is not valid JSON — will prompt for overwrite.");
31
+ }
32
+
12
33
  const res = await prompts({
13
- name: "overwrite",
14
- type: "confirm",
15
- message: "A prettier config file already exists. Overwrite?",
16
- initial: true,
34
+ name: "merge",
35
+ type: "select",
36
+ message: "A prettier config file already exists. What would you like to do?",
37
+ choices: [
38
+ { title: "Merge configs", value: "merge" },
39
+ { title: "Overwrite with new config", value: "overwrite" },
40
+ { title: "Cancel", value: "cancel" },
41
+ ],
42
+ initial: 0,
17
43
  });
18
- if (!res.overwrite) return false;
44
+
45
+ if (res.merge === "merge") {
46
+ // Merge existing config with PRETTIER_CONFIG
47
+ finalConfig = {
48
+ ...existingConfig,
49
+ ...PRETTIER_CONFIG,
50
+ };
51
+ } else if (res.merge === "overwrite") {
52
+ finalConfig = PRETTIER_CONFIG;
53
+ } else {
54
+ return false; // Cancelled
55
+ }
19
56
  }
20
- await fse.writeFile(prettierLocation, PRETTIER_CONFIG, "utf-8");
57
+
58
+ // Save merged or new config
59
+ await fse.writeFile(prettierLocation, JSON.stringify(finalConfig, null, 2), "utf-8");
60
+
21
61
  if (!format) return true;
62
+
63
+ // Format project files
22
64
  const spinner = ora("Formatting files with prettier...").start();
23
65
  await $`npx prettier --write .`;
24
66
  spinner.succeed("Files formatted with prettier");
67
+
25
68
  return true;
26
69
  };