Skip to content

Commit e4a5ac2

Browse files
fix: adjust add-on option types (#692)
Co-authored-by: jycouet <[email protected]>
1 parent eee212e commit e4a5ac2

File tree

8 files changed

+61
-54
lines changed

8 files changed

+61
-54
lines changed

.changeset/mighty-papers-hammer.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+
fix: improve add-on option types

packages/addons/drizzle/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const options = defineAddonOptions()
1717
.add('database', {
1818
question: 'Which database would you like to use?',
1919
type: 'select',
20-
default: 'sqlite' as Database,
20+
default: 'sqlite',
2121
options: [
2222
{ value: 'postgresql', label: 'PostgreSQL' },
2323
{ value: 'mysql', label: 'MySQL' },
@@ -28,7 +28,7 @@ const options = defineAddonOptions()
2828
question: 'Which PostgreSQL client would you like to use?',
2929
type: 'select',
3030
group: 'client',
31-
default: 'postgres.js' as 'postgres.js' | 'neon',
31+
default: 'postgres.js',
3232
options: [
3333
{ value: 'postgres.js', label: 'Postgres.JS', hint: 'recommended for most users' },
3434
{ value: 'neon', label: 'Neon', hint: 'popular hosted platform' }
@@ -39,7 +39,7 @@ const options = defineAddonOptions()
3939
question: 'Which MySQL client would you like to use?',
4040
type: 'select',
4141
group: 'client',
42-
default: 'mysql2' as 'mysql2' | 'planetscale',
42+
default: 'mysql2',
4343
options: [
4444
{ value: 'mysql2', hint: 'recommended for most users' },
4545
{ value: 'planetscale', label: 'PlanetScale', hint: 'popular hosted platform' }
@@ -113,7 +113,7 @@ export default defineAddon({
113113
if (options.sqlite === 'better-sqlite3') {
114114
sv.dependency('better-sqlite3', '^11.8.0');
115115
sv.devDependency('@types/better-sqlite3', '^7.6.12');
116-
sv.pnpmBuildDependendency('better-sqlite3');
116+
sv.pnpmBuildDependency('better-sqlite3');
117117
}
118118

119119
if (options.sqlite === 'libsql' || options.sqlite === 'turso')

packages/addons/sveltekit-adapter/index.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,21 @@ import { defineAddon, defineAddonOptions } from '@sveltejs/cli-core';
22
import { exports, functions, imports, object, type AstTypes } from '@sveltejs/cli-core/js';
33
import { parseJson, parseScript } from '@sveltejs/cli-core/parsers';
44

5-
type Adapter = {
6-
id: string;
7-
package: string;
8-
version: string;
9-
};
10-
11-
const adapters: Adapter[] = [
5+
const adapters = [
126
{ id: 'auto', package: '@sveltejs/adapter-auto', version: '^6.0.0' },
137
{ id: 'node', package: '@sveltejs/adapter-node', version: '^5.2.12' },
148
{ id: 'static', package: '@sveltejs/adapter-static', version: '^3.0.8' },
159
{ id: 'vercel', package: '@sveltejs/adapter-vercel', version: '^5.6.3' },
1610
{ id: 'cloudflare', package: '@sveltejs/adapter-cloudflare', version: '^7.0.0' },
1711
{ id: 'netlify', package: '@sveltejs/adapter-netlify', version: '^5.0.0' }
18-
];
12+
] as const;
1913

2014
const options = defineAddonOptions()
2115
.add('adapter', {
2216
type: 'select',
2317
question: 'Which SvelteKit adapter would you like to use?',
24-
options: adapters.map((p) => ({ value: p.id, label: p.id, hint: p.package })),
25-
default: 'auto'
18+
default: 'auto',
19+
options: adapters.map((p) => ({ value: p.id, label: p.id, hint: p.package }))
2620
})
2721
.build();
2822

packages/addons/tailwindcss/index.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,25 @@ 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-
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: {
6+
const plugins = [
7+
{
8+
id: 'typography',
129
package: '@tailwindcss/typography',
1310
version: '^0.5.15'
1411
},
15-
forms: {
12+
{
13+
id: 'forms',
1614
package: '@tailwindcss/forms',
1715
version: '^0.5.9'
1816
}
19-
} as const;
17+
] as const;
2018

2119
const options = defineAddonOptions()
2220
.add('plugins', {
2321
type: 'multiselect',
2422
question: 'Which plugins would you like to add?',
25-
options: typedEntries(plugins).map(([id, p]) => ({ value: id, label: id, hint: p.package })),
26-
default: [] as Array<keyof typeof plugins>,
23+
options: plugins.map((p) => ({ value: p.id, label: p.id, hint: p.package })),
24+
default: [] as Array<(typeof plugins)[number]['id']>,
2725
required: false
2826
})
2927
.build();
@@ -42,8 +40,8 @@ export default defineAddon({
4240

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

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

4846
sv.devDependency(plugin.package, plugin.version);
4947
}
@@ -81,8 +79,8 @@ export default defineAddon({
8179
const lastAtRule = atRules.findLast((rule) => ['plugin', 'import'].includes(rule.name));
8280
const pluginPos = lastAtRule!.source!.end!.offset;
8381

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

8785
const pluginRule = findAtRule('plugin', plugin.package);
8886
if (!pluginRule) {

packages/cli/commands/add/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -634,10 +634,10 @@ function getOptionChoices(details: AddonWithoutExplicitArgs) {
634634
const choices: string[] = [];
635635
const defaults: string[] = [];
636636
const groups: Record<string, string[]> = {};
637-
const options: Record<string, unknown> = {};
637+
const options: OptionValues<any> = {};
638638
for (const [id, question] of Object.entries(details.options)) {
639639
let values: string[] = [];
640-
const applyDefault = question.condition?.(options as any) !== false;
640+
const applyDefault = question.condition?.(options) !== false;
641641
if (question.type === 'boolean') {
642642
values = ['yes', `no`];
643643
if (applyDefault) {

packages/cli/lib/install.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ async function runAddon({ addon, multiple, workspace }: RunAddon) {
173173
devDependency: (pkg, version) => {
174174
dependencies.push({ pkg, version, dev: true });
175175
},
176-
pnpmBuildDependendency: (pkg) => {
176+
pnpmBuildDependency: (pkg) => {
177177
pnpmBuildDependencies.push(pkg);
178178
}
179179
};

packages/core/addon/config.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export type Scripts<Args extends OptionDefinition> = {
2020
};
2121

2222
export type SvApi = {
23-
pnpmBuildDependendency: (pkg: string) => void;
23+
pnpmBuildDependency: (pkg: string) => void;
2424
dependency: (pkg: string, version: string) => void;
2525
devDependency: (pkg: string, version: string) => void;
2626
execute: (args: string[], stdio: 'inherit' | 'pipe') => Promise<void>;
@@ -85,29 +85,32 @@ export type Verification = {
8585
run: () => MaybePromise<{ success: boolean; message: string | undefined }>;
8686
};
8787

88+
type Prettify<T> = {
89+
[K in keyof T]: T[K];
90+
} & unknown;
91+
8892
// 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>>>(
93+
export type OptionBuilder<T extends OptionDefinition> = {
94+
add<K extends string, const Q extends Question<T & Record<K, Q>>>(
9195
key: K,
9296
question: Q
9397
): OptionBuilder<T & Record<K, Q>>;
94-
build(): T;
98+
build(): Prettify<T>;
9599
};
96100

97-
export function defineAddonOptions(): OptionBuilder<Record<string, any>> {
98-
return createOptionBuilder({} as Record<string, any>);
101+
// Initializing with an empty object is intended given that the starting state _is_ empty.
102+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
103+
export function defineAddonOptions(): OptionBuilder<{}> {
104+
return createOptionBuilder({});
99105
}
100106

101-
function createOptionBuilder<T extends OptionDefinition>(options: T = {} as T): OptionBuilder<T> {
107+
function createOptionBuilder<const T extends OptionDefinition>(options: T): OptionBuilder<T> {
102108
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>;
109+
add(key, question) {
110+
const newOptions = { ...options, [key]: question };
108111
return createOptionBuilder(newOptions);
109112
},
110-
build(): T {
113+
build() {
111114
return options;
112115
}
113116
};

packages/core/addon/options.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,20 @@ export type NumberQuestion = {
1717
placeholder?: string;
1818
};
1919

20-
export type SelectQuestion<Value = any> = {
20+
export type SelectQuestion<Value> = {
2121
type: 'select';
22-
default: Value;
23-
options: Array<{ value: string; label?: string; hint?: string }>;
22+
default: NoInfer<Value>;
23+
options: Array<{ value: Value; label?: string; hint?: string }>;
2424
};
2525

26-
export type MultiSelectQuestion<Value extends any[] = string[]> = {
26+
export type MultiSelectQuestion<Value> = {
2727
type: 'multiselect';
28-
default: Value;
29-
options: Array<{ value: string; label?: string; hint?: string }>;
28+
default: NoInfer<Value[]>;
29+
options: Array<{ value: Value; label?: string; hint?: string }>;
3030
required: boolean;
3131
};
3232

33-
export type BaseQuestion<Args extends OptionDefinition = OptionDefinition> = {
33+
export type BaseQuestion<Args extends OptionDefinition> = {
3434
question: string;
3535
group?: string;
3636
/**
@@ -41,9 +41,16 @@ export type BaseQuestion<Args extends OptionDefinition = OptionDefinition> = {
4141
};
4242

4343
export type Question<Args extends OptionDefinition = OptionDefinition> = BaseQuestion<Args> &
44-
(BooleanQuestion | StringQuestion | NumberQuestion | SelectQuestion | MultiSelectQuestion);
44+
(
45+
| BooleanQuestion
46+
| StringQuestion
47+
| NumberQuestion
48+
| SelectQuestion<any>
49+
| MultiSelectQuestion<any>
50+
);
4551

4652
export type OptionDefinition = Record<string, Question<any>>;
53+
4754
export type OptionValues<Args extends OptionDefinition> = {
4855
[K in keyof Args]: Args[K] extends StringQuestion
4956
? string
@@ -54,6 +61,6 @@ export type OptionValues<Args extends OptionDefinition> = {
5461
: Args[K] extends SelectQuestion<infer Value>
5562
? Value
5663
: Args[K] extends MultiSelectQuestion<infer Value>
57-
? Value // as the type of Value should already be an array (default: [] or default: ['foo', 'bar'])
58-
: never;
64+
? Value[]
65+
: 'ERROR: The value for this type is invalid. Ensure that the `default` value exists in `options`.';
5966
};

0 commit comments

Comments
 (0)