Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/early-trees-go.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'sv': patch
---

chore(core): change `defineAddonOptions({ /*config */ })` to `defineAddonOptions().add('key', { /*config */ }).build()` in order to provide better type safety.
8 changes: 4 additions & 4 deletions community-addon-template/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { defineAddon, defineAddonOptions } from '@sveltejs/cli-core';
import { imports } from '@sveltejs/cli-core/js';
import { parseSvelte } from '@sveltejs/cli-core/parsers';

export const options = defineAddonOptions({
demo: {
export const options = defineAddonOptions()
.add('demo', {
question: 'Do you want to use a demo?',
type: 'boolean',
default: false
}
});
})
.build();

export default defineAddon({
id: 'community-addon',
Expand Down
35 changes: 18 additions & 17 deletions packages/addons/drizzle/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,46 +6,47 @@ import { parseJson, parseScript } from '@sveltejs/cli-core/parsers';
import { resolveCommand } from 'package-manager-detector/commands';
import { getNodeTypesVersion } from '../common.ts';

const PORTS = {
type Database = 'mysql' | 'postgresql' | 'sqlite';
const PORTS: Record<Database, string> = {
mysql: '3306',
postgresql: '5432',
sqlite: ''
} as const;
};

const options = defineAddonOptions({
database: {
const options = defineAddonOptions()
.add('database', {
question: 'Which database would you like to use?',
type: 'select',
default: 'sqlite',
default: 'sqlite' as Database,
options: [
{ value: 'postgresql', label: 'PostgreSQL' },
{ value: 'mysql', label: 'MySQL' },
{ value: 'sqlite', label: 'SQLite' }
]
},
postgresql: {
})
.add('postgresql', {
question: 'Which PostgreSQL client would you like to use?',
type: 'select',
group: 'client',
default: 'postgres.js',
default: 'postgres.js' as 'postgres.js' | 'neon',
options: [
{ value: 'postgres.js', label: 'Postgres.JS', hint: 'recommended for most users' },
{ value: 'neon', label: 'Neon', hint: 'popular hosted platform' }
],
condition: ({ database }) => database === 'postgresql'
},
mysql: {
})
.add('mysql', {
question: 'Which MySQL client would you like to use?',
type: 'select',
group: 'client',
default: 'mysql2',
default: 'mysql2' as 'mysql2' | 'planetscale',
options: [
{ value: 'mysql2', hint: 'recommended for most users' },
{ value: 'planetscale', label: 'PlanetScale', hint: 'popular hosted platform' }
],
condition: ({ database }) => database === 'mysql'
},
sqlite: {
})
.add('sqlite', {
question: 'Which SQLite client would you like to use?',
type: 'select',
group: 'client',
Expand All @@ -56,16 +57,16 @@ const options = defineAddonOptions({
{ value: 'turso', label: 'Turso', hint: 'popular hosted platform' }
],
condition: ({ database }) => database === 'sqlite'
},
docker: {
})
.add('docker', {
question: 'Do you want to run the database locally with docker-compose?',
default: false,
type: 'boolean',
condition: ({ database, mysql, postgresql }) =>
(database === 'mysql' && mysql === 'mysql2') ||
(database === 'postgresql' && postgresql === 'postgres.js')
}
});
})
.build();

export default defineAddon({
id: 'drizzle',
Expand Down
8 changes: 4 additions & 4 deletions packages/addons/lucia/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ type Dialect = 'mysql' | 'postgresql' | 'sqlite' | 'turso';
let drizzleDialect: Dialect;
let schemaPath: string;

const options = defineAddonOptions({
demo: {
const options = defineAddonOptions()
.add('demo', {
type: 'boolean',
default: true,
question: `Do you want to include a demo? ${colors.dim('(includes a login/register page)')}`
}
});
})
.build();

export default defineAddon({
id: 'lucia',
Expand Down
12 changes: 6 additions & 6 deletions packages/addons/paraglide/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ const DEFAULT_INLANG_PROJECT = {
}
};

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

return undefined;
}
},
demo: {
})
.add('demo', {
type: 'boolean',
default: true,
question: 'Do you want to include a demo?'
}
});
})
.build();

