Skip to content

Commit fca11e1

Browse files
committed
fix(adapter-sveltekit): properly handle SvelteKit base path dynamically via $app/paths
1 parent 6a261b7 commit fca11e1

File tree

4 files changed

+119
-19
lines changed

4 files changed

+119
-19
lines changed

packages/adapter-sveltekit/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tablecraft/adapter-sveltekit",
3-
"version": "0.1.0",
3+
"version": "0.1.1",
44
"description": "SvelteKit adapter for TableCraft",
55
"type": "module",
66
"main": "dist/index.js",
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
declare module '$app/paths' {
2+
export const base: string;
3+
}

packages/adapter-sveltekit/src/index.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// ** import types
22
import type { Handle, RequestEvent, RequestHandler } from '@sveltejs/kit';
33

4+
// ** import core packages
5+
import { base } from '$app/paths';
6+
47
// ** import apis
58
import {
69
createEngines,
@@ -202,12 +205,15 @@ async function hasTableAccess(
202205
* @returns The remaining path segment (e.g., 'table/_meta') or null if it doesn't match the prefix
203206
*/
204207
function stripPrefix(pathname: string, prefix: string): string | null {
205-
// Note: Since this is an adapter library, we cannot easily import $app/paths directly
206-
// without complicating the build. SvelteKit passes the full URL including base to hooks,
207-
// so developers using base paths may need to manually configure the prefix
208-
// (e.g. prefix: '/base/api/data').
208+
let cleanPathname = pathname;
209+
if (base && cleanPathname.startsWith(base)) {
210+
const nextChar = cleanPathname[base.length];
211+
if (nextChar === undefined || nextChar === '/') {
212+
cleanPathname = cleanPathname.slice(base.length);
213+
}
214+
}
209215

210-
const normalizedPath = pathname.replace(/\/+$/g, '') || '/';
216+
const normalizedPath = cleanPathname.replace(/\/+$/g, '') || '/';
211217

212218
if (normalizedPath === prefix) {
213219
return null;

packages/adapter-sveltekit/test/index.test.ts

Lines changed: 104 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,33 @@
11
// ** import core packages
22
import { beforeEach, describe, expect, it, vi } from 'vitest';
33

4-
const createEngines = vi.fn();
5-
const createTableEngine = vi.fn();
6-
const parseRequest = vi.fn();
7-
const defaultCheckAccess = vi.fn();
8-
const getExportMeta = vi.fn();
9-
10-
class MockTableCraftError extends Error {
11-
statusCode: number;
12-
13-
constructor(message: string, statusCode = 500) {
14-
super(message);
15-
this.statusCode = statusCode;
4+
const {
5+
createEngines,
6+
createTableEngine,
7+
parseRequest,
8+
defaultCheckAccess,
9+
getExportMeta,
10+
MockTableCraftError,
11+
mockState
12+
} = vi.hoisted(() => {
13+
class MockTableCraftError extends Error {
14+
statusCode: number;
15+
constructor(message: string, statusCode = 500) {
16+
super(message);
17+
this.statusCode = statusCode;
18+
}
1619
}
17-
}
20+
21+
return {
22+
createEngines: vi.fn(),
23+
createTableEngine: vi.fn(),
24+
parseRequest: vi.fn(),
25+
defaultCheckAccess: vi.fn(),
26+
getExportMeta: vi.fn(),
27+
MockTableCraftError,
28+
mockState: { base: '' }
29+
};
30+
});
1831

1932
vi.mock('@tablecraft/engine', () => ({
2033
createEngines,
@@ -25,6 +38,10 @@ vi.mock('@tablecraft/engine', () => ({
2538
TableCraftError: MockTableCraftError,
2639
}));
2740

41+
vi.mock('$app/paths', () => ({
42+
get base() { return mockState.base; }
43+
}));
44+
2845
import {
2946
createSvelteKitHandle,
3047
createSvelteKitHandlers,
@@ -44,6 +61,7 @@ function createEvent(urlStr: string, params: Record<string, string> = {}) {
4461
describe('@tablecraft/adapter-sveltekit', () => {
4562
beforeEach(() => {
4663
vi.clearAllMocks();
64+
mockState.base = '';
4765
parseRequest.mockReturnValue({ page: 1 });
4866
defaultCheckAccess.mockReturnValue(true);
4967
getExportMeta.mockReturnValue({
@@ -215,6 +233,79 @@ describe('@tablecraft/adapter-sveltekit', () => {
215233
expect(resolve).toHaveBeenCalledOnce();
216234
});
217235

236+
it('strips base path if present', async () => {
237+
mockState.base = '/myapp';
238+
239+
const usersEngine = {
240+
getConfig: vi.fn(() => ({ name: 'users' })),
241+
query: vi.fn(async () => ({
242+
data: [{ id: 8 }],
243+
meta: { total: 1, page: 1, pageSize: 10, totalPages: 1 },
244+
})),
245+
exportData: vi.fn(),
246+
getMetadata: vi.fn(),
247+
};
248+
249+
createEngines.mockReturnValue({ users: usersEngine });
250+
251+
const handle = createSvelteKitHandle({
252+
db: {},
253+
schema: {},
254+
configs: [],
255+
prefix: '/api',
256+
});
257+
258+
const resolve = vi.fn(async () => new Response('resolved', { status: 200 }));
259+
260+
const apiResponse = await handle({
261+
event: createEvent('https://example.test/myapp/api/users'),
262+
resolve,
263+
} as any);
264+
265+
expect(apiResponse.status).toBe(200);
266+
await expect(apiResponse.json()).resolves.toEqual({
267+
data: [{ id: 8 }],
268+
meta: { total: 1, page: 1, pageSize: 10, totalPages: 1 },
269+
});
270+
expect(resolve).not.toHaveBeenCalled();
271+
});
272+
273+
it('does not strip base path if it is a prefix of another folder', async () => {
274+
mockState.base = '/app';
275+
276+
const usersEngine = {
277+
getConfig: vi.fn(() => ({ name: 'users' })),
278+
query: vi.fn(async () => ({
279+
data: [{ id: 8 }],
280+
meta: { total: 1, page: 1, pageSize: 10, totalPages: 1 },
281+
})),
282+
exportData: vi.fn(),
283+
getMetadata: vi.fn(),
284+
};
285+
286+
createEngines.mockReturnValue({ users: usersEngine });
287+
288+
const handle = createSvelteKitHandle({
289+
db: {},
290+
schema: {},
291+
configs: [],
292+
prefix: '/api',
293+
});
294+
295+
const resolve = vi.fn(async () => new Response('resolved', { status: 200 }));
296+
297+
// /apple is NOT under /app
298+
const apiResponse = await handle({
299+
event: createEvent('https://example.test/apple/api/users'),
300+
resolve,
301+
} as any);
302+
303+
// Should passthrough
304+
expect(apiResponse.status).toBe(200);
305+
await expect(apiResponse.text()).resolves.toBe('resolved');
306+
expect(resolve).toHaveBeenCalledOnce();
307+
});
308+
218309
it('serves metadata through the hook prefix and rejects non-GET methods', async () => {
219310
const usersEngine = {
220311
getConfig: vi.fn(() => ({ name: 'users' })),

0 commit comments

Comments
 (0)