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
18 changes: 18 additions & 0 deletions e2e/create-pages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,24 @@ test.describe(`create-pages`, () => {
});
});

test('static api wildcard passes correct params', async () => {
const res1 = await fetch(
`http://localhost:${port}/api/static-wildcard/a/b`,
);
expect(res1.status).toBe(200);
expect(await res1.json()).toEqual({ params: { slugs: ['a', 'b'] } });

const res2 = await fetch(`http://localhost:${port}/api/static-wildcard/c`);
expect(res2.status).toBe(200);
expect(await res2.json()).toEqual({ params: { slugs: ['c'] } });
});

test('static api wildcard with empty path', async () => {
const res = await fetch(`http://localhost:${port}/api/static-wildcard`);
expect(res.status).toBe(200);
expect(await res.json()).toEqual({ params: { slugs: [] } });
});

test('exactPath', async ({ page }) => {
await page.goto(`http://localhost:${port}/exact/[slug]/[...wild]`);
await expect(
Expand Down
10 changes: 10 additions & 0 deletions e2e/fixtures/create-pages/src/waku.server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,16 @@ const pages: ReturnType<typeof createPages> = createPages(
},
}),

createApi({
path: '/api/static-wildcard/[...slugs]',
render: 'static',
method: 'GET',
staticPaths: [['a', 'b'], ['c'], []],
handler: async (_req, ctx) => {
return Response.json({ params: (ctx as any).params });
},
}),

createPage({
render: 'static',
path: '/exact/[slug]/[...wild]',
Expand Down
11 changes: 8 additions & 3 deletions packages/waku/src/router/create-pages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ export const createPages = <
render: 'static' | 'dynamic';
pathSpec: PathSpec;
handlers: Partial<Record<Method | 'all', ApiHandler>>;
staticParams?: Record<string, string | string[]>;
}
>();
const staticComponentMap = new Map<string, FunctionComponent<any>>();
Expand Down Expand Up @@ -552,7 +553,7 @@ export const createPages = <
if (staticPath.length !== numSlugs && numWildcards === 0) {
throw new Error('staticPaths does not match with slug pattern');
}
const { definedPath, pathItems } = expandStaticPathSpec(
const { definedPath, pathItems, mapping } = expandStaticPathSpec(
pathSpec,
staticPath,
);
Expand All @@ -563,6 +564,7 @@ export const createPages = <
render: 'static',
pathSpec: pathItems.map((name) => ({ type: 'literal', name })),
handlers: { GET: options.handler },
staticParams: mapping,
});
}
} else {
Expand Down Expand Up @@ -854,7 +856,7 @@ export const createPages = <
});
}
const apiConfigs = Array.from(apiPathMap.values()).map(
({ pathSpec, render, handlers }) => {
({ pathSpec, render, handlers, staticParams }) => {
return {
type: 'api' as const,
path: pathSpec,
Expand All @@ -871,7 +873,10 @@ export const createPages = <
'API method not found: ' + method + 'for path: ' + path,
);
}
return handler(req, apiContext);
return handler(
req,
staticParams ? { params: staticParams } : apiContext,
);
},
};
},
Expand Down
22 changes: 21 additions & 1 deletion packages/waku/src/router/define-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,16 @@ export function unstable_defineRouter(fns: {
const { runTask, waitForTasks } = createTaskRunner(500);

// static api
const staticApiPathnames = new Set<string>();
for (const item of myConfig.configs) {
if (item.type !== 'api' || !item.isStatic) {
continue;
}
const pathname = pathSpec2pathname(item.path);
if (pathname) {
staticApiPathnames.add(pathname);
}
}
for (const item of myConfig.configs) {
if (item.type !== 'api') {
continue;
Expand All @@ -665,11 +675,21 @@ export function unstable_defineRouter(fns: {
if (!pathname) {
continue;
}
// If this pathname is also a directory prefix for another static API
// route, emit as pathname/index to avoid file-system conflicts.
Comment on lines +678 to +679
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about this. What use cases is this trying to cover?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The one from #1978. It supports the empty staticPath as a / on /[...wildcard]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, but adding /index here seems wrong. Any other solution?

let hasConflict = false;
for (const other of staticApiPathnames) {
if (other !== pathname && other.startsWith(pathname + '/')) {
hasConflict = true;
break;
}
}
const filePath = hasConflict ? pathname + '/index' : pathname;
const req = new Request(new URL(pathname, 'http://localhost:3000'));
runTask(async () => {
await withRequest(req, async () => {
const res = await item.handler(req, { params: {} });
await generateFile(pathname, res.body || '');
await generateFile(filePath, res.body || '');
});
});
}
Expand Down
81 changes: 81 additions & 0 deletions packages/waku/tests/create-pages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2116,6 +2116,87 @@ describe('createPages api', () => {
expect(text).toEqual('Hello World foo');
expect(res.status).toEqual(200);
});

it('static api with wildcard passes correct params', async () => {
const receivedParams: unknown[] = [];
createPages(async ({ createApi }) => [
createApi({
path: '/test/[...slugs]',
render: 'static',
method: 'GET',
staticPaths: [['a', 'b'], ['c']],
handler: async (_req, ctx) => {
receivedParams.push((ctx as any).params);
return new Response('ok');
},
}),
]);
const { getConfigs } = injectedFunctions();
const configs = Array.from(await getConfigs()) as any[];
const apiConfigs = configs.filter((c: any) => c.type === 'api');
expect(apiConfigs).toHaveLength(2);

// Verify paths are all-literal
expect(apiConfigs[0]!.path).toEqual([
{ type: 'literal', name: 'test' },
{ type: 'literal', name: 'a' },
{ type: 'literal', name: 'b' },
]);
expect(apiConfigs[1]!.path).toEqual([
{ type: 'literal', name: 'test' },
{ type: 'literal', name: 'c' },
]);

// Call handlers and verify params
await apiConfigs[0]!.handler(
new Request('http://localhost:3000/test/a/b'),
{ params: {} },
);
await apiConfigs[1]!.handler(new Request('http://localhost:3000/test/c'), {
params: {},
});
expect(receivedParams).toEqual([{ slugs: ['a', 'b'] }, { slugs: ['c'] }]);
});

it('static api with wildcard and empty path produces correct configs', async () => {
const receivedParams: unknown[] = [];
createPages(async ({ createApi }) => [
createApi({
path: '/test/[...slugs]',
render: 'static',
method: 'GET',
staticPaths: [[], ['foo']],
handler: async (_req, ctx) => {
receivedParams.push((ctx as any).params);
return new Response('ok');
},
}),
]);
const { getConfigs } = injectedFunctions();
const configs = Array.from(await getConfigs()) as any[];
const apiConfigs = configs.filter((c: any) => c.type === 'api');
expect(apiConfigs).toHaveLength(2);

// Find configs by path length
const emptyConfig = apiConfigs.find((c: any) => c.path.length === 1);
const fooConfig = apiConfigs.find((c: any) => c.path.length === 2);

// Verify paths: empty wildcard should produce just /test
expect(emptyConfig!.path).toEqual([{ type: 'literal', name: 'test' }]);
expect(fooConfig!.path).toEqual([
{ type: 'literal', name: 'test' },
{ type: 'literal', name: 'foo' },
]);

// Call handlers and verify params
await emptyConfig!.handler(new Request('http://localhost:3000/test'), {
params: {},
});
await fooConfig!.handler(new Request('http://localhost:3000/test/foo'), {
params: {},
});
expect(receivedParams).toEqual([{ slugs: [] }, { slugs: ['foo'] }]);
});
});

describe('createPages - exactPath', () => {
Expand Down
Loading