-
-
Notifications
You must be signed in to change notification settings - Fork 124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SSG: support wildcards in static paths #666
Changes from all commits
1441e48
dd31d4d
0ff4a8d
3372cad
b633b13
b085fb6
2f90814
571061f
bcc8b6b
41eb3d6
25604f6
ae27243
5eac498
595c934
d56db34
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
{ | ||
"name": "waku-example", | ||
"version": "0.1.0", | ||
"type": "module", | ||
"private": true, | ||
"scripts": { | ||
"dev": "waku dev", | ||
"build": "waku build", | ||
"start": "waku start" | ||
}, | ||
"dependencies": { | ||
"react": "19.0.0-beta-4508873393-20240430", | ||
"react-dom": "19.0.0-beta-4508873393-20240430", | ||
"react-server-dom-webpack": "19.0.0-beta-4508873393-20240430", | ||
"waku": "workspace:*" | ||
}, | ||
"devDependencies": { | ||
"@types/react": "18.3.1", | ||
"@types/react-dom": "18.3.0", | ||
"typescript": "5.4.4" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { StrictMode } from 'react'; | ||
import { createRoot, hydrateRoot } from 'react-dom/client'; | ||
import { Router } from 'waku/router/client'; | ||
|
||
const rootElement = ( | ||
<StrictMode> | ||
<Router /> | ||
</StrictMode> | ||
); | ||
|
||
if (document.body.dataset.hydrate) { | ||
hydrateRoot(document.body, rootElement); | ||
} else { | ||
createRoot(document.body).render(rootElement); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
const Page = ({ wildcard }: { wildcard: string[] }) => ( | ||
<div> | ||
<h1>/{wildcard.join('/')}</h1> | ||
</div> | ||
); | ||
|
||
export const getConfig = async () => { | ||
return { | ||
render: 'static', | ||
staticPaths: [[], 'foo', ['bar', 'baz']], | ||
}; | ||
}; | ||
|
||
export default Page; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import type { ReactNode } from 'react'; | ||
|
||
const Layout = ({ children }: { children: ReactNode }) => ( | ||
<div> | ||
<title>Waku</title> | ||
{children} | ||
</div> | ||
); | ||
|
||
export default Layout; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{ | ||
"compilerOptions": { | ||
"composite": true, | ||
"strict": true, | ||
"target": "esnext", | ||
"downlevelIteration": true, | ||
"esModuleInterop": true, | ||
"module": "esnext", | ||
"moduleResolution": "bundler", | ||
"skipLibCheck": true, | ||
"noUncheckedIndexedAccess": true, | ||
"exactOptionalPropertyTypes": true, | ||
"types": ["react/experimental"], | ||
"jsx": "react-jsx" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import { debugChildProcess, getFreePort, terminate, test } from './utils.js'; | ||
import { fileURLToPath } from 'node:url'; | ||
import { cp, mkdtemp } from 'node:fs/promises'; | ||
import { exec, execSync } from 'node:child_process'; | ||
import { expect } from '@playwright/test'; | ||
import waitPort from 'wait-port'; | ||
import { join } from 'node:path'; | ||
import { tmpdir } from 'node:os'; | ||
import { createRequire } from 'node:module'; | ||
|
||
let standaloneDir: string; | ||
const exampleDir = fileURLToPath( | ||
new URL('./fixtures/ssg-wildcard', import.meta.url), | ||
); | ||
const wakuDir = fileURLToPath(new URL('../packages/waku', import.meta.url)); | ||
const { version } = createRequire(import.meta.url)( | ||
join(wakuDir, 'package.json'), | ||
); | ||
|
||
test.describe('ssg wildcard', async () => { | ||
test.beforeEach(async () => { | ||
// GitHub Action on Windows doesn't support mkdtemp on global temp dir, | ||
// Which will cause files in `src` folder to be empty. | ||
// I don't know why | ||
const tmpDir = process.env.TEMP_DIR ? process.env.TEMP_DIR : tmpdir(); | ||
Comment on lines
+22
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, does that mean @himself65 thoughts? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, but I think we could just make things under .cache folder? I forget why I prefer to use tmpdir here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, this comment is copied from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, copy from my code 😆 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I like to call it "proudly found elsewhere". |
||
standaloneDir = await mkdtemp(join(tmpDir, 'waku-ssg-wildcard-')); | ||
await cp(exampleDir, standaloneDir, { | ||
filter: (src) => { | ||
return !src.includes('node_modules') && !src.includes('dist'); | ||
}, | ||
recursive: true, | ||
}); | ||
execSync(`pnpm pack --pack-destination ${standaloneDir}`, { | ||
cwd: wakuDir, | ||
stdio: 'inherit', | ||
}); | ||
const name = `waku-${version}.tgz`; | ||
execSync(`npm install ${join(standaloneDir, name)}`, { | ||
cwd: standaloneDir, | ||
stdio: 'inherit', | ||
}); | ||
}); | ||
|
||
test(`works`, async ({ page }) => { | ||
execSync( | ||
`node ${join(standaloneDir, './node_modules/waku/dist/cli.js')} build`, | ||
{ | ||
cwd: standaloneDir, | ||
stdio: 'inherit', | ||
}, | ||
); | ||
const port = await getFreePort(); | ||
const cp = exec( | ||
`node ${join(standaloneDir, './node_modules/waku/dist/cli.js')} start --port ${port}`, | ||
{ cwd: standaloneDir }, | ||
); | ||
debugChildProcess(cp, fileURLToPath(import.meta.url), [ | ||
/ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time/, | ||
]); | ||
|
||
await waitPort({ port }); | ||
|
||
await page.goto(`http://localhost:${port}`); | ||
await expect(page.getByRole('heading', { name: '/' })).toBeVisible(); | ||
|
||
await page.goto(`http://localhost:${port}/foo`); | ||
await expect(page.getByRole('heading', { name: '/foo' })).toBeVisible(); | ||
|
||
await page.goto(`http://localhost:${port}/bar/baz`); | ||
await expect(page.getByRole('heading', { name: '/bar/baz' })).toBeVisible(); | ||
|
||
await terminate(cp.pid!); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -82,7 +82,7 @@ export type CreatePage = < | |||
} | ||||
| { | ||||
render: 'static'; | ||||
path: PathWithSlug<Path, SlugKey>; | ||||
path: PathWithWildcard<Path, SlugKey, WildSlugKey>; | ||||
staticPaths: string[] | string[][]; | ||||
component: FunctionComponent<RouteProps & Record<SlugKey, string>>; | ||||
} | ||||
|
@@ -170,27 +170,35 @@ export function createPages( | |||
staticPathSet.add([page.path, pathSpec]); | ||||
const id = joinPath(page.path, 'page').replace(/^\//, ''); | ||||
registerStaticComponent(id, page.component); | ||||
} else if (page.render === 'static' && numSlugs > 0 && numWildcards === 0) { | ||||
} else if (page.render === 'static' && numSlugs > 0) { | ||||
const staticPaths = ( | ||||
page as { | ||||
staticPaths: string[] | string[][]; | ||||
} | ||||
).staticPaths.map((item) => (Array.isArray(item) ? item : [item])); | ||||
for (const staticPath of staticPaths) { | ||||
if (staticPath.length !== numSlugs) { | ||||
if (staticPath.length !== numSlugs && numWildcards === 0) { | ||||
throw new Error('staticPaths does not match with slug pattern'); | ||||
} | ||||
const mapping: Record<string, string> = {}; | ||||
const mapping: Record<string, string | string[]> = {}; | ||||
let slugIndex = 0; | ||||
const pathItems = pathSpec.map(({ type, name }) => { | ||||
if (type !== 'literal') { | ||||
const actualName = staticPath[slugIndex++]!; | ||||
if (name) { | ||||
mapping[name] = actualName; | ||||
} | ||||
return actualName; | ||||
const pathItems = [] as string[]; | ||||
pathSpec.forEach(({ type, name }) => { | ||||
switch (type) { | ||||
case 'literal': | ||||
pathItems.push(name!); | ||||
break; | ||||
case 'wildcard': | ||||
mapping[name!] = staticPath.slice(slugIndex); | ||||
staticPath.slice(slugIndex++).forEach((slug) => { | ||||
pathItems.push(slug); | ||||
}); | ||||
break; | ||||
case 'group': | ||||
pathItems.push(staticPath[slugIndex++]!); | ||||
mapping[name!] = pathItems[pathItems.length - 1]!; | ||||
break; | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, now you read the create-page.ts code, do you have any idea to improve it or completely rewrite it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did find my way in reasonable time, so it can't be that bad 😄
Happy to take a stab at this, in a separate PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That would be more than helpful! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There we go: #667 I created a pure setup-PR first, since i'm also working on another PR where vitest would help. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will you add some tests for this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was not too easy to find a way to test it. But i think this gives good coverage to do a confident refactor in a later step: #692 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So the unit tests immediately revealed a regression! 😅 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's nice! Yeah, because I was very unsure that I could review the code. |
||||
} | ||||
return name; | ||||
}); | ||||
staticPathSet.add([ | ||||
page.path, | ||||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't type checked.
I wonder whether we should make
ssg-wildcard
withcreatePages
API instead of fs-router.We can say that types are covered with unit tests, but still fixtures should be as basic as possible?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, it is not type-covered in
createPages
either:waku/packages/waku/tests/create-pages.test.ts
Lines 60 to 67 in 10c2865
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I see. I hope we can fix it.
My question still holds: Should we make fixtures use low-level createPages instead of fs-router? (Unless we specifically tests fs-router.)
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. My gut feeling would be to use integration tests to cover as much ground as possible, but in this case
createPages
is a common entry point (and even my preferred one). Alsofs-router.ts
is a good target for a unit test.But this then should also apply to
partial-build
andssg-performance
. And most other tests don't usecreatePages
butdefineRouter
. Maybe another PR where we look at the e2e setup holistically, including the typing hiccups that we encountered?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. Sounds all good!