From 412866de66b1fff551c65ad091fb0615780cdae4 Mon Sep 17 00:00:00 2001 From: "Willow (GHOST)" Date: Sun, 14 Dec 2025 17:27:37 +0000 Subject: [PATCH 1/6] feat: able to fully setup cloudflare workers/pages --- .changeset/fifty-cobras-wish.md | 5 + .../sv/lib/addons/sveltekit-adapter/index.ts | 149 +++++++++++++++++- packages/sv/lib/core/tooling/index.ts | 9 ++ packages/sv/lib/core/tooling/js/ts-estree.ts | 1 + packages/sv/lib/core/tooling/parsers.ts | 11 ++ packages/sv/package.json | 1 + pnpm-lock.yaml | 9 ++ 7 files changed, 182 insertions(+), 3 deletions(-) create mode 100644 .changeset/fifty-cobras-wish.md diff --git a/.changeset/fifty-cobras-wish.md b/.changeset/fifty-cobras-wish.md new file mode 100644 index 00000000..969e2116 --- /dev/null +++ b/.changeset/fifty-cobras-wish.md @@ -0,0 +1,5 @@ +--- +'sv': minor +--- + +feat: able to fully setup cloudflare workers/pages diff --git a/packages/sv/lib/addons/sveltekit-adapter/index.ts b/packages/sv/lib/addons/sveltekit-adapter/index.ts index 7f82302b..114eeea5 100644 --- a/packages/sv/lib/addons/sveltekit-adapter/index.ts +++ b/packages/sv/lib/addons/sveltekit-adapter/index.ts @@ -1,6 +1,9 @@ import { defineAddon, defineAddonOptions } from '../../core/index.ts'; -import { exports, functions, imports, object } from '../../core/tooling/js/index.ts'; -import { parseJson, parseScript } from '../../core/tooling/parsers.ts'; +import { exports, functions, imports, object, type AstTypes } from '../../core/tooling/js/index.ts'; +import { parseJson, parseScript, parseToml } from '../../core/tooling/parsers.ts'; +import { fileExists, readFile } from '../../cli/add/utils.ts'; +import { resolveCommand } from 'package-manager-detector'; +import * as js from '../../core/tooling/js/index.ts'; const adapters = [ { id: 'auto', package: '@sveltejs/adapter-auto', version: '^7.0.0' }, @@ -18,6 +21,16 @@ const options = defineAddonOptions() default: 'auto', options: adapters.map((p) => ({ value: p.id, label: p.id, hint: p.package })) }) + .add('cfTarget', { + condition: (options) => options.adapter === 'cloudflare', + type: 'select', + question: 'Are you deploying to Workers (assets) or Pages?', + default: 'workers', + options: [ + { value: 'workers', label: 'Workers', hint: 'Recommended way to deploy to Cloudflare' }, + { value: 'pages', label: 'Pages' } + ] + }) .build(); export default defineAddon({ @@ -29,7 +42,7 @@ export default defineAddon({ setup: ({ kit, unsupported }) => { if (!kit) unsupported('Requires SvelteKit'); }, - run: ({ sv, options, files }) => { + run: ({ sv, options, files, cwd, packageManager, typescript }) => { const adapter = adapters.find((a) => a.id === options.adapter)!; // removes previously installed adapters @@ -99,5 +112,135 @@ export default defineAddon({ return generateCode(); }); + + if (adapter.package === '@sveltejs/adapter-cloudflare') { + sv.devDependency('wrangler', 'latest'); + + // default to jsonc + const configFormat = fileExists(cwd, 'wrangler.toml') ? 'toml' : 'jsonc'; + + // Setup Cloudlfare workers/pages config + sv.file(`wrangler.${configFormat}`, (content) => { + const { data, generateCode } = + configFormat === 'jsonc' ? parseJson(content) : parseToml(content); + + if (configFormat === 'jsonc') { + data.$schema ??= './node_modules/wrangler/config-schema.json'; + } + + if (!data.name) { + const pkg = parseJson(readFile(cwd, files.package)); + data.name = pkg.data.name; + } + + data.compatibility_date ??= new Date().toISOString().split('T')[0]; + data.compatibility_flags ??= []; + + if ( + !data.compatibility_flags.includes('nodejs_compat') && + !data.compatibility_flags.includes('nodejs_als') + ) { + data.compatibility_flags.push('nodejs_als'); + } + + switch (options.cfTarget) { + case 'workers': + data.main = '.svelte-kit/cloudflare/_worker.js'; + data.assets ??= {}; + data.assets.binding = 'ASSETS'; + data.assets.directory = '.svelte-kit/cloudflare'; + data.workers_dev = true; + data.preview_urls = true; + break; + + case 'pages': + data.pages_build_output_dir = '.svelte-kit/cloudflare'; + break; + } + + return generateCode(); + }); + + const jsconfig = fileExists(cwd, 'jsconfig.json'); + const typeChecked = typescript || jsconfig; + + if (typeChecked) { + // Ignore generated Cloudflare Types + sv.file(files.gitignore, (content) => { + return content.includes('.wrangler') && content.includes('worker-configuration.d.ts') + ? content + : `${content.trimEnd()}\n\n# Cloudflare Types\n/worker-configuration.d.ts`; + }); + + // Setup wrangler types command + sv.file(files.package, (content) => { + const { data, generateCode } = parseJson(content); + + data.scripts ??= {}; + data.scripts.types = 'wrangler types'; + const { command, args } = resolveCommand(packageManager, 'run', ['types'])!; + data.scripts.prepare = data.scripts.prepare + ? `${command} ${args.join(' ')} && ${data.scripts.prepare}` + : `${command} ${args.join(' ')}`; + + return generateCode(); + }); + + // Add Cloudflare generated types to tsconfig + sv.file(`${jsconfig ? 'jsconfig' : 'tsconfig'}.json`, (content) => { + const { data, generateCode } = parseJson(content); + + data.compilerOptions ??= {}; + data.compilerOptions.types ??= []; + data.compilerOptions.types.push('worker-configuration.d.ts'); + + return generateCode(); + }); + + sv.file('src/app.d.ts', (content) => { + const { ast, generateCode } = parseScript(content); + + const platform = js.kit.addGlobalAppInterface(ast, { name: 'Platform' }); + if (!platform) { + throw new Error('Failed detecting `platform` interface in `src/app.d.ts`'); + } + + platform.body.body.push( + createCloudflarePlatformType('env', 'Env'), + createCloudflarePlatformType('ctx', 'ExecutionContext'), + createCloudflarePlatformType('caches', 'CacheStorage'), + createCloudflarePlatformType('cf', 'IncomingRequestCfProperties', true) + ); + + return generateCode(); + }); + } + } } }); + +function createCloudflarePlatformType( + name: string, + value: string, + optional = false +): AstTypes.TSInterfaceBody['body'][number] { + return { + type: 'TSPropertySignature', + key: { + type: 'Identifier', + name + }, + computed: false, + optional, + typeAnnotation: { + type: 'TSTypeAnnotation', + typeAnnotation: { + type: 'TSTypeReference', + typeName: { + type: 'Identifier', + name: value + } + } + } + }; +} diff --git a/packages/sv/lib/core/tooling/index.ts b/packages/sv/lib/core/tooling/index.ts index 5ef8d559..d42f84e1 100644 --- a/packages/sv/lib/core/tooling/index.ts +++ b/packages/sv/lib/core/tooling/index.ts @@ -8,6 +8,7 @@ import { tsPlugin } from '@sveltejs/acorn-typescript'; import { parse as svelteParse, type AST as SvelteAst, print as sveltePrint } from 'svelte/compiler'; import * as yaml from 'yaml'; import type { BaseNode } from 'estree'; +import * as toml from 'smol-toml'; export { // ast walker @@ -288,3 +289,11 @@ export function parseSvelte(content: string): SvelteAst.Root { export function serializeSvelte(ast: SvelteAst.SvelteNode): string { return sveltePrint(ast).code; } + +export function parseToml(content: string): toml.TomlTable { + return toml.parse(content); +} + +export function serializeToml(data: toml.TomlTable): string { + return toml.stringify(data); +} diff --git a/packages/sv/lib/core/tooling/js/ts-estree.ts b/packages/sv/lib/core/tooling/js/ts-estree.ts index 49ffba8a..45d0473a 100644 --- a/packages/sv/lib/core/tooling/js/ts-estree.ts +++ b/packages/sv/lib/core/tooling/js/ts-estree.ts @@ -46,6 +46,7 @@ declare module 'estree' { type: 'TSPropertySignature'; computed: boolean; key: Identifier; + optional?: boolean; typeAnnotation: TSTypeAnnotation; } interface TSProgram extends Omit { diff --git a/packages/sv/lib/core/tooling/parsers.ts b/packages/sv/lib/core/tooling/parsers.ts index 4b763f8a..2dc8c597 100644 --- a/packages/sv/lib/core/tooling/parsers.ts +++ b/packages/sv/lib/core/tooling/parsers.ts @@ -1,3 +1,4 @@ +import type { TomlTable } from 'smol-toml'; import * as utils from './index.ts'; type ParseBase = { @@ -57,3 +58,13 @@ export function parseSvelte(source: string): { ast: utils.SvelteAst.Root } & Par generateCode }; } + +export function parseToml(source: string): { data: TomlTable } & ParseBase { + const data = utils.parseToml(source); + + return { + data, + source, + generateCode: () => utils.serializeToml(data) + }; +} diff --git a/packages/sv/package.json b/packages/sv/package.json index 49c96ac7..b8db4aa3 100644 --- a/packages/sv/package.json +++ b/packages/sv/package.json @@ -53,6 +53,7 @@ "picocolors": "^1.1.1", "ps-tree": "^1.2.0", "silver-fleece": "^1.2.1", + "smol-toml": "^1.5.2", "sucrase": "^3.35.0", "svelte": "^5.45.9", "tiny-glob": "^0.2.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8fc17f7f..8f425560 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -184,6 +184,9 @@ importers: silver-fleece: specifier: ^1.2.1 version: 1.2.1 + smol-toml: + specifier: ^1.5.2 + version: 1.5.2 sucrase: specifier: ^3.35.0 version: 3.35.1 @@ -1877,6 +1880,10 @@ packages: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} + smol-toml@1.5.2: + resolution: {integrity: sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ==} + engines: {node: '>= 18'} + sort-object-keys@1.1.3: resolution: {integrity: sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==} @@ -3800,6 +3807,8 @@ snapshots: slash@3.0.0: {} + smol-toml@1.5.2: {} + sort-object-keys@1.1.3: {} sort-package-json@3.4.0: From 242dfb01a69782d8f61b6b1f4d4bd7c74cd9b951 Mon Sep 17 00:00:00 2001 From: jycouet Date: Fri, 19 Dec 2025 16:22:15 +0100 Subject: [PATCH 2/6] update changeset --- .changeset/fifty-cobras-wish.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fifty-cobras-wish.md b/.changeset/fifty-cobras-wish.md index 969e2116..0c98fc94 100644 --- a/.changeset/fifty-cobras-wish.md +++ b/.changeset/fifty-cobras-wish.md @@ -2,4 +2,4 @@ 'sv': minor --- -feat: able to fully setup cloudflare workers/pages +feat(cloudflare): able to fully setup cloudflare adapter for workers/pages From f983eea5e2b4af98c176117d4198f88962252e28 Mon Sep 17 00:00:00 2001 From: jycouet Date: Fri, 19 Dec 2025 16:25:02 +0100 Subject: [PATCH 3/6] update doc --- documentation/docs/30-add-ons/45-sveltekit-adapter.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/documentation/docs/30-add-ons/45-sveltekit-adapter.md b/documentation/docs/30-add-ons/45-sveltekit-adapter.md index 2df35754..cf864fbf 100644 --- a/documentation/docs/30-add-ons/45-sveltekit-adapter.md +++ b/documentation/docs/30-add-ons/45-sveltekit-adapter.md @@ -30,3 +30,11 @@ Which SvelteKit adapter to use: ```sh npx sv add sveltekit-adapter="adapter:node" ``` + +### cloudflare target + +Whether to deploy to Cloudflare Workers or Pages. Only available for `cloudflare` adapter. + +```sh +npx sv add sveltekit-adapter="adapter:cloudflare+cfTarget:workers" +``` From 367f6eb50bfc87b92cd663c1d637c2ca4927687e Mon Sep 17 00:00:00 2001 From: jycouet Date: Fri, 19 Dec 2025 16:33:02 +0100 Subject: [PATCH 4/6] fix wrangler version (will be updated with pnpm update-deps, like any other deps) --- packages/sv/lib/addons/sveltekit-adapter/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sv/lib/addons/sveltekit-adapter/index.ts b/packages/sv/lib/addons/sveltekit-adapter/index.ts index 114eeea5..aa9a8754 100644 --- a/packages/sv/lib/addons/sveltekit-adapter/index.ts +++ b/packages/sv/lib/addons/sveltekit-adapter/index.ts @@ -114,7 +114,7 @@ export default defineAddon({ }); if (adapter.package === '@sveltejs/adapter-cloudflare') { - sv.devDependency('wrangler', 'latest'); + sv.devDependency('wrangler', '^4.56.0'); // default to jsonc const configFormat = fileExists(cwd, 'wrangler.toml') ? 'toml' : 'jsonc'; From bbeb7fa988597b51aa5573c10ec90d793414b9cd Mon Sep 17 00:00:00 2001 From: jycouet Date: Fri, 19 Dec 2025 17:51:31 +0100 Subject: [PATCH 5/6] tweak package.json --- packages/sv/lib/addons/sveltekit-adapter/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/sv/lib/addons/sveltekit-adapter/index.ts b/packages/sv/lib/addons/sveltekit-adapter/index.ts index aa9a8754..b5630edc 100644 --- a/packages/sv/lib/addons/sveltekit-adapter/index.ts +++ b/packages/sv/lib/addons/sveltekit-adapter/index.ts @@ -56,6 +56,15 @@ export default defineAddon({ } } + // in sk 3, we will keep "preview": "vite preview" like any other adapter + if (options.adapter === 'cloudflare') { + if (options.cfTarget === 'workers') { + data.scripts.preview = 'wrangler dev .svelte-kit/cloudflare/_worker.js'; + } else if (options.cfTarget === 'pages') { + data.scripts.preview = 'wrangler pages dev .svelte-kit/cloudflare'; + } + } + return generateCode(); }); From 7b3271bff61faac7ee2b4d2d3077eaf31ccd09a5 Mon Sep 17 00:00:00 2001 From: jycouet Date: Fri, 19 Dec 2025 18:02:57 +0100 Subject: [PATCH 6/6] adding a test with cloudflare --- .../addons/_tests/sveltekit-adapter/test.ts | 52 +++++++++++-------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/packages/sv/lib/addons/_tests/sveltekit-adapter/test.ts b/packages/sv/lib/addons/_tests/sveltekit-adapter/test.ts index b11a8fd7..f5ea1e45 100644 --- a/packages/sv/lib/addons/_tests/sveltekit-adapter/test.ts +++ b/packages/sv/lib/addons/_tests/sveltekit-adapter/test.ts @@ -5,36 +5,42 @@ import sveltekitAdapter from '../../sveltekit-adapter/index.ts'; import { setupTest } from '../_setup/suite.ts'; const addonId = sveltekitAdapter.id; -const { test, testCases, prepareServer } = setupTest( +const { test, testCases } = setupTest( { [addonId]: sveltekitAdapter }, { kinds: [ { type: 'node', options: { [addonId]: { adapter: 'node' } } }, - { type: 'auto', options: { [addonId]: { adapter: 'auto' } } } + { type: 'auto', options: { [addonId]: { adapter: 'auto' } } }, + { + type: 'cloudflare-workers', + options: { [addonId]: { adapter: 'cloudflare', cfTarget: 'workers' } } + }, + { + type: 'cloudflare-pages', + options: { [addonId]: { adapter: 'cloudflare', cfTarget: 'pages' } } + } ], - filter: (addonTestCase) => addonTestCase.variant.includes('kit') + filter: (addonTestCase) => addonTestCase.variant.includes('kit'), + browser: false } ); -test.concurrent.for(testCases)( - 'adapter $kind.type $variant', - async (testCase, { page, ...ctx }) => { - const cwd = ctx.cwd(testCase); +test.concurrent.for(testCases)('adapter $kind.type $variant', async (testCase, { ...ctx }) => { + const cwd = ctx.cwd(testCase); - const { close } = await prepareServer({ cwd, page }); - // kill server process when we're done - ctx.onTestFinished(async () => await close()); - - if (testCase.kind.type === 'node') { - expect(await readFile(join(cwd, 'svelte.config.js'), 'utf8')).not.toMatch('adapter-auto'); - expect(await readFile(join(cwd, 'svelte.config.js'), 'utf8')).not.toMatch( - 'adapter-auto only supports some environments' - ); - } else if (testCase.kind.type === 'auto') { - expect(await readFile(join(cwd, 'svelte.config.js'), 'utf8')).toMatch('adapter-auto'); - expect(await readFile(join(cwd, 'svelte.config.js'), 'utf8')).toMatch( - 'adapter-auto only supports some environments' - ); - } + if (testCase.kind.type === 'node') { + expect(await readFile(join(cwd, 'svelte.config.js'), 'utf8')).not.toMatch('adapter-auto'); + expect(await readFile(join(cwd, 'svelte.config.js'), 'utf8')).not.toMatch( + 'adapter-auto only supports some environments' + ); + } else if (testCase.kind.type === 'auto') { + expect(await readFile(join(cwd, 'svelte.config.js'), 'utf8')).toMatch('adapter-auto'); + expect(await readFile(join(cwd, 'svelte.config.js'), 'utf8')).toMatch( + 'adapter-auto only supports some environments' + ); + } else if (testCase.kind.type === 'cloudflare-workers') { + expect(await readFile(join(cwd, 'wrangler.jsonc'), 'utf8')).toMatch('ASSETS'); + } else if (testCase.kind.type === 'cloudflare-pages') { + expect(await readFile(join(cwd, 'wrangler.jsonc'), 'utf8')).toMatch('pages_build_output_dir'); } -); +});