Skip to content

Commit 32deaf0

Browse files
feat: adds sveltekit-adapter add-on (#346)
* implement `sveltekit-adapter` add-on * short description * add alias * fiy typo * add to docs * fix add-on flags * tweak * no concurrent storybook test in CI * test --------- Co-authored-by: Manuel Serret <[email protected]>
1 parent cf2d2bc commit 32deaf0

File tree

7 files changed

+143
-14
lines changed

7 files changed

+143
-14
lines changed

.changeset/giant-peaches-run.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+
feat: add `sveltekit-adapter` add-on

documentation/docs/20-commands/20-sv-add.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ You can select multiple space-separated add-ons from [the list below](#Official-
2828

2929
- `drizzle`
3030
- `eslint`
31+
- `sveltekit-adapter`
3132
- `lucia`
3233
- `mdsvex`
3334
- `paraglide`

packages/addons/_config/official.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { AddonWithoutExplicitArgs } from '@sveltejs/cli-core';
22

33
import drizzle from '../drizzle/index.ts';
44
import eslint from '../eslint/index.ts';
5+
import sveltekitAdapter from '../sveltekit-adapter/index.ts';
56
import lucia from '../lucia/index.ts';
67
import mdsvex from '../mdsvex/index.ts';
78
import paraglide from '../paraglide/index.ts';
@@ -19,6 +20,7 @@ export const officialAddons = [
1920
vitest,
2021
playwright,
2122
tailwindcss,
23+
sveltekitAdapter,
2224
drizzle,
2325
lucia,
2426
mdsvex,

packages/addons/_tests/storybook/test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@ const { test, variants, prepareServer } = setupTest({ storybook });
77

88
let port = 6006;
99

10-
const windowsCI = process.env.CI && process.platform === 'win32';
1110
test.for(variants)(
1211
'storybook loaded - %s',
13-
{ concurrent: !windowsCI },
12+
{ concurrent: !process.env.CI },
1413
async (variant, { page, ...ctx }) => {
1514
const cwd = await ctx.run(variant, { storybook: {} });
1615

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { expect } from '@playwright/test';
2+
import { setupTest } from '../_setup/suite.ts';
3+
import sveltekitAdapter from '../../sveltekit-adapter/index.ts';
4+
5+
const addonId = sveltekitAdapter.id;
6+
const { test, variants, prepareServer } = setupTest({ [addonId]: sveltekitAdapter });
7+
8+
const kitOnly = variants.filter((v) => v.includes('kit'));
9+
test.concurrent.for(kitOnly)('core - %s', async (variant, { page, ...ctx }) => {
10+
const cwd = await ctx.run(variant, { [addonId]: { adapter: 'node' } });
11+
12+
const { close } = await prepareServer({ cwd, page });
13+
// kill server process when we're done
14+
ctx.onTestFinished(async () => await close());
15+
16+
expect(true).toBe(true);
17+
});
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { defineAddon, defineAddonOptions } from '@sveltejs/cli-core';
2+
import { exports, functions, imports, object, type AstTypes } from '@sveltejs/cli-core/js';
3+
import { parseJson, parseScript } from '@sveltejs/cli-core/parsers';
4+
5+
type Adapter = {
6+
id: string;
7+
package: string;
8+
version: string;
9+
};
10+
11+
const adapters: Adapter[] = [
12+
{ id: 'node', package: '@sveltejs/adapter-node', version: '^5.2.9' },
13+
{ id: 'static', package: '@sveltejs/adapter-static', version: '^3.0.6' },
14+
{ id: 'vercel', package: '@sveltejs/adapter-vercel', version: '^5.5.0' },
15+
{ id: 'cloudflare-pages', package: '@sveltejs/adapter-cloudflare', version: '^4.8.0' },
16+
{ id: 'cloudflare-workers', package: '@sveltejs/adapter-cloudflare-workers', version: '^2.6.0' },
17+
{ id: 'netlify', package: '@sveltejs/adapter-netlify', version: '^4.4.0' }
18+
];
19+
20+
const options = defineAddonOptions({
21+
adapter: {
22+
type: 'select',
23+
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: 'node'
26+
}
27+
});
28+
29+
export default defineAddon({
30+
id: 'sveltekit-adapter',
31+
alias: 'adapter',
32+
shortDescription: 'deployment',
33+
homepage: 'https://svelte.dev/docs/kit/adapters',
34+
options,
35+
setup: ({ kit, unsupported }) => {
36+
if (!kit) unsupported('Requires SvelteKit');
37+
},
38+
run: ({ sv, options }) => {
39+
const adapter = adapters.find((a) => a.id === options.adapter)!;
40+
41+
// removes previously installed adapters
42+
sv.file('package.json', (content) => {
43+
const { data, generateCode } = parseJson(content);
44+
const devDeps = data['devDependencies'];
45+
46+
for (const pkg of Object.keys(devDeps)) {
47+
if (pkg.startsWith('@sveltejs/adapter-')) {
48+
delete devDeps[pkg];
49+
}
50+
}
51+
52+
return generateCode();
53+
});
54+
55+
sv.devDependency(adapter.package, adapter.version);
56+
57+
sv.file('svelte.config.js', (content) => {
58+
const { ast, generateCode } = parseScript(content);
59+
60+
// finds any existing adapter's import declaration
61+
const importDecls = ast.body.filter((n) => n.type === 'ImportDeclaration');
62+
const adapterImportDecl = importDecls.find(
63+
(importDecl) =>
64+
typeof importDecl.source.value === 'string' &&
65+
importDecl.source.value.startsWith('@sveltejs/adapter-') &&
66+
importDecl.importKind === 'value'
67+
);
68+
69+
let adapterName = 'adapter';
70+
if (adapterImportDecl) {
71+
// replaces the import's source with the new adapter
72+
adapterImportDecl.source.value = adapter.package;
73+
adapterName = adapterImportDecl.specifiers?.find((s) => s.type === 'ImportDefaultSpecifier')
74+
?.local?.name as string;
75+
} else {
76+
imports.addDefault(ast, adapter.package, adapterName);
77+
}
78+
79+
const { value: config } = exports.defaultExport(ast, object.createEmpty());
80+
const kitConfig = config.properties.find(
81+
(p) => p.type === 'ObjectProperty' && p.key.type === 'Identifier' && p.key.name === 'kit'
82+
) as AstTypes.ObjectProperty | undefined;
83+
84+
if (kitConfig && kitConfig.value.type === 'ObjectExpression') {
85+
// only overrides the `adapter` property so we can reset it's args
86+
object.overrideProperties(kitConfig.value, {
87+
adapter: functions.callByIdentifier(adapterName, [])
88+
});
89+
} else {
90+
// creates the `kit` property when absent
91+
object.properties(config, {
92+
kit: object.create({
93+
adapter: functions.callByIdentifier(adapterName, [])
94+
})
95+
});
96+
}
97+
98+
return generateCode();
99+
});
100+
}
101+
});

packages/cli/commands/add/index.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,19 @@ import { installDependencies, packageManagerPrompt } from '../../utils/package-m
2222
import { getGlobalPreconditions } from './preconditions.ts';
2323
import { type AddonMap, applyAddons, setupAddons } from '../../lib/install.ts';
2424

25+
const aliases = officialAddons.map((c) => c.alias).filter((v) => v !== undefined);
26+
const addonsOptions = getAddonOptionFlags();
27+
const communityDetails: AddonWithoutExplicitArgs[] = [];
28+
29+
const OptionFlagSchema = v.optional(v.array(v.string()));
30+
31+
const addonOptionFlags = addonsOptions.reduce(
32+
(flags, opt) => Object.assign(flags, { [opt.attributeName()]: OptionFlagSchema }),
33+
{}
34+
);
35+
2536
const AddonsSchema = v.array(v.string());
26-
const AddonOptionFlagsSchema = v.object({
27-
tailwindcss: v.optional(v.array(v.string())),
28-
drizzle: v.optional(v.array(v.string())),
29-
lucia: v.optional(v.array(v.string())),
30-
paraglide: v.optional(v.array(v.string()))
31-
});
37+
const AddonOptionFlagsSchema = v.object(addonOptionFlags);
3238
const OptionsSchema = v.strictObject({
3339
cwd: v.string(),
3440
install: v.boolean(),
@@ -38,10 +44,6 @@ const OptionsSchema = v.strictObject({
3844
});
3945
type Options = v.InferOutput<typeof OptionsSchema>;
4046

41-
const aliases = officialAddons.map((c) => c.alias).filter((v) => v !== undefined);
42-
const addonsOptions = getAddonOptionFlags();
43-
const communityDetails: AddonWithoutExplicitArgs[] = [];
44-
4547
// infers the workspace cwd if a `package.json` resides in a parent directory
4648
const defaultPkgPath = pkg.up();
4749
const defaultCwd = defaultPkgPath ? path.dirname(defaultPkgPath) : undefined;
@@ -111,8 +113,10 @@ export async function runAddCommand(
111113

112114
// apply specified options from flags
113115
for (const addonOption of addonsOptions) {
114-
const addonId = addonOption.attributeName() as keyof Options;
115-
const specifiedOptions = options[addonId] as string[] | undefined;
116+
const addonId = addonOption.name() as keyof Options;
117+
// if the add-on flag contains a `-`, it'll be camelcased (e.g. `sveltekit-adapter` is `sveltekitAdapter`)
118+
const aliased = addonOption.attributeName() as keyof Options;
119+
const specifiedOptions = (options[addonId] || options[aliased]) as string[] | undefined;
116120
if (!specifiedOptions) continue;
117121

118122
const details = getAddonDetails(addonId);

0 commit comments

Comments
 (0)