From 32deaf0471d00e6f6d947c61b442d3c4909ff779 Mon Sep 17 00:00:00 2001 From: CokaKoala <31664583+AdrianGonz97@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:01:11 -0500 Subject: [PATCH] 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 --- .changeset/giant-peaches-run.md | 5 + documentation/docs/20-commands/20-sv-add.md | 1 + packages/addons/_config/official.ts | 2 + packages/addons/_tests/storybook/test.ts | 3 +- .../addons/_tests/sveltekit-adapter/test.ts | 17 +++ packages/addons/sveltekit-adapter/index.ts | 101 ++++++++++++++++++ packages/cli/commands/add/index.ts | 28 ++--- 7 files changed, 143 insertions(+), 14 deletions(-) create mode 100644 .changeset/giant-peaches-run.md create mode 100644 packages/addons/_tests/sveltekit-adapter/test.ts create mode 100644 packages/addons/sveltekit-adapter/index.ts diff --git a/.changeset/giant-peaches-run.md b/.changeset/giant-peaches-run.md new file mode 100644 index 00000000..dda51918 --- /dev/null +++ b/.changeset/giant-peaches-run.md @@ -0,0 +1,5 @@ +--- +'sv': patch +--- + +feat: add `sveltekit-adapter` add-on diff --git a/documentation/docs/20-commands/20-sv-add.md b/documentation/docs/20-commands/20-sv-add.md index 0df56f7a..d05c3d12 100644 --- a/documentation/docs/20-commands/20-sv-add.md +++ b/documentation/docs/20-commands/20-sv-add.md @@ -28,6 +28,7 @@ You can select multiple space-separated add-ons from [the list below](#Official- - `drizzle` - `eslint` +- `sveltekit-adapter` - `lucia` - `mdsvex` - `paraglide` diff --git a/packages/addons/_config/official.ts b/packages/addons/_config/official.ts index 21ac8dc7..3ef21878 100644 --- a/packages/addons/_config/official.ts +++ b/packages/addons/_config/official.ts @@ -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'; @@ -19,6 +20,7 @@ export const officialAddons = [ vitest, playwright, tailwindcss, + sveltekitAdapter, drizzle, lucia, mdsvex, diff --git a/packages/addons/_tests/storybook/test.ts b/packages/addons/_tests/storybook/test.ts index 8a9e0258..5a7e76b8 100644 --- a/packages/addons/_tests/storybook/test.ts +++ b/packages/addons/_tests/storybook/test.ts @@ -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: {} }); diff --git a/packages/addons/_tests/sveltekit-adapter/test.ts b/packages/addons/_tests/sveltekit-adapter/test.ts new file mode 100644 index 00000000..ffdf6a4f --- /dev/null +++ b/packages/addons/_tests/sveltekit-adapter/test.ts @@ -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); +}); diff --git a/packages/addons/sveltekit-adapter/index.ts b/packages/addons/sveltekit-adapter/index.ts new file mode 100644 index 00000000..880c0d2b --- /dev/null +++ b/packages/addons/sveltekit-adapter/index.ts @@ -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(); + }); + } +}); diff --git a/packages/cli/commands/add/index.ts b/packages/cli/commands/add/index.ts index b0c8ef00..32014c35 100644 --- a/packages/cli/commands/add/index.ts +++ b/packages/cli/commands/add/index.ts @@ -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(), @@ -38,10 +44,6 @@ const OptionsSchema = v.strictObject({ }); type Options = v.InferOutput; -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; @@ -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);