Skip to content

Commit 8876283

Browse files
jycouetmanuel3108
andauthored
Builder pattern for addon options (#686)
* Builder pattern for addon options * Piouf even more amazing * internal should be ok * rmv defineAddonOptions and break things * typiiiiiing * update changelog * naming update * TS * playing with types * add comment * Update .changeset/early-trees-go.md --------- Co-authored-by: Manuel <[email protected]>
1 parent 7c71303 commit 8876283

File tree

11 files changed

+103
-81
lines changed

11 files changed

+103
-81
lines changed

.changeset/early-trees-go.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'sv': patch
3+
---
4+
5+
chore(core): change `defineAddonOptions({ /*config */ })` to `defineAddonOptions().add('key', { /*config */ }).build()` in order to provide better type safety.

community-addon-template/src/index.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import { defineAddon, defineAddonOptions } from '@sveltejs/cli-core';
22
import { imports } from '@sveltejs/cli-core/js';
33
import { parseSvelte } from '@sveltejs/cli-core/parsers';
44

5-
export const options = defineAddonOptions({
6-
demo: {
5+
export const options = defineAddonOptions()
6+
.add('demo', {
77
question: 'Do you want to use a demo?',
88
type: 'boolean',
99
default: false
10-
}
11-
});
10+
})
11+
.build();
1212

1313
export default defineAddon({
1414
id: 'community-addon',

packages/addons/drizzle/index.ts

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,46 +6,47 @@ import { parseJson, parseScript } from '@sveltejs/cli-core/parsers';
66
import { resolveCommand } from 'package-manager-detector/commands';
77
import { getNodeTypesVersion } from '../common.ts';
88

9-
const PORTS = {
9+
type Database = 'mysql' | 'postgresql' | 'sqlite';
10+
const PORTS: Record<Database, string> = {
1011
mysql: '3306',
1112
postgresql: '5432',
1213
sqlite: ''
13-
} as const;
14+
};
1415

15-
const options = defineAddonOptions({
16-
database: {
16+
const options = defineAddonOptions()
17+
.add('database', {
1718
question: 'Which database would you like to use?',
1819
type: 'select',
19-
default: 'sqlite',
20+
default: 'sqlite' as Database,
2021
options: [
2122
{ value: 'postgresql', label: 'PostgreSQL' },
2223
{ value: 'mysql', label: 'MySQL' },
2324
{ value: 'sqlite', label: 'SQLite' }
2425
]
25-
},
26-
postgresql: {
26+
})
27+
.add('postgresql', {
2728
question: 'Which PostgreSQL client would you like to use?',
2829
type: 'select',
2930
group: 'client',
30-
default: 'postgres.js',
31+
default: 'postgres.js' as 'postgres.js' | 'neon',
3132
options: [
3233
{ value: 'postgres.js', label: 'Postgres.JS', hint: 'recommended for most users' },
3334
{ value: 'neon', label: 'Neon', hint: 'popular hosted platform' }
3435
],
3536
condition: ({ database }) => database === 'postgresql'
36-
},
37-
mysql: {
37+
})
38+
.add('mysql', {
3839
question: 'Which MySQL client would you like to use?',
3940
type: 'select',
4041
group: 'client',
41-
default: 'mysql2',
42+
default: 'mysql2' as 'mysql2' | 'planetscale',
4243
options: [
4344
{ value: 'mysql2', hint: 'recommended for most users' },
4445
{ value: 'planetscale', label: 'PlanetScale', hint: 'popular hosted platform' }
4546
],
4647
condition: ({ database }) => database === 'mysql'
47-
},
48-
sqlite: {
48+
})
49+
.add('sqlite', {
4950
question: 'Which SQLite client would you like to use?',
5051
type: 'select',
5152
group: 'client',
@@ -56,16 +57,16 @@ const options = defineAddonOptions({
5657
{ value: 'turso', label: 'Turso', hint: 'popular hosted platform' }
5758
],
5859
condition: ({ database }) => database === 'sqlite'
59-
},
60-
docker: {
60+
})
61+
.add('docker', {
6162
question: 'Do you want to run the database locally with docker-compose?',
6263
default: false,
6364
type: 'boolean',
6465
condition: ({ database, mysql, postgresql }) =>
6566
(database === 'mysql' && mysql === 'mysql2') ||
6667
(database === 'postgresql' && postgresql === 'postgres.js')
67-
}
68-
});
68+
})
69+
.build();
6970

7071
export default defineAddon({
7172
id: 'drizzle',

packages/addons/lucia/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ type Dialect = 'mysql' | 'postgresql' | 'sqlite' | 'turso';
2626
let drizzleDialect: Dialect;
2727
let schemaPath: string;
2828

29-
const options = defineAddonOptions({
30-
demo: {
29+
const options = defineAddonOptions()
30+
.add('demo', {
3131
type: 'boolean',
3232
default: true,
3333
question: `Do you want to include a demo? ${colors.dim('(includes a login/register page)')}`
34-
}
35-
});
34+
})
35+
.build();
3636

3737
export default defineAddon({
3838
id: 'lucia',

packages/addons/paraglide/index.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ const DEFAULT_INLANG_PROJECT = {
1616
}
1717
};
1818

19-
const options = defineAddonOptions({
20-
languageTags: {
19+
const options = defineAddonOptions()
20+
.add('languageTags', {
2121
question: `Which languages would you like to support? ${colors.gray('(e.g. en,de-ch)')}`,
2222
type: 'string',
2323
default: 'en, es',
@@ -39,13 +39,13 @@ const options = defineAddonOptions({
3939

4040
return undefined;
4141
}
42-
},
43-
demo: {
42+
})
43+
.add('demo', {
4444
type: 'boolean',
4545
default: true,
4646
question: 'Do you want to include a demo?'
47-
}
48-
});
47+
})
48+
.build();
4949

5050
export default defineAddon({
5151
id: 'paraglide',

packages/addons/sveltekit-adapter/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ const adapters: Adapter[] = [
1717
{ id: 'netlify', package: '@sveltejs/adapter-netlify', version: '^5.0.0' }
1818
];
1919

20-
const options = defineAddonOptions({
21-
adapter: {
20+
const options = defineAddonOptions()
21+
.add('adapter', {
2222
type: 'select',
2323
question: 'Which SvelteKit adapter would you like to use?',
2424
options: adapters.map((p) => ({ value: p.id, label: p.id, hint: p.package })),
2525
default: 'auto'
26-
}
27-
});
26+
})
27+
.build();
2828

2929
export default defineAddon({
3030
id: 'sveltekit-adapter',

packages/addons/tailwindcss/index.ts

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,30 @@ import { imports, vite } from '@sveltejs/cli-core/js';
33
import { parseCss, parseJson, parseScript, parseSvelte } from '@sveltejs/cli-core/parsers';
44
import { addSlot } from '@sveltejs/cli-core/html';
55

6-
type Plugin = {
7-
id: string;
8-
package: string;
9-
version: string;
10-
identifier: string;
11-
};
12-
13-
const plugins: Plugin[] = [
14-
{
15-
id: 'typography',
6+
function typedEntries<T extends Record<string, any>>(obj: T): Array<[keyof T, T[keyof T]]> {
7+
return Object.entries(obj) as Array<[keyof T, T[keyof T]]>;
8+
}
9+
10+
const plugins = {
11+
typography: {
1612
package: '@tailwindcss/typography',
17-
version: '^0.5.15',
18-
identifier: 'typography'
13+
version: '^0.5.15'
1914
},
20-
{
21-
id: 'forms',
15+
forms: {
2216
package: '@tailwindcss/forms',
23-
version: '^0.5.9',
24-
identifier: 'forms'
17+
version: '^0.5.9'
2518
}
26-
];
19+
} as const;
2720

28-
const options = defineAddonOptions({
29-
plugins: {
21+
const options = defineAddonOptions()
22+
.add('plugins', {
3023
type: 'multiselect',
3124
question: 'Which plugins would you like to add?',
32-
options: plugins.map((p) => ({ value: p.id, label: p.id, hint: p.package })),
33-
default: [],
25+
options: typedEntries(plugins).map(([id, p]) => ({ value: id, label: id, hint: p.package })),
26+
default: [] as Array<keyof typeof plugins>,
3427
required: false
35-
}
36-
});
28+
})
29+
.build();
3730

3831
export default defineAddon({
3932
id: 'tailwindcss',
@@ -49,8 +42,8 @@ export default defineAddon({
4942

5043
if (prettierInstalled) sv.devDependency('prettier-plugin-tailwindcss', '^0.6.11');
5144

52-
for (const plugin of plugins) {
53-
if (!options.plugins.includes(plugin.id)) continue;
45+
for (const [id, plugin] of typedEntries(plugins)) {
46+
if (!options.plugins.includes(id)) continue;
5447

5548
sv.devDependency(plugin.package, plugin.version);
5649
}
@@ -88,8 +81,8 @@ export default defineAddon({
8881
const lastAtRule = atRules.findLast((rule) => ['plugin', 'import'].includes(rule.name));
8982
const pluginPos = lastAtRule!.source!.end!.offset;
9083

91-
for (const plugin of plugins) {
92-
if (!options.plugins.includes(plugin.id)) continue;
84+
for (const [id, plugin] of typedEntries(plugins)) {
85+
if (!options.plugins.includes(id)) continue;
9386

9487
const pluginRule = findAtRule('plugin', plugin.package);
9588
if (!pluginRule) {

packages/addons/vitest-addon/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { dedent, defineAddon, defineAddonOptions, log } from '@sveltejs/cli-core
22
import { array, exports, functions, object } from '@sveltejs/cli-core/js';
33
import { parseJson, parseScript } from '@sveltejs/cli-core/parsers';
44

5-
const options = defineAddonOptions({
6-
usages: {
5+
const options = defineAddonOptions()
6+
.add('usages', {
77
question: 'What do you want to use vitest for?',
88
type: 'multiselect',
99
default: ['unit', 'component'],
@@ -12,8 +12,8 @@ const options = defineAddonOptions({
1212
{ value: 'component', label: 'component testing' }
1313
],
1414
required: true
15-
}
16-
});
15+
})
16+
.build();
1717

1818
export default defineAddon({
1919
id: 'vitest',

packages/cli/commands/add/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,7 @@ function getOptionChoices(details: AddonWithoutExplicitArgs) {
637637
const options: Record<string, unknown> = {};
638638
for (const [id, question] of Object.entries(details.options)) {
639639
let values: string[] = [];
640-
const applyDefault = question.condition?.(options) !== false;
640+
const applyDefault = question.condition?.(options as any) !== false;
641641
if (question.type === 'boolean') {
642642
values = ['yes', `no`];
643643
if (applyDefault) {

packages/core/addon/config.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,37 @@ export type TestDefinition<Args extends OptionDefinition> = {
7878
condition?: (options: OptionValues<Args>) => boolean;
7979
};
8080

81-
export function defineAddonOptions<const Args extends OptionDefinition>(options: Args): Args {
82-
return options;
83-
}
84-
8581
type MaybePromise<T> = Promise<T> | T;
8682

8783
export type Verification = {
8884
name: string;
8985
run: () => MaybePromise<{ success: boolean; message: string | undefined }>;
9086
};
87+
88+
// Builder pattern for addon options
89+
export type OptionBuilder<T extends OptionDefinition = Record<string, any>> = {
90+
add<K extends string, Q extends Question<T & Record<K, Q>>>(
91+
key: K,
92+
question: Q
93+
): OptionBuilder<T & Record<K, Q>>;
94+
build(): T;
95+
};
96+
97+
export function defineAddonOptions(): OptionBuilder<Record<string, any>> {
98+
return createOptionBuilder({} as Record<string, any>);
99+
}
100+
101+
function createOptionBuilder<T extends OptionDefinition>(options: T = {} as T): OptionBuilder<T> {
102+
return {
103+
add<K extends string, Q extends Question<T & Record<K, Q>>>(
104+
key: K,
105+
question: Q
106+
): OptionBuilder<T & Record<K, Q>> {
107+
const newOptions = { ...options, [key]: question } as T & Record<K, Q>;
108+
return createOptionBuilder(newOptions);
109+
},
110+
build(): T {
111+
return options;
112+
}
113+
};
114+
}

0 commit comments

Comments
 (0)