Skip to content

Commit

Permalink
feat: adds sveltekit-adapter add-on (#346)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
AdrianGonz97 and manuel3108 authored Dec 16, 2024
1 parent cf2d2bc commit 32deaf0
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 14 deletions.
5 changes: 5 additions & 0 deletions .changeset/giant-peaches-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'sv': patch
---

feat: add `sveltekit-adapter` add-on
1 change: 1 addition & 0 deletions documentation/docs/20-commands/20-sv-add.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ You can select multiple space-separated add-ons from [the list below](#Official-

- `drizzle`
- `eslint`
- `sveltekit-adapter`
- `lucia`
- `mdsvex`
- `paraglide`
Expand Down
2 changes: 2 additions & 0 deletions packages/addons/_config/official.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { AddonWithoutExplicitArgs } from '@sveltejs/cli-core';

import drizzle from '../drizzle/index.ts';
import eslint from '../eslint/index.ts';
import sveltekitAdapter from '../sveltekit-adapter/index.ts';
import lucia from '../lucia/index.ts';
import mdsvex from '../mdsvex/index.ts';
import paraglide from '../paraglide/index.ts';
Expand All @@ -19,6 +20,7 @@ export const officialAddons = [
vitest,
playwright,
tailwindcss,
sveltekitAdapter,
drizzle,
lucia,
mdsvex,
Expand Down
3 changes: 1 addition & 2 deletions packages/addons/_tests/storybook/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ const { test, variants, prepareServer } = setupTest({ storybook });

let port = 6006;

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

Expand Down
17 changes: 17 additions & 0 deletions packages/addons/_tests/sveltekit-adapter/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { expect } from '@playwright/test';
import { setupTest } from '../_setup/suite.ts';
import sveltekitAdapter from '../../sveltekit-adapter/index.ts';

const addonId = sveltekitAdapter.id;
const { test, variants, prepareServer } = setupTest({ [addonId]: sveltekitAdapter });

const kitOnly = variants.filter((v) => v.includes('kit'));
test.concurrent.for(kitOnly)('core - %s', async (variant, { page, ...ctx }) => {
const cwd = await ctx.run(variant, { [addonId]: { adapter: 'node' } });

const { close } = await prepareServer({ cwd, page });
// kill server process when we're done
ctx.onTestFinished(async () => await close());

expect(true).toBe(true);
});
101 changes: 101 additions & 0 deletions packages/addons/sveltekit-adapter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { defineAddon, defineAddonOptions } from '@sveltejs/cli-core';
import { exports, functions, imports, object, type AstTypes } from '@sveltejs/cli-core/js';
import { parseJson, parseScript } from '@sveltejs/cli-core/parsers';

type Adapter = {
id: string;
package: string;
version: string;
};

const adapters: Adapter[] = [
{ id: 'node', package: '@sveltejs/adapter-node', version: '^5.2.9' },
{ id: 'static', package: '@sveltejs/adapter-static', version: '^3.0.6' },
{ id: 'vercel', package: '@sveltejs/adapter-vercel', version: '^5.5.0' },
{ id: 'cloudflare-pages', package: '@sveltejs/adapter-cloudflare', version: '^4.8.0' },
{ id: 'cloudflare-workers', package: '@sveltejs/adapter-cloudflare-workers', version: '^2.6.0' },
{ id: 'netlify', package: '@sveltejs/adapter-netlify', version: '^4.4.0' }
];

const options = defineAddonOptions({
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: 'node'
}
});

export default defineAddon({
id: 'sveltekit-adapter',
alias: 'adapter',
shortDescription: 'deployment',
homepage: 'https://svelte.dev/docs/kit/adapters',
options,
setup: ({ kit, unsupported }) => {
if (!kit) unsupported('Requires SvelteKit');
},
run: ({ sv, options }) => {
const adapter = adapters.find((a) => a.id === options.adapter)!;

// removes previously installed adapters
sv.file('package.json', (content) => {
const { data, generateCode } = parseJson(content);
const devDeps = data['devDependencies'];

for (const pkg of Object.keys(devDeps)) {
if (pkg.startsWith('@sveltejs/adapter-')) {
delete devDeps[pkg];
}
}

return generateCode();
});

sv.devDependency(adapter.package, adapter.version);

sv.file('svelte.config.js', (content) => {
const { ast, generateCode } = parseScript(content);

// finds any existing adapter's import declaration
const importDecls = ast.body.filter((n) => n.type === 'ImportDeclaration');
const adapterImportDecl = importDecls.find(
(importDecl) =>
typeof importDecl.source.value === 'string' &&
importDecl.source.value.startsWith('@sveltejs/adapter-') &&
importDecl.importKind === 'value'
);

let adapterName = 'adapter';
if (adapterImportDecl) {
// replaces the import's source with the new adapter
adapterImportDecl.source.value = adapter.package;
adapterName = adapterImportDecl.specifiers?.find((s) => s.type === 'ImportDefaultSpecifier')
?.local?.name as string;
} else {
imports.addDefault(ast, adapter.package, adapterName);
}

const { value: config } = exports.defaultExport(ast, object.createEmpty());
const kitConfig = config.properties.find(
(p) => p.type === 'ObjectProperty' && p.key.type === 'Identifier' && p.key.name === 'kit'
) as AstTypes.ObjectProperty | undefined;

if (kitConfig && kitConfig.value.type === 'ObjectExpression') {
// only overrides the `adapter` property so we can reset it's args
object.overrideProperties(kitConfig.value, {
adapter: functions.callByIdentifier(adapterName, [])
});
} else {
// creates the `kit` property when absent
object.properties(config, {
kit: object.create({
adapter: functions.callByIdentifier(adapterName, [])
})
});
}

return generateCode();
});
}
});
28 changes: 16 additions & 12 deletions packages/cli/commands/add/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,19 @@ import { installDependencies, packageManagerPrompt } from '../../utils/package-m
import { getGlobalPreconditions } from './preconditions.ts';
import { type AddonMap, applyAddons, setupAddons } from '../../lib/install.ts';

const aliases = officialAddons.map((c) => c.alias).filter((v) => v !== undefined);
const addonsOptions = getAddonOptionFlags();
const communityDetails: AddonWithoutExplicitArgs[] = [];

const OptionFlagSchema = v.optional(v.array(v.string()));

const addonOptionFlags = addonsOptions.reduce(
(flags, opt) => Object.assign(flags, { [opt.attributeName()]: OptionFlagSchema }),
{}
);

const AddonsSchema = v.array(v.string());
const AddonOptionFlagsSchema = v.object({
tailwindcss: v.optional(v.array(v.string())),
drizzle: v.optional(v.array(v.string())),
lucia: v.optional(v.array(v.string())),
paraglide: v.optional(v.array(v.string()))
});
const AddonOptionFlagsSchema = v.object(addonOptionFlags);
const OptionsSchema = v.strictObject({
cwd: v.string(),
install: v.boolean(),
Expand All @@ -38,10 +44,6 @@ const OptionsSchema = v.strictObject({
});
type Options = v.InferOutput<typeof OptionsSchema>;

const aliases = officialAddons.map((c) => c.alias).filter((v) => v !== undefined);
const addonsOptions = getAddonOptionFlags();
const communityDetails: AddonWithoutExplicitArgs[] = [];

// infers the workspace cwd if a `package.json` resides in a parent directory
const defaultPkgPath = pkg.up();
const defaultCwd = defaultPkgPath ? path.dirname(defaultPkgPath) : undefined;
Expand Down Expand Up @@ -111,8 +113,10 @@ export async function runAddCommand(

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

const details = getAddonDetails(addonId);
Expand Down

0 comments on commit 32deaf0

Please sign in to comment.