From 1cc958e034a3eb37c53c70c981f5a30abe73f201 Mon Sep 17 00:00:00 2001 From: jycouet Date: Tue, 28 Apr 2026 21:51:02 +0200 Subject: [PATCH 1/3] cancel bubble up --- .changeset/fix-cancel-propagation.md | 5 +++ packages/sv/src/cli/add.ts | 10 ++++- packages/sv/src/core/engine.ts | 14 ++++++ packages/sv/src/core/tests/engine.ts | 65 ++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-cancel-propagation.md create mode 100644 packages/sv/src/core/tests/engine.ts diff --git a/.changeset/fix-cancel-propagation.md b/.changeset/fix-cancel-propagation.md new file mode 100644 index 000000000..69c95184b --- /dev/null +++ b/.changeset/fix-cancel-propagation.md @@ -0,0 +1,5 @@ +--- +'sv': patch +--- + +fix(sv): skip add-ons when a `dependsOn` dependency cancels diff --git a/packages/sv/src/cli/add.ts b/packages/sv/src/cli/add.ts index 5f967f901..4e9031dbb 100644 --- a/packages/sv/src/cli/add.ts +++ b/packages/sv/src/cli/add.ts @@ -697,8 +697,14 @@ export async function runAddonsApply({ const successfulAddons = loadedAddons.filter((a) => !canceledAddonIds.includes(a.addon.id)); if (addonSuccess.length === 0) { - p.cancel('All selected add-ons were canceled.'); - process.exit(1); + // `create` already scaffolded the project on disk - exiting here would hide + // the "Project created" success and the next-steps. Just warn instead. + if (fromCommand === 'create') { + p.log.warn('All selected add-ons were canceled.'); + } else { + p.cancel('All selected add-ons were canceled.'); + process.exit(1); + } } else { p.log.success( `Successfully setup add-ons: ${addonSuccess.map((c) => color.addon(c)).join(', ')}` diff --git a/packages/sv/src/core/engine.ts b/packages/sv/src/core/engine.ts index ecd19cd47..c91bc152a 100644 --- a/packages/sv/src/core/engine.ts +++ b/packages/sv/src/core/engine.ts @@ -111,6 +111,7 @@ export async function applyAddons({ }> { const filesToFormat = new Set(); const status: Record = {}; + const canceledAddons = new Set(); const addonDefs = loadedAddons.map((l) => l.addon); const ordered = orderAddons(addonDefs, setupResults); @@ -118,6 +119,18 @@ export async function applyAddons({ let hasFormatter = false; for (const addon of ordered) { + // Skip addons whose `dependsOn` dependency was canceled. Running them would + // fail with misleading errors since they expect state from the canceled addon. + const dependsOn = setupResults[addon.id]?.dependsOn ?? []; + const canceledDeps = dependsOn.filter((dep) => canceledAddons.has(dep)); + if (canceledDeps.length > 0) { + canceledAddons.add(addon.id); + status[addon.id] = canceledDeps.map( + (dep) => `Because dependency '${dep}' was canceled` + ); + continue; + } + const loaded = loadedAddons.find((l) => l.addon.id === addon.id)!; const workspaceOptions = options[addon.id] || {}; @@ -141,6 +154,7 @@ export async function applyAddons({ if (cancels.length === 0) { status[addon.id] = 'success'; } else { + canceledAddons.add(addon.id); status[addon.id] = cancels; } } diff --git a/packages/sv/src/core/tests/engine.ts b/packages/sv/src/core/tests/engine.ts new file mode 100644 index 000000000..7a653c482 --- /dev/null +++ b/packages/sv/src/core/tests/engine.ts @@ -0,0 +1,65 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { defineAddon, defineAddonOptions, type LoadedAddon } from '../config.ts'; +import { applyAddons, setupAddons } from '../engine.ts'; +import { createWorkspace } from '../workspace.ts'; + +function makeWorkspace() { + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sv-engine-')); + fs.writeFileSync(path.join(cwd, 'package.json'), '{"name":"t","private":true}'); + return cwd; +} +const loaded = (a: ReturnType>): LoadedAddon => ({ + reference: { specifier: a.id, options: [], source: { kind: 'official', id: a.id } }, + addon: a +}); +const dep = defineAddon({ + id: 'dep', + options: defineAddonOptions().build(), + run: ({ cancel }) => cancel('nope') +}); + +describe('applyAddons cancel propagation', () => { + it("skips addons whose 'dependsOn' was canceled", async () => { + const child = defineAddon({ + id: 'child', + options: defineAddonOptions().build(), + setup: ({ dependsOn }) => dependsOn('dep' as never), + run: () => expect.fail('child should not have run') + }); + const workspace = await createWorkspace({ cwd: makeWorkspace() }); + const addons = [loaded(dep), loaded(child)]; + const { status } = await applyAddons({ + loadedAddons: addons, + workspace, + setupResults: setupAddons(addons, workspace), + options: { dep: {}, child: {} } + }); + expect(status.dep).toEqual(['nope']); + expect(status.child).toEqual(["Because dependency 'dep' was canceled"]); + }); + + it("does not skip addons that only 'runsAfter' a canceled addon", async () => { + let ran = false; + const child = defineAddon({ + id: 'child', + options: defineAddonOptions().build(), + setup: ({ runsAfter }) => runsAfter('dep' as never), + run: () => { + ran = true; + } + }); + const workspace = await createWorkspace({ cwd: makeWorkspace() }); + const addons = [loaded(dep), loaded(child)]; + const { status } = await applyAddons({ + loadedAddons: addons, + workspace, + setupResults: setupAddons(addons, workspace), + options: { dep: {}, child: {} } + }); + expect(status.child).toBe('success'); + expect(ran).toBe(true); + }); +}); From 3c1e97a0b264dd697ca354ffc494de4ce0053cc1 Mon Sep 17 00:00:00 2001 From: jycouet Date: Tue, 28 Apr 2026 22:03:53 +0200 Subject: [PATCH 2/3] fix(drizzle): don't cancel if `D1` is selected without `@sveltejs/adapter-cloudflare`, but add info to next steps --- .changeset/fifty-rats-smile.md | 5 +++++ packages/sv/src/addons/drizzle.ts | 10 ++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 .changeset/fifty-rats-smile.md diff --git a/.changeset/fifty-rats-smile.md b/.changeset/fifty-rats-smile.md new file mode 100644 index 000000000..132faaaca --- /dev/null +++ b/.changeset/fifty-rats-smile.md @@ -0,0 +1,5 @@ +--- +'sv': patch +--- + +fix(drizzle): don't cancel if `D1` is selected without `@sveltejs/adapter-cloudflare`, but add info to next steps diff --git a/packages/sv/src/addons/drizzle.ts b/packages/sv/src/addons/drizzle.ts index 6c1adabbb..d942c075b 100644 --- a/packages/sv/src/addons/drizzle.ts +++ b/packages/sv/src/addons/drizzle.ts @@ -101,9 +101,6 @@ export default defineAddon({ file, packageManager }) => { - if (options.database === 'd1' && !dependencyVersion('@sveltejs/adapter-cloudflare')) { - return cancel('Cloudflare D1 requires @sveltejs/adapter-cloudflare - add the adapter first'); - } const [ts] = createPrinter(language === 'ts'); const baseDBPath = path.resolve(cwd, directory.lib, 'server', 'db'); @@ -508,9 +505,14 @@ export default defineAddon({ ); }, - nextSteps: ({ options, packageManager, cwd }) => { + nextSteps: ({ options, packageManager, cwd, dependencyVersion }) => { const steps: string[] = []; if (options.database === 'd1') { + if (!dependencyVersion('@sveltejs/adapter-cloudflare')) { + steps.push( + `Cloudflare D1 requires ${color.addon('@sveltejs/adapter-cloudflare')}. Run ${color.command(resolveCommandArray(packageManager, 'execute', ['sv', 'add', 'sveltekit-adapter=adapter:cloudflare']))} to add it` + ); + } const ext = fileExists(cwd, 'wrangler.toml') ? 'toml' : 'jsonc'; steps.push( `Add your ${color.env('CLOUDFLARE_ACCOUNT_ID')}, ${color.env('CLOUDFLARE_DATABASE_ID')}, and ${color.env('CLOUDFLARE_D1_TOKEN')} to ${color.path('.env')}` From bf4aec6d50ff58f54ad26dec91af186b7f1cdffb Mon Sep 17 00:00:00 2001 From: jycouet Date: Tue, 28 Apr 2026 22:12:07 +0200 Subject: [PATCH 3/3] =?UTF-8?q?fmt=C2=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/sv/src/addons/drizzle.ts | 1 - packages/sv/src/core/engine.ts | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/sv/src/addons/drizzle.ts b/packages/sv/src/addons/drizzle.ts index d942c075b..d594e77be 100644 --- a/packages/sv/src/addons/drizzle.ts +++ b/packages/sv/src/addons/drizzle.ts @@ -101,7 +101,6 @@ export default defineAddon({ file, packageManager }) => { - const [ts] = createPrinter(language === 'ts'); const baseDBPath = path.resolve(cwd, directory.lib, 'server', 'db'); const paths = { diff --git a/packages/sv/src/core/engine.ts b/packages/sv/src/core/engine.ts index c91bc152a..6643a2513 100644 --- a/packages/sv/src/core/engine.ts +++ b/packages/sv/src/core/engine.ts @@ -125,9 +125,7 @@ export async function applyAddons({ const canceledDeps = dependsOn.filter((dep) => canceledAddons.has(dep)); if (canceledDeps.length > 0) { canceledAddons.add(addon.id); - status[addon.id] = canceledDeps.map( - (dep) => `Because dependency '${dep}' was canceled` - ); + status[addon.id] = canceledDeps.map((dep) => `Because dependency '${dep}' was canceled`); continue; }