export default defineAddon({
id: 'paraglide',
Expand Down
8 changes: 4 additions & 4 deletions packages/addons/sveltekit-adapter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ const adapters: Adapter[] = [
{ id: 'netlify', package: '@sveltejs/adapter-netlify', version: '^5.0.0' }
];

const options = defineAddonOptions({
adapter: {
const options = defineAddonOptions()
.add('adapter', {
type: 'select',
question: 'Which SvelteKit adapter would you like to use?',
options: adapters.map((p) => ({ value: p.id, label: p.id, hint: p.package })),
default: 'auto'
}
});
})
.build();

export default defineAddon({
id: 'sveltekit-adapter',
Expand Down
47 changes: 20 additions & 27 deletions packages/addons/tailwindcss/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,30 @@ import { imports, vite } from '@sveltejs/cli-core/js';
import { parseCss, parseJson, parseScript, parseSvelte } from '@sveltejs/cli-core/parsers';
import { addSlot } from '@sveltejs/cli-core/html';

type Plugin = {
id: string;
package: string;
version: string;
identifier: string;
};

const plugins: Plugin[] = [
{
id: 'typography',
function typedEntries<T extends Record<string, any>>(obj: T): Array<[keyof T, T[keyof T]]> {
return Object.entries(obj) as Array<[keyof T, T[keyof T]]>;
}

const plugins = {
typography: {
package: '@tailwindcss/typography',
version: '^0.5.15',
identifier: 'typography'
version: '^0.5.15'
},
{
id: 'forms',
forms: {
package: '@tailwindcss/forms',
version: '^0.5.9',
identifier: 'forms'
version: '^0.5.9'
}
];
} as const;

const options = defineAddonOptions({
plugins: {
const options = defineAddonOptions()
.add('plugins', {
type: 'multiselect',
question: 'Which plugins would you like to add?',
options: plugins.map((p) => ({ value: p.id, label: p.id, hint: p.package })),
default: [],
options: typedEntries(plugins).map(([id, p]) => ({ value: id, label: id, hint: p.package })),
default: [] as Array<keyof typeof plugins>,
required: false
}
});
})
.build();

export default defineAddon({
id: 'tailwindcss',
Expand All @@ -49,8 +42,8 @@ export default defineAddon({

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

for (const plugin of plugins) {
if (!options.plugins.includes(plugin.id)) continue;
for (const [id, plugin] of typedEntries(plugins)) {
if (!options.plugins.includes(id)) continue;

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

for (const plugin of plugins) {
if (!options.plugins.includes(plugin.id)) continue;
for (const [id, plugin] of typedEntries(plugins)) {
if (!options.plugins.includes(id)) continue;

const pluginRule = findAtRule('plugin', plugin.package);
if (!pluginRule) {
Expand Down
8 changes: 4 additions & 4 deletions packages/addons/vitest-addon/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { dedent, defineAddon, defineAddonOptions, log } from '@sveltejs/cli-core
import { array, exports, functions, object } from '@sveltejs/cli-core/js';
import { parseJson, parseScript } from '@sveltejs/cli-core/parsers';

const options = defineAddonOptions({
usages: {
const options = defineAddonOptions()
.add('usages', {
question: 'What do you want to use vitest for?',
type: 'multiselect',
default: ['unit', 'component'],
Expand All @@ -12,8 +12,8 @@ const options = defineAddonOptions({
{ value: 'component', label: 'component testing' }
],
required: true
}
});
})
.build();

export default defineAddon({
id: 'vitest',
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/commands/add/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,7 @@ function getOptionChoices(details: AddonWithoutExplicitArgs) {
const options: Record<string, unknown> = {};
for (const [id, question] of Object.entries(details.options)) {
let values: string[] = [];
const applyDefault = question.condition?.(options) !== false;
const applyDefault = question.condition?.(options as any) !== false;
if (question.type === 'boolean') {
values = ['yes', `no`];
if (applyDefault) {
Expand Down
32 changes: 28 additions & 4 deletions packages/core/addon/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,37 @@ export type TestDefinition<Args extends OptionDefinition> = {
condition?: (options: OptionValues<Args>) => boolean;
};

export function defineAddonOptions<const Args extends OptionDefinition>(options: Args): Args {
return options;
}

type MaybePromise<T> = Promise<T> | T;

export type Verification = {
name: string;
run: () => MaybePromise<{ success: boolean; message: string | undefined }>;
};

// Builder pattern for addon options
export type OptionBuilder<T extends OptionDefinition = Record<string, any>> = {
add<K extends string, Q extends Question<T & Record<K, Q>>>(
key: K,
question: Q
): OptionBuilder<T & Record<K, Q>>;
build(): T;
};

export function defineAddonOptions(): OptionBuilder<Record<string, any>> {
return createOptionBuilder({} as Record<string, any>);
}

function createOptionBuilder<T extends OptionDefinition>(options: T = {} as T): OptionBuilder<T> {
return {
add<K extends string, Q extends Question<T & Record<K, Q>>>(
key: K,
question: Q
): OptionBuilder<T & Record<K, Q>> {
const newOptions = { ...options, [key]: question } as T & Record<K, Q>;
return createOptionBuilder(newOptions);
},
build(): T {
return options;
}
};
}
19 changes: 9 additions & 10 deletions packages/core/addon/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,31 +20,30 @@ export type NumberQuestion = {
export type SelectQuestion<Value = any> = {
type: 'select';
default: Value;
options: Array<{ value: Value; label?: string; hint?: string }>;
options: Array<{ value: string; label?: string; hint?: string }>;
};

export type MultiSelectQuestion<Value = any> = {
export type MultiSelectQuestion<Value extends any[] = string[]> = {
type: 'multiselect';
default: Value[];
options: Array<{ value: Value; label?: string; hint?: string }>;
default: Value;
options: Array<{ value: string; label?: string; hint?: string }>;
required: boolean;
};

export type BaseQuestion = {
export type BaseQuestion<Args extends OptionDefinition = OptionDefinition> = {
question: string;
group?: string;
/**
* When this condition explicitly returns `false`, the question's value will
* always be `undefined` and will not fallback to the specified `default` value.
*/
condition?: (options: any) => boolean;
// TODO: we want to type `options` similar to OptionValues<Args> so that its option values can be inferred
condition?: (options: OptionValues<Args>) => boolean;
};

export type Question = BaseQuestion &
export type Question<Args extends OptionDefinition = OptionDefinition> = BaseQuestion<Args> &
(BooleanQuestion | StringQuestion | NumberQuestion | SelectQuestion | MultiSelectQuestion);

export type OptionDefinition = Record<string, Question>;
export type OptionDefinition = Record<string, Question<any>>;
export type OptionValues<Args extends OptionDefinition> = {
[K in keyof Args]: Args[K] extends StringQuestion
? string
Expand All @@ -55,6 +54,6 @@ export type OptionValues<Args extends OptionDefinition> = {
: Args[K] extends SelectQuestion<infer Value>
? Value
: Args[K] extends MultiSelectQuestion<infer Value>
? Value[]
? Value // as the type of Value should already be an array (default: [] or default: ['foo', 'bar'])
: never;
};
Loading