Skip to content

runAddCommand doesn't actually test for conditions #770

@GrygrFlzr

Description

@GrygrFlzr

When the user runs

npx sv add drizzle=database:sqlite+client:libsql

The user will get:

┌  Welcome to the Svelte CLI! (v0.9.9)
│
■  Error: Incompatible 'drizzle' option specified: 'libsql'

This is because the following lines of code:

const optionEntry = optionEntries.find(
([id, question]) => id === optionId || question.group === optionId
);

Will only check if the id or group matches, which unfortunately means that with our current drizzle definitions:
.add('postgresql', {
question: 'Which PostgreSQL client would you like to use?',
type: 'select',
group: 'client',
default: 'postgres.js',
options: [
{ value: 'postgres.js', label: 'Postgres.JS', hint: 'recommended for most users' },
{ value: 'neon', label: 'Neon', hint: 'popular hosted platform' }
],
condition: ({ database }) => database === 'postgresql'
})
.add('mysql', {
question: 'Which MySQL client would you like to use?',
type: 'select',
group: 'client',
default: 'mysql2',
options: [
{ value: 'mysql2', hint: 'recommended for most users' },
{ value: 'planetscale', label: 'PlanetScale', hint: 'popular hosted platform' }
],
condition: ({ database }) => database === 'mysql'
})
.add('sqlite', {
question: 'Which SQLite client would you like to use?',
type: 'select',
group: 'client',
default: 'libsql',
options: [
{ value: 'better-sqlite3', hint: 'for traditional Node environments' },
{ value: 'libsql', label: 'libSQL', hint: 'for serverless environments' },
{ value: 'turso', label: 'Turso', hint: 'popular hosted platform' }
],
condition: ({ database }) => database === 'sqlite'
})

.find will return the first match... which is always postgresql, because it has group: 'client' and happens to be declared first.
This means npx sv add drizzle=... only ever works for postgresql, and not mysql or sqlite.

The fix is to always check the condition is valid when it exists:

 const optionEntries = Object.entries(details.options);
+const specifiedOptionsObject = Object.fromEntries(
+        specifiedOptions.map((option) => option.split(':', 2))
+);
 for (const option of specifiedOptions) {
         let [optionId, optionValue] = option.split(':', 2);
 
         // validates that the option exists
-        const optionEntry = optionEntries.find(
-                ([id, question]) => id === optionId || question.group === optionId
-        );
+        const optionEntry = optionEntries.find(([id, question]) => {
+                // simple ID match
+                if (id === optionId) return true;
+
+                // group match - need to check conditions and value validity
+                if (question.group === optionId) {
+                        // does the value exist for this option?
+                        if (question.type === 'select' || question.type === 'multiselect') {
+                                const isValidValue = question.options.some((opt) => opt.value === optionValue);
+                                if (!isValidValue) return false;
+                        }
+
+                        // if there's a condition, does it pass?
+                        if (question.condition) {
+                                return question.condition(specifiedOptionsObject);
+                        }
+
+                        // finally, unconditional
+                        return true;
+                }
+
+                // unrecognized optionId
+                return false;
+        });
+
         if (!optionEntry) {
                 const { choices } = getOptionChoices(details);
                 throw new Error(

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions