Skip to content

Commit 7bf9e53

Browse files
authored
fix drizzle & D1 & !adapter-cloudflare (#1071)
1 parent a5f7780 commit 7bf9e53

6 files changed

Lines changed: 101 additions & 7 deletions

File tree

.changeset/fifty-rats-smile.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(drizzle): don't cancel if `D1` is selected without `@sveltejs/adapter-cloudflare`, but add info to next steps
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(sv): skip add-ons when a `dependsOn` dependency cancels

packages/sv/src/addons/drizzle.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,6 @@ export default defineAddon({
101101
file,
102102
packageManager
103103
}) => {
104-
if (options.database === 'd1' && !dependencyVersion('@sveltejs/adapter-cloudflare')) {
105-
return cancel('Cloudflare D1 requires @sveltejs/adapter-cloudflare - add the adapter first');
106-
}
107-
108104
const [ts] = createPrinter(language === 'ts');
109105
const baseDBPath = path.resolve(cwd, directory.lib, 'server', 'db');
110106
const paths = {
@@ -508,9 +504,14 @@ export default defineAddon({
508504
);
509505
},
510506

511-
nextSteps: ({ options, packageManager, cwd }) => {
507+
nextSteps: ({ options, packageManager, cwd, dependencyVersion }) => {
512508
const steps: string[] = [];
513509
if (options.database === 'd1') {
510+
if (!dependencyVersion('@sveltejs/adapter-cloudflare')) {
511+
steps.push(
512+
`Cloudflare D1 requires ${color.addon('@sveltejs/adapter-cloudflare')}. Run ${color.command(resolveCommandArray(packageManager, 'execute', ['sv', 'add', 'sveltekit-adapter=adapter:cloudflare']))} to add it`
513+
);
514+
}
514515
const ext = fileExists(cwd, 'wrangler.toml') ? 'toml' : 'jsonc';
515516
steps.push(
516517
`Add your ${color.env('CLOUDFLARE_ACCOUNT_ID')}, ${color.env('CLOUDFLARE_DATABASE_ID')}, and ${color.env('CLOUDFLARE_D1_TOKEN')} to ${color.path('.env')}`

packages/sv/src/cli/add.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -697,8 +697,14 @@ export async function runAddonsApply({
697697
const successfulAddons = loadedAddons.filter((a) => !canceledAddonIds.includes(a.addon.id));
698698

699699
if (addonSuccess.length === 0) {
700-
p.cancel('All selected add-ons were canceled.');
701-
process.exit(1);
700+
// `create` already scaffolded the project on disk - exiting here would hide
701+
// the "Project created" success and the next-steps. Just warn instead.
702+
if (fromCommand === 'create') {
703+
p.log.warn('All selected add-ons were canceled.');
704+
} else {
705+
p.cancel('All selected add-ons were canceled.');
706+
process.exit(1);
707+
}
702708
} else {
703709
p.log.success(
704710
`Successfully setup add-ons: ${addonSuccess.map((c) => color.addon(c)).join(', ')}`

packages/sv/src/core/engine.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,24 @@ export async function applyAddons({
111111
}> {
112112
const filesToFormat = new Set<string>();
113113
const status: Record<string, string[] | 'success'> = {};
114+
const canceledAddons = new Set<string>();
114115

115116
const addonDefs = loadedAddons.map((l) => l.addon);
116117
const ordered = orderAddons(addonDefs, setupResults);
117118

118119
let hasFormatter = false;
119120

120121
for (const addon of ordered) {
122+
// Skip addons whose `dependsOn` dependency was canceled. Running them would
123+
// fail with misleading errors since they expect state from the canceled addon.
124+
const dependsOn = setupResults[addon.id]?.dependsOn ?? [];
125+
const canceledDeps = dependsOn.filter((dep) => canceledAddons.has(dep));
126+
if (canceledDeps.length > 0) {
127+
canceledAddons.add(addon.id);
128+
status[addon.id] = canceledDeps.map((dep) => `Because dependency '${dep}' was canceled`);
129+
continue;
130+
}
131+
121132
const loaded = loadedAddons.find((l) => l.addon.id === addon.id)!;
122133
const workspaceOptions = options[addon.id] || {};
123134

@@ -141,6 +152,7 @@ export async function applyAddons({
141152
if (cancels.length === 0) {
142153
status[addon.id] = 'success';
143154
} else {
155+
canceledAddons.add(addon.id);
144156
status[addon.id] = cancels;
145157
}
146158
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import fs from 'node:fs';
2+
import os from 'node:os';
3+
import path from 'node:path';
4+
import { describe, expect, it } from 'vitest';
5+
import { defineAddon, defineAddonOptions, type LoadedAddon } from '../config.ts';
6+
import { applyAddons, setupAddons } from '../engine.ts';
7+
import { createWorkspace } from '../workspace.ts';
8+
9+
function makeWorkspace() {
10+
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sv-engine-'));
11+
fs.writeFileSync(path.join(cwd, 'package.json'), '{"name":"t","private":true}');
12+
return cwd;
13+
}
14+
const loaded = (a: ReturnType<typeof defineAddon<any, any>>): LoadedAddon => ({
15+
reference: { specifier: a.id, options: [], source: { kind: 'official', id: a.id } },
16+
addon: a
17+
});
18+
const dep = defineAddon({
19+
id: 'dep',
20+
options: defineAddonOptions().build(),
21+
run: ({ cancel }) => cancel('nope')
22+
});
23+
24+
describe('applyAddons cancel propagation', () => {
25+
it("skips addons whose 'dependsOn' was canceled", async () => {
26+
const child = defineAddon({
27+
id: 'child',
28+
options: defineAddonOptions().build(),
29+
setup: ({ dependsOn }) => dependsOn('dep' as never),
30+
run: () => expect.fail('child should not have run')
31+
});
32+
const workspace = await createWorkspace({ cwd: makeWorkspace() });
33+
const addons = [loaded(dep), loaded(child)];
34+
const { status } = await applyAddons({
35+
loadedAddons: addons,
36+
workspace,
37+
setupResults: setupAddons(addons, workspace),
38+
options: { dep: {}, child: {} }
39+
});
40+
expect(status.dep).toEqual(['nope']);
41+
expect(status.child).toEqual(["Because dependency 'dep' was canceled"]);
42+
});
43+
44+
it("does not skip addons that only 'runsAfter' a canceled addon", async () => {
45+
let ran = false;
46+
const child = defineAddon({
47+
id: 'child',
48+
options: defineAddonOptions().build(),
49+
setup: ({ runsAfter }) => runsAfter('dep' as never),
50+
run: () => {
51+
ran = true;
52+
}
53+
});
54+
const workspace = await createWorkspace({ cwd: makeWorkspace() });
55+
const addons = [loaded(dep), loaded(child)];
56+
const { status } = await applyAddons({
57+
loadedAddons: addons,
58+
workspace,
59+
setupResults: setupAddons(addons, workspace),
60+
options: { dep: {}, child: {} }
61+
});
62+
expect(status.child).toBe('success');
63+
expect(ran).toBe(true);
64+
});
65+
});

0 commit comments

Comments
 (0)