Skip to content

Commit 87f2bea

Browse files
manuel3108benmccannAdrianGonz97
authored
chore: improve community adders (#47)
Co-authored-by: Ben McCann <[email protected]> Co-authored-by: AdrianGonz97 <[email protected]>
1 parent c8147c5 commit 87f2bea

File tree

12 files changed

+140
-99
lines changed

12 files changed

+140
-99
lines changed

community/unocss.ts renamed to community-adders/unocss.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,5 @@ export default {
66
category: 'CSS',
77
npm: 'unocss-svelte-integration',
88
repo: 'https://github.com/owner-name/repo-name',
9-
website: 'https://unocss.dev',
10-
logo: 'unocss.svg'
9+
website: 'https://unocss.dev'
1110
} satisfies CommunityAdder;

community/unplugin-icons.ts renamed to community-adders/unplugin-icons.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import type { CommunityAdder } from '../packages/adders/_config/community.ts';
33
export default {
44
name: 'unplugin-icons',
55
description: 'Access thousands of icons as components on-demand universally',
6-
category: 'Icons',
6+
category: 'Icon',
77
npm: 'unplugin-icons-svelte-integration',
88
repo: 'https://github.com/owner-name/repo-name',
9-
website: 'https://www.npmjs.com/package/unplugin-icons',
10-
logo: 'unplugin-icons.svg'
9+
website: 'https://github.com/unplugin/unplugin-icons'
1110
} satisfies CommunityAdder;
Lines changed: 13 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,16 @@
1-
export type CategoryKeys = 'codeQuality' | 'css' | 'db' | 'testing' | 'auth' | 'additional';
1+
export type Category = (typeof categories)[number];
2+
export type CommunityCategory = (typeof communityCategories)[number];
23

3-
export type CategoryInfo = {
4-
id: CategoryKeys;
5-
name: string;
6-
description: string;
7-
};
4+
export type AdderCategories = Record<Category, string[]>;
85

9-
export type CategoryDetails = Record<CategoryKeys, CategoryInfo>;
6+
// the order defined here is how it'll be shown in the prompt
7+
export const categories = [
8+
'Code Quality',
9+
'Testing',
10+
'CSS',
11+
'Database',
12+
'Auth',
13+
'Additional Functionality'
14+
] as const;
1015

11-
export type AdderCategories = Record<CategoryKeys, string[]>;
12-
13-
export const categories: CategoryDetails = {
14-
codeQuality: {
15-
id: 'codeQuality',
16-
name: 'Code Quality',
17-
description: ''
18-
},
19-
testing: {
20-
id: 'testing',
21-
name: 'Testing',
22-
description: ''
23-
},
24-
css: {
25-
id: 'css',
26-
name: 'CSS',
27-
description: 'Can be used to style your components'
28-
},
29-
db: {
30-
id: 'db',
31-
name: 'Database',
32-
description: ''
33-
},
34-
auth: {
35-
id: 'auth',
36-
name: 'Auth',
37-
description: ''
38-
},
39-
additional: {
40-
id: 'additional',
41-
name: 'Additional functionality',
42-
description: ''
43-
}
44-
};
16+
export const communityCategories = ['Icon'] as const;
Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1+
import type { Category, CommunityCategory } from './categories.ts';
2+
13
export type CommunityAdder = {
24
name: string;
35
description: string;
4-
category: string;
6+
category: Category | CommunityCategory;
57
npm: string;
68
repo: string;
79
website: string;
8-
logo: string;
910
};
1011

1112
/** EVALUATED AT BUILD TIME */
1213
export const communityAdderIds: string[] = [];
1314

14-
export async function getCommunityAdders(name: string): Promise<CommunityAdder> {
15-
const { default: details } = await import(`../../../community/${name}.ts`);
15+
export async function getCommunityAdder(name: string): Promise<CommunityAdder> {
16+
const { default: details } = await import(`../../../community-adders/${name}.ts`);
1617
return details;
1718
}

packages/adders/_config/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
export { adderIds, adderCategories, getAdderDetails } from './official.ts';
2-
export { categories, type CategoryKeys, type CategoryInfo } from './categories.ts';
3-
export { getCommunityAdders, communityAdderIds } from './community.ts';
2+
export {
3+
categories,
4+
communityCategories,
5+
type Category,
6+
type CommunityCategory
7+
} from './categories.ts';
8+
export { getCommunityAdder, communityAdderIds } from './community.ts';

packages/adders/_config/official.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { AdderCategories } from './categories.ts';
2-
import type { AdderWithoutExplicitArgs } from '@svelte-cli/core';
1+
import type { AdderCategories, Category } from './categories.ts';
2+
import type { AdderWithoutExplicitArgs, Adder } from '@svelte-cli/core';
33

44
// adders
55
import drizzle from '../drizzle/index.ts';
@@ -13,13 +13,13 @@ import storybook from '../storybook/index.ts';
1313
import tailwindcss from '../tailwindcss/index.ts';
1414
import vitest from '../vitest/index.ts';
1515

16-
const categories = {
17-
codeQuality: [prettier, eslint],
18-
testing: [vitest, playwright],
19-
css: [tailwindcss],
20-
db: [drizzle],
21-
auth: [lucia],
22-
additional: [storybook, mdsvex, routify]
16+
const categories: Record<Category, Array<Adder<any>>> = {
17+
'Code Quality': [prettier, eslint],
18+
Testing: [vitest, playwright],
19+
CSS: [tailwindcss],
20+
Database: [drizzle],
21+
Auth: [lucia],
22+
'Additional Functionality': [storybook, mdsvex, routify]
2323
};
2424

2525
export const adderCategories: AdderCategories = getCategoriesById();

packages/adders/eslint/config/adder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const adder = defineAdderConfig({
1414
website: {
1515
logo: './eslint.svg',
1616
keywords: ['eslint', 'code', 'linter'],
17-
documentation: 'https://eslint.org/'
17+
documentation: 'https://eslint.org'
1818
}
1919
},
2020
options,

packages/adders/playwright/config/adder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export const adder = defineAdderConfig({
1313
website: {
1414
logo: './playwright.svg',
1515
keywords: ['test', 'testing', 'end-to-end', 'e2e', 'integration'],
16-
documentation: 'https://playwright.dev/'
16+
documentation: 'https://playwright.dev'
1717
}
1818
},
1919
options,

packages/adders/vitest/config/adder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const adder = defineAdderConfig({
1111
website: {
1212
logo: './vitest.svg',
1313
keywords: ['test', 'testing', 'unit', 'unit-testing'],
14-
documentation: 'https://vitest.dev/'
14+
documentation: 'https://vitest.dev'
1515
}
1616
},
1717
options,

packages/cli/commands/add.ts

Lines changed: 82 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
adderIds,
1212
getAdderDetails,
1313
communityAdderIds,
14-
getCommunityAdders
14+
getCommunityAdder
1515
} from '@svelte-cli/adders';
1616
import {
1717
createOrUpdateFiles,
@@ -39,7 +39,7 @@ const OptionsSchema = v.strictObject({
3939
cwd: v.string(),
4040
install: v.boolean(),
4141
preconditions: v.boolean(),
42-
community: v.optional(AddersSchema),
42+
community: v.optional(v.union([AddersSchema, v.boolean()])),
4343
...AdderOptionFlagsSchema.entries
4444
});
4545
type Options = v.InferOutput<typeof OptionsSchema>;
@@ -76,17 +76,6 @@ export const add = new Command('add')
7676
process.exit(1);
7777
}
7878

79-
// we'll print a list of available options when `--community` is specified with no args
80-
if (opts.community === true) {
81-
console.log('Usage: sv add --community [adder...]\n');
82-
console.log('Applies community made adders\n');
83-
console.log(`Available options: ${communityAdderIds.join(', ')}\n`);
84-
console.warn(
85-
'The Svelte maintainers have not reviewed community adders for malicious code. Use at your discretion.'
86-
);
87-
process.exit(0);
88-
}
89-
9079
const adders = v.parse(AddersSchema, adderArgs);
9180
const options = v.parse(OptionsSchema, opts);
9281

@@ -185,8 +174,47 @@ export async function runAddCommand(options: Options, adders: string[]): Promise
185174
}
186175
}
187176

177+
type AdderChoices = Record<string, Array<{ value: string; label: string }>>;
178+
179+
// we'll let the user choose community adders when `--community` is specified without args
180+
if (options.community === true) {
181+
const promptOptions: AdderChoices = {};
182+
const communityAdders = await Promise.all(
183+
communityAdderIds.map(async (id) => ({ id, ...(await getCommunityAdder(id)) }))
184+
);
185+
const categories = new Set(communityAdders.map((adder) => adder.category));
186+
187+
for (const category of categories) {
188+
promptOptions[category] = communityAdders
189+
.filter((adder) => adder.category === category)
190+
.map((adder) => ({
191+
value: adder.id,
192+
label: adder.name,
193+
hint: adder.repo
194+
}));
195+
}
196+
197+
const selected = await p.groupMultiselect({
198+
message: 'Which community tools would you like to add to your project?',
199+
options: promptOptions,
200+
spacedGroups: true,
201+
selectableGroups: false,
202+
required: false
203+
});
204+
205+
if (p.isCancel(selected)) {
206+
p.cancel('Operation cancelled.');
207+
process.exit(1);
208+
} else if (selected.length === 0) {
209+
p.cancel('No adders selected. Exiting.');
210+
process.exit(1);
211+
}
212+
213+
options.community = selected;
214+
}
215+
188216
// validate and download community adders
189-
if (options.community && options.community?.length > 0) {
217+
if (Array.isArray(options.community) && options.community.length > 0) {
190218
// validate adders
191219
const adders = options.community.map((id) => {
192220
// ids with directives are passed unmodified so they can be processed during downloads
@@ -208,16 +236,33 @@ export async function runAddCommand(options: Options, adders: string[]): Promise
208236
start('Resolving community adder packages');
209237
const pkgs = await Promise.all(
210238
adders.map(async (id) => {
211-
const pkg = await getCommunityAdders(id).catch(() => undefined);
212-
const packageName = pkg?.npm ?? id;
213-
return getPackageJSON({ cwd: options.cwd, packageName });
239+
const adder = await getCommunityAdder(id).catch(() => undefined);
240+
const packageName = adder?.npm ?? id;
241+
const details = await getPackageJSON({ cwd: options.cwd, packageName });
242+
return {
243+
...details,
244+
// prioritize community adder defined repo urls
245+
repo: adder?.repo ?? details.repo
246+
};
214247
})
215248
);
216249
stop('Resolved community adder packages');
217250

218-
const ids = pkgs.map(({ pkg }) => pc.yellowBright(pkg.name) + pc.dim(` (v${pkg.version})`));
219-
p.log.warn('Community packages are not reviewed for malicious code:');
220-
p.log.message(ids.join(', '));
251+
p.log.warn(
252+
'The Svelte maintainers have not reviewed community adders for malicious code. Use at your discretion.'
253+
);
254+
255+
const paddingName = getPadding(pkgs.map(({ pkg }) => pkg.name));
256+
const paddingVersion = getPadding(pkgs.map(({ pkg }) => `(v${pkg.version})`));
257+
258+
const packageInfos = pkgs.map(({ pkg, repo: _repo }) => {
259+
const name = pc.yellowBright(pkg.name.padEnd(paddingName));
260+
const version = pc.dim(`(v${pkg.version})`.padEnd(paddingVersion));
261+
const repo = pc.dim(`(${_repo})`);
262+
return `${name} ${version} ${repo}`;
263+
});
264+
p.log.message(packageInfos.join('\n'));
265+
221266
const confirm = await p.confirm({ message: 'Would you like to continue?' });
222267
if (confirm !== true) {
223268
p.cancel('Operation cancelled.');
@@ -241,24 +286,28 @@ export async function runAddCommand(options: Options, adders: string[]): Promise
241286

242287
// prompt which adders to apply
243288
if (selectedAdders.length === 0) {
244-
const adderOptions: Record<string, Array<{ value: string; label: string }>> = {};
289+
const adderOptions: AdderChoices = {};
245290
const workspace = createWorkspace(options.cwd);
246291
const projectType = workspace.kit ? 'kit' : 'svelte';
247-
for (const { id, name } of Object.values(categories)) {
248-
const category = adderCategories[id];
249-
const categoryOptions = category
292+
for (const category of categories) {
293+
const adderIds = adderCategories[category];
294+
const categoryOptions = adderIds
250295
.map((id) => {
251296
const config = getAdderDetails(id).config;
252297
// we'll only display adders within their respective project types
253298
if (projectType === 'kit' && !config.metadata.environments.kit) return;
254299
if (projectType === 'svelte' && !config.metadata.environments.svelte) return;
255300

256-
return { label: config.metadata.name, value: config.metadata.id };
301+
return {
302+
label: config.metadata.name,
303+
value: config.metadata.id,
304+
hint: config.metadata.website?.documentation
305+
};
257306
})
258307
.filter((c) => !!c);
259308

260309
if (categoryOptions.length > 0) {
261-
adderOptions[name] = categoryOptions;
310+
adderOptions[category] = categoryOptions;
262311
}
263312
}
264313

@@ -526,12 +575,9 @@ async function processExternalAdder<Args extends OptionDefinition>(
526575
if (!TESTING) p.log.message(`Executing external command ${pc.gray(`(${config.metadata.id})`)}`);
527576

528577
try {
578+
const env = { ...process.env, ...config.environment };
529579
await exec('npx', config.command.split(' '), {
530-
nodeOptions: {
531-
cwd,
532-
env: Object.assign(process.env, config.environment ?? {}),
533-
stdio: TESTING ? 'pipe' : 'inherit'
534-
}
580+
nodeOptions: { cwd, env, stdio: TESTING ? 'pipe' : 'inherit' }
535581
});
536582
} catch (error) {
537583
const typedError = error as Error;
@@ -622,3 +668,8 @@ function getOptionChoices(details: AdderWithoutExplicitArgs) {
622668
}
623669
return { choices, defaults, groups };
624670
}
671+
672+
function getPadding(lines: string[]) {
673+
const lengths = lines.map((s) => s.length);
674+
return Math.max(...lengths);
675+
}

0 commit comments

Comments
 (0)