Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sveltekit-public-config-loader.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@workflow/sveltekit': patch
---

Load SvelteKit route configuration through `@sveltejs/load-config`, including projects that configure SvelteKit exclusively in `vite.config`.
5 changes: 4 additions & 1 deletion packages/sveltekit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "vitest run src",
"clean": "tsc --build --clean && rm -rf dist"
},
"dependencies": {
"@sveltejs/load-config": "^0.2.3",
"@swc/core": "catalog:",
"@workflow/builders": "workspace:*",
"@workflow/rollup": "workspace:*",
Expand All @@ -39,6 +41,7 @@
"@types/node": "catalog:",
"@workflow/tsconfig": "workspace:*",
"typescript": "catalog:",
"vite": "7.3.6"
"vite": "7.3.6",
"vitest": "catalog:"
}
}
40 changes: 5 additions & 35 deletions packages/sveltekit/src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import {
stat,
writeFile,
} from 'node:fs/promises';
import { createRequire } from 'node:module';
import { dirname, join, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { join, resolve } from 'node:path';
import {
BaseBuilder,
createBaseBuilderConfig,
Expand All @@ -27,7 +25,7 @@ const SVELTEKIT_VIRTUAL_MODULES = [
];

export class SvelteKitBuilder extends BaseBuilder {
#routesDir: string | undefined;
#routesDir: string;

constructor(config: Partial<SvelteKitConfig> & { routesDir?: string } = {}) {
const workingDir = resolve(config.workingDir || process.cwd());
Expand All @@ -38,9 +36,7 @@ export class SvelteKitBuilder extends BaseBuilder {
'src/routes',
];
const projectRoot = config.projectRoot ?? resolveProjectRoot(workingDir);
const routesDir = config.routesDir
? resolve(workingDir, config.routesDir)
: undefined;
const routesDir = resolve(workingDir, config.routesDir ?? 'src/routes');
super({
...createBaseBuilderConfig({
workingDir,
Expand Down Expand Up @@ -197,37 +193,11 @@ export const OPTIONS = createSvelteKitHandler('OPTIONS');`
}

private async loadRoutesDirectory(): Promise<string> {
const routesDir =
this.#routesDir ?? (await loadSvelteKitRoutesDir(this.config.workingDir));
await assertDirectory(routesDir);
return routesDir;
await assertDirectory(this.#routesDir);
return this.#routesDir;
}
}

export async function loadSvelteKitRoutesDir(
workingDir: string
): Promise<string> {
const require = createRequire(join(workingDir, 'package.json'));
const packageJsonPath = require.resolve('@sveltejs/kit/package.json');
const loaderPath = join(dirname(packageJsonPath), 'src/core/config/index.js');

const configModule = await import(pathToFileURL(loaderPath).href);
// SvelteKit 2.62+ `load_config()` resolves Vite config first. Calling it
// while `workflow/sveltekit` is imported from vite.config.ts recursively
// reloads vite.config.ts and leaves this top-level build unresolved.
const config =
configModule.load_svelte_config != null
? await configModule.load_svelte_config(workingDir)
: await configModule.load_config({ cwd: workingDir });
const routesDir = config.kit?.files?.routes;
if (routesDir == null || typeof routesDir !== 'string') {
throw new Error(
'Expected SvelteKit config loader to return kit.files.routes as a string.'
);
}
return routesDir;
}

async function assertDirectory(path: string): Promise<void> {
await access(path, constants.F_OK);
const stats = await stat(path);
Expand Down
8 changes: 0 additions & 8 deletions packages/sveltekit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,8 @@ import path from 'node:path';
import { getWorkflowQueueTrigger } from '@workflow/builders';
import fs from 'fs-extra';

import { SvelteKitBuilder } from './builder.js';
import { stripWorkflowQueueTriggers } from './vc-config.js';

const builder = new SvelteKitBuilder();

// This needs to be in the top-level as we need to create these
// entries before svelte plugin is started or the entries are
// a race to be created before svelte discovers entries
await builder.build();

process.on('beforeExit', () => {
// Don't patch functions output if not in Vercel adapter
if (!process.env.VERCEL_DEPLOYMENT_ID) {
Expand Down
147 changes: 147 additions & 0 deletions packages/sveltekit/src/plugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

const mocks = vi.hoisted(() => ({
build: vi.fn<() => Promise<void>>(),
builderConfigs: [] as Array<Record<string, unknown>>,
loadConfig: vi.fn(),
}));

vi.mock('@sveltejs/load-config', () => ({
loadConfig: mocks.loadConfig,
}));

vi.mock('@workflow/builders', () => ({
createBuildQueue: () => (fn: () => Promise<void>) => fn(),
}));

vi.mock('@workflow/rollup', () => ({
workflowTransformPlugin: () => ({ name: 'workflow:transform' }),
}));

vi.mock('@workflow/vite', () => ({
workflowHotUpdatePlugin: () => ({ name: 'workflow:hot-update' }),
}));

vi.mock('./builder.js', () => ({
SvelteKitBuilder: class {
constructor(config: Record<string, unknown>) {
mocks.builderConfigs.push(config);
}

build = mocks.build;
},
}));

import { workflowPlugin } from './plugin.js';

const originalWorkingDir = process.cwd();
let workingDir: string;

beforeEach(async () => {
workingDir = await mkdtemp(join(tmpdir(), 'workflow-sveltekit-'));
process.chdir(workingDir);
workingDir = process.cwd(); // MacOS: resolves symlink of generated /var/folders/... through to /private/var/folders/...
mocks.build.mockResolvedValue(undefined);
});

afterEach(async () => {
process.chdir(originalWorkingDir);
await rm(workingDir, { force: true, recursive: true });
vi.clearAllMocks();
mocks.builderConfigs.length = 0;
});

describe('workflowPlugin', () => {
it('loads svelte.config.js directly and uses the default routes directory', async () => {
const configPath = join(workingDir, 'svelte.config.js');
await writeFile(configPath, 'export default {};');
mocks.loadConfig.mockResolvedValue({
config: {},
configFilePath: configPath,
configSource: 'svelte',
});

const [plugins] = workflowPlugin();
await plugins;

expect(mocks.loadConfig).toHaveBeenCalledWith(configPath, {
traverse: false,
});
expect(mocks.builderConfigs).toEqual([
{ routesDir: join(workingDir, 'src/routes'), workingDir },
]);
expect(mocks.build).toHaveBeenCalledOnce();
});

it('loads routes from the SvelteKit Vite plugin without recursing', async () => {
const routesDir = join(workingDir, 'app/routes');
let nestedPlugins: ReturnType<typeof workflowPlugin> | undefined;
mocks.loadConfig.mockImplementation(async () => {
nestedPlugins = workflowPlugin();
return {
config: { kit: { files: { routes: routesDir } } },
configFilePath: join(workingDir, 'vite.config.ts'),
configSource: 'vite',
};
});

const [plugins] = workflowPlugin();
await plugins;

expect(nestedPlugins).toEqual([]);
expect(mocks.loadConfig).toHaveBeenCalledWith(workingDir, {
traverse: false,
});
expect(mocks.builderConfigs).toEqual([{ routesDir, workingDir }]);
expect(mocks.build).toHaveBeenCalledOnce();
});

it('reuses routes generated by another Vite config context', async () => {
const routesDir = join(workingDir, 'app/routes');
await mkdir(join(workingDir, 'node_modules/.cache/workflow'), {
recursive: true,
});
await mkdir(join(routesDir, '.well-known/workflow/v1/flow'), {
recursive: true,
});
await writeFile(
join(workingDir, 'node_modules/.cache/workflow/sveltekit-build.json'),
JSON.stringify({ pid: process.pid, routesDir })
);
await writeFile(
join(routesDir, '.well-known/workflow/v1/flow/+server.js'),
''
);

const [plugins] = workflowPlugin();
await plugins;

expect(mocks.loadConfig).not.toHaveBeenCalled();
expect(mocks.builderConfigs).toEqual([{ routesDir, workingDir }]);
expect(mocks.build).not.toHaveBeenCalled();
});

it('passes source map options to the builder', async () => {
const configPath = join(workingDir, 'svelte.config.js');
await writeFile(configPath, 'export default {};');
mocks.loadConfig.mockResolvedValue({
config: {},
configFilePath: configPath,
configSource: 'svelte',
});

const [plugins] = workflowPlugin({ sourcemap: 'external' });
await plugins;

expect(mocks.builderConfigs).toEqual([
{
routesDir: join(workingDir, 'src/routes'),
workingDir,
sourcemap: 'external',
},
]);
});
});
Loading
Loading