Skip to content

Commit c8e473d

Browse files
hi-ogawadai-shi
andauthored
refactor!: make unstable_fsRouter API to be independent from fs internally (#1603)
Alternative of #1599. This simplifies `unstable_fsRouter` API so that fs crawling are expected to be handled outside. For managed mode, it uses `import.meta.glob` to set this up automatically. New API accepts a mapping of a file to a module such as ```tsx unstalbe_fsRouter( { "index.tsx": () => ({ default: Index }), "bar/index.tsx": () => ({ default: Bar }), }, { apiDir: 'api' }, ); function Index() { return <div>Index</div> } function Bar() { return <div>Bar</div> } ``` which can be ergonomically generated by Vite's `import.meta.glob`. ```tsx unstalbe_fsRouter( import.meta.glob("/src/pages/**/*.tsx", { base: "/src/pages" }) { apiDir: 'api' }, ); ``` --------- Co-authored-by: Daishi Kato <dai-shi@users.noreply.github.com>
1 parent 13be182 commit c8e473d

5 files changed

Lines changed: 42 additions & 118 deletions

File tree

examples/11_fs-router/src/server-entry.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
import { unstable_fsRouter as fsRouter } from 'waku/router/server';
33

44
export default fsRouter(
5-
import.meta.url,
6-
(file: string) =>
7-
import.meta.glob('./pages/**/*.{tsx,ts}')[`./pages/${file}`]?.(),
8-
{ pagesDir: 'pages', apiDir: 'api', slicesDir: '_slices' },
5+
import.meta.glob('/src/pages/**/*.{tsx,ts}', { base: '/src/pages' }),
6+
{ apiDir: 'api', slicesDir: '_slices' },
97
);

examples/12_nossr/src/server-entry.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ import { unstable_defineEntries as defineEntries } from 'waku/minimal/server';
44
import { unstable_fsRouter as fsRouter } from 'waku/router/server';
55

66
const router = fsRouter(
7-
import.meta.url,
8-
(file) => import.meta.glob('./pages/**/*.tsx')[`./pages/${file}`]?.(),
9-
{ pagesDir: 'pages', apiDir: 'api', slicesDir: '_slices' },
7+
import.meta.glob('/src/pages/**/*.tsx', { base: '/src/pages' }),
8+
{ apiDir: 'api', slicesDir: '_slices' },
109
);
1110

1211
export default defineEntries({
Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
1+
import type { Config } from '../../config.js';
2+
import type { unstable_fsRouter } from '../../router/fs-router.js';
13
import { EXTENSIONS } from '../builder/constants.js';
2-
import { filePathToFileURL } from '../utils/path.js';
34

4-
export const getManagedEntries = (
5-
filePath: string,
6-
srcDir: string,
7-
options: { pagesDir: string; apiDir: string; slicesDir: string },
8-
) => `
5+
export const getManagedServerEntry = (config: Required<Config>) => {
6+
const globBase = `/${config.srcDir}/${config.pagesDir}/`;
7+
const globPattern = `${globBase}**/*.{${EXTENSIONS.map((ext) => ext.slice(1)).join(',')}}`;
8+
const fsRouterOptions: Parameters<typeof unstable_fsRouter>[1] = {
9+
apiDir: config.apiDir,
10+
slicesDir: config.slicesDir,
11+
};
12+
return `
913
import { unstable_fsRouter as fsRouter } from 'waku/router/server';
10-
11-
export default fsRouter(
12-
'${filePathToFileURL(filePath)}',
13-
(file) => import.meta.glob('/${srcDir}/pages/**/*.{${EXTENSIONS.map((ext) =>
14-
ext.replace(/^\./, ''),
15-
).join(',')}}')[\`/${srcDir}/pages/\${file}\`]?.(),
16-
{ pagesDir: '${options.pagesDir}', apiDir: '${options.apiDir}', slicesDir: '${options.slicesDir}' },
17-
);
14+
const glob = import.meta.glob(${JSON.stringify(globPattern)}, { base: ${JSON.stringify(globBase)} });
15+
export default fsRouter(glob, ${JSON.stringify(fsRouterOptions)});
1816
`;
17+
};
1918

20-
export const getManagedMain = () => `
19+
export const getManagedClientEntry = () => {
20+
return `
2121
import { StrictMode, createElement } from 'react';
2222
import { createRoot, hydrateRoot } from 'react-dom/client';
2323
import { Router } from 'waku/router/client';
@@ -30,3 +30,4 @@ if (globalThis.__WAKU_HYDRATE__) {
3030
createRoot(document).render(rootElement);
3131
}
3232
`;
33+
};

packages/waku/src/lib/vite-rsc/plugin.ts

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
1313
import path from 'node:path';
1414
import assert from 'node:assert/strict';
1515
import fs from 'node:fs';
16+
import {
17+
getManagedClientEntry,
18+
getManagedServerEntry,
19+
} from '../utils/managed.js';
1620
import type { Config } from '../../config.js';
1721
import { INTERNAL_setAllEnv, unstable_getBuildOptions } from '../../server.js';
1822
import { emitStaticFile, waitForTasks } from '../builder/build.js';
19-
import { getManagedEntries, getManagedMain } from '../utils/managed.js';
2023
import { deployVercelPlugin } from './deploy/vercel/plugin.js';
2124
import { allowServerPlugin } from '../vite-plugins/allow-server.js';
2225
import {
@@ -30,7 +33,7 @@ import { deployCloudflarePlugin } from './deploy/cloudflare/plugin.js';
3033
import { deployPartykitPlugin } from './deploy/partykit/plugin.js';
3134
import { deployDenoPlugin } from './deploy/deno/plugin.js';
3235
import { deployAwsLambdaPlugin } from './deploy/aws-lambda/plugin.js';
33-
import { filePathToFileURL, joinPath } from '../utils/path.js';
36+
import { joinPath } from '../utils/path.js';
3437

3538
const PKG_NAME = 'waku';
3639
const __dirname = fileURLToPath(new URL('.', import.meta.url));
@@ -74,7 +77,6 @@ export function rscPlugin(rscPluginOptions?: RscPluginOptions): PluginOption {
7477
};
7578
const flags = rscPluginOptions?.flags ?? {};
7679
let privatePath: string;
77-
let customServerEntry: string | undefined;
7880

7981
const extraPlugins = [...(config.vite?.plugins ?? [])];
8082
// add react plugin automatically if users didn't include it on their own (e.g. swc, oxc, babel react compiler)
@@ -255,7 +257,6 @@ export function rscPlugin(rscPluginOptions?: RscPluginOptions): PluginOption {
255257
undefined,
256258
options,
257259
);
258-
customServerEntry = resolved?.id;
259260
return resolved ? resolved : '\0' + source;
260261
}
261262
if (source === 'virtual:vite-rsc-waku/client-entry') {
@@ -277,32 +278,10 @@ if (import.meta.hot) {
277278
`;
278279
}
279280
if (id === '\0virtual:vite-rsc-waku/server-entry-inner') {
280-
return getManagedEntries(
281-
joinPath(
282-
this.environment.config.root,
283-
config.srcDir,
284-
'server-entry.js',
285-
),
286-
'src',
287-
{
288-
pagesDir: config.pagesDir,
289-
apiDir: config.apiDir,
290-
slicesDir: config.slicesDir,
291-
},
292-
);
281+
return getManagedServerEntry(config);
293282
}
294283
if (id === '\0virtual:vite-rsc-waku/client-entry') {
295-
return getManagedMain();
296-
}
297-
},
298-
transform(code, id) {
299-
// rewrite `fsRouter(import.meta.url, ...)` in custom server entry
300-
// e.g. examples/11_fs-router/src/server-entry.tsx
301-
// TODO: rework fsRouter to entirely avoid fs access on runtime
302-
if (id === customServerEntry && code.includes('fsRouter')) {
303-
const replacement = JSON.stringify(filePathToFileURL(id));
304-
code = code.replaceAll(/\bimport\.meta\.url\b/g, () => replacement);
305-
return code;
284+
return getManagedClientEntry();
306285
}
307286
},
308287
},

packages/waku/src/router/fs-router.ts

Lines changed: 15 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,21 @@
1-
import {
2-
unstable_getPlatformData,
3-
unstable_setPlatformData,
4-
unstable_getBuildOptions,
5-
} from '../server.js';
61
import { createPages, METHODS } from './create-pages.js';
72
import type { Method } from './create-pages.js';
83

9-
import { EXTENSIONS } from '../lib/builder/constants.js';
104
import { isIgnoredPath } from '../lib/utils/fs-router.js';
115

12-
const DO_NOT_BUNDLE = '';
13-
146
export function unstable_fsRouter(
15-
importMetaUrl: string,
16-
loadPage: (file: string) => Promise<any> | undefined,
7+
/**
8+
* A mapping from a file path to a route module, e.g.
9+
* {
10+
* "_layout.tsx": () => ({ default: ... }),
11+
* "index.tsx": () => ({ default: ... }),
12+
* "foo/index.tsx": () => ...,
13+
* }
14+
* This mapping can be created by Vite's import.meta.glob, e.g.
15+
* import.meta.glob("/src/pages/**\/*.tsx", { base: "/src/pages" })
16+
*/
17+
pages: { [file: string]: () => Promise<any> },
1718
options: {
18-
/** e.g. `"pages"` will detect pages in `src/pages`. */
19-
pagesDir: string;
2019
/**
2120
* e.g. `"api"` will detect pages in `src/pages/api`. Or, if `options.pagesDir`
2221
* is `"foo"`, then it will detect pages in `src/foo/api`.
@@ -26,7 +25,6 @@ export function unstable_fsRouter(
2625
slicesDir: string;
2726
},
2827
) {
29-
const buildOptions = unstable_getBuildOptions();
3028
return createPages(
3129
async ({
3230
createPage,
@@ -35,61 +33,10 @@ export function unstable_fsRouter(
3533
createApi,
3634
createSlice,
3735
}) => {
38-
let files = await unstable_getPlatformData<string[]>('fsRouterFiles');
39-
if (!files) {
40-
// dev and build only
41-
if (
42-
import.meta.env &&
43-
import.meta.env.MODE === 'production' &&
44-
!buildOptions.unstable_phase
45-
) {
46-
throw new Error('files must be set in production.');
47-
}
48-
const [
49-
{ readdir },
50-
{ join, dirname, extname, sep },
51-
{ fileURLToPath },
52-
] = await Promise.all([
53-
import(/* @vite-ignore */ DO_NOT_BUNDLE + 'node:fs/promises'),
54-
import(/* @vite-ignore */ DO_NOT_BUNDLE + 'node:path'),
55-
import(/* @vite-ignore */ DO_NOT_BUNDLE + 'node:url'),
56-
]);
57-
const pagesDir = join(
58-
dirname(fileURLToPath(importMetaUrl)),
59-
options.pagesDir,
60-
);
61-
files = await readdir(pagesDir, {
62-
encoding: 'utf8',
63-
recursive: true,
64-
});
65-
files = files!.flatMap((file) => {
66-
const myExt = extname(file);
67-
const myExtIndex = EXTENSIONS.indexOf(myExt);
68-
if (myExtIndex === -1) {
69-
return [];
70-
}
71-
// HACK: replace "_slug_" to "[slug]" for build
72-
file = file.replace(/(?<=^|\/|\\)_([^/]+)_(?=\/|\\|\.)/g, '[$1]');
73-
// For Windows
74-
file = sep === '/' ? file : file.replace(/\\/g, '/');
75-
// HACK: resolve different extensions for build
76-
const exts = [myExt, ...EXTENSIONS];
77-
exts.splice(myExtIndex + 1, 1); // remove the second myExt
78-
for (const ext of exts) {
79-
const f = file.slice(0, -myExt.length) + ext;
80-
if (loadPage(f)) {
81-
return [f];
82-
}
83-
}
84-
throw new Error('Failed to resolve ' + file);
85-
});
86-
}
87-
// build only - skip in dev
88-
if (buildOptions.unstable_phase) {
89-
await unstable_setPlatformData('fsRouterFiles', files, true);
90-
}
91-
for (const file of files) {
92-
const mod = await loadPage(file);
36+
for (let file in pages) {
37+
const mod = await pages[file]!();
38+
// strip "./" prefix
39+
file = file.replace(/^\.\//, '');
9340
const config = await mod.getConfig?.();
9441
const pathItems = file
9542
.replace(/\.\w+$/, '')

0 commit comments

Comments
 (0)