From a9cc6c768a4875b8ad21d9ec30f2d146207767e6 Mon Sep 17 00:00:00 2001 From: Tyler <26290074+thegitduck@users.noreply.github.com> Date: Thu, 26 Sep 2024 21:36:41 -0700 Subject: [PATCH 1/4] feat: entries gen for fs router projects --- examples/05_nossr/src/entries.gen.tsx | 32 +++ examples/05_nossr/src/pages/_layout.tsx | 2 +- examples/05_nossr/src/pages/about.tsx | 2 +- examples/05_nossr/src/pages/index.tsx | 2 +- examples/10_fs-router/src/entries.gen.tsx | 38 ++++ examples/10_fs-router/src/entries.tsx | 11 - examples/10_fs-router/src/pages/_layout.tsx | 5 + examples/10_fs-router/src/pages/layout.tsx | 7 + packages/waku/src/lib/constants.ts | 1 + .../src/lib/middleware/dev-server-impl.ts | 2 + .../plugins/vite-plugin-fs-router-typegen.ts | 195 ++++++++++++++++++ .../create-pages-utils/inferred-path-types.ts | 20 +- packages/waku/src/router/create-pages.ts | 10 +- packages/waku/tests/create-pages.test.ts | 2 +- 14 files changed, 301 insertions(+), 28 deletions(-) create mode 100644 examples/05_nossr/src/entries.gen.tsx create mode 100644 examples/10_fs-router/src/entries.gen.tsx delete mode 100644 examples/10_fs-router/src/entries.tsx create mode 100644 examples/10_fs-router/src/pages/layout.tsx create mode 100644 packages/waku/src/lib/plugins/vite-plugin-fs-router-typegen.ts diff --git a/examples/05_nossr/src/entries.gen.tsx b/examples/05_nossr/src/entries.gen.tsx new file mode 100644 index 000000000..8697395d8 --- /dev/null +++ b/examples/05_nossr/src/entries.gen.tsx @@ -0,0 +1,32 @@ +import { createPages } from 'waku'; +import type { PathsForPages } from 'waku/router'; + +import _Layout, { getConfig as _Layout_getConfig } from './pages/_layout'; +import About, { getConfig as About_getConfig } from './pages/about'; +import Index, { getConfig as Index_getConfig } from './pages/index'; + +const _pages = createPages(async (pagesFns) => [ + pagesFns.createLayout({ + path: '/', + component: _Layout, + ...(await _Layout_getConfig()), + }), + pagesFns.createPage({ + path: '/about', + component: About, + ...(await About_getConfig()), + }), + pagesFns.createPage({ + path: '/', + component: Index, + ...(await Index_getConfig()), + }), +]); + +declare module 'waku/router' { + interface RouteConfig { + paths: PathsForPages; + } +} + +export default _pages; diff --git a/examples/05_nossr/src/pages/_layout.tsx b/examples/05_nossr/src/pages/_layout.tsx index 8fc8d3a3f..fb1a97c9e 100644 --- a/examples/05_nossr/src/pages/_layout.tsx +++ b/examples/05_nossr/src/pages/_layout.tsx @@ -40,5 +40,5 @@ const getData = async () => { export const getConfig = async () => { return { render: 'static', - }; + } as const; }; diff --git a/examples/05_nossr/src/pages/about.tsx b/examples/05_nossr/src/pages/about.tsx index d946bb7ac..15d4c90e1 100644 --- a/examples/05_nossr/src/pages/about.tsx +++ b/examples/05_nossr/src/pages/about.tsx @@ -28,5 +28,5 @@ const getData = async () => { export const getConfig = async () => { return { render: 'static', - }; + } as const; }; diff --git a/examples/05_nossr/src/pages/index.tsx b/examples/05_nossr/src/pages/index.tsx index 68235c94b..889b9d5f4 100644 --- a/examples/05_nossr/src/pages/index.tsx +++ b/examples/05_nossr/src/pages/index.tsx @@ -31,5 +31,5 @@ const getData = async () => { export const getConfig = async () => { return { render: 'static', - }; + } as const; }; diff --git a/examples/10_fs-router/src/entries.gen.tsx b/examples/10_fs-router/src/entries.gen.tsx new file mode 100644 index 000000000..047b56b0d --- /dev/null +++ b/examples/10_fs-router/src/entries.gen.tsx @@ -0,0 +1,38 @@ +import { createPages } from 'waku'; +import type { PathsForPages } from 'waku/router'; + +import _Layout from './pages/_layout'; +import Bar from './pages/bar'; +import FooIndex from './pages/foo/index'; +import Index from './pages/index'; +import Layout from './pages/layout'; +import NestedSlugName from './pages/nested/[name]'; + +const _pages = createPages(async (pagesFns) => [ + pagesFns.createLayout({ path: '/', component: _Layout, render: 'static' }), + pagesFns.createPage({ path: '/bar', component: Bar, render: 'dynamic' }), + pagesFns.createPage({ + path: '/foo/', + component: FooIndex, + render: 'dynamic', + }), + pagesFns.createPage({ path: '/', component: Index, render: 'dynamic' }), + pagesFns.createPage({ + path: '/layout', + component: Layout, + render: 'dynamic', + }), + pagesFns.createPage({ + path: '/nested/[name]', + component: NestedSlugName, + render: 'dynamic', + }), +]); + +declare module 'waku/router' { + interface RouteConfig { + paths: PathsForPages; + } +} + +export default _pages; diff --git a/examples/10_fs-router/src/entries.tsx b/examples/10_fs-router/src/entries.tsx deleted file mode 100644 index eae54b1ec..000000000 --- a/examples/10_fs-router/src/entries.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { fsRouter } from 'waku/router/server'; - -declare global { - interface ImportMeta { - readonly glob: any; - } -} - -export default fsRouter(import.meta.url, (file: string) => - import.meta.glob('./pages/**/*.tsx')[`./pages/${file}`]?.(), -); diff --git a/examples/10_fs-router/src/pages/_layout.tsx b/examples/10_fs-router/src/pages/_layout.tsx index 01c77f7a0..068f570da 100644 --- a/examples/10_fs-router/src/pages/_layout.tsx +++ b/examples/10_fs-router/src/pages/_layout.tsx @@ -67,6 +67,11 @@ const HomeLayout = ({ children }: { children: ReactNode }) => ( Nested / Qux +
  • + }> + A Page named Layout + +
  • {children} diff --git a/examples/10_fs-router/src/pages/layout.tsx b/examples/10_fs-router/src/pages/layout.tsx new file mode 100644 index 000000000..1b26ae095 --- /dev/null +++ b/examples/10_fs-router/src/pages/layout.tsx @@ -0,0 +1,7 @@ +const Layout = () => ( +
    +

    This is a page named Layout

    +
    +); + +export default Layout; diff --git a/packages/waku/src/lib/constants.ts b/packages/waku/src/lib/constants.ts index 69eeeb837..87520cf05 100644 --- a/packages/waku/src/lib/constants.ts +++ b/packages/waku/src/lib/constants.ts @@ -1,3 +1,4 @@ export const EXTENSIONS = ['.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs']; export const SRC_MAIN = 'main'; export const SRC_ENTRIES = 'entries'; +export const SRC_PAGES = 'pages'; diff --git a/packages/waku/src/lib/middleware/dev-server-impl.ts b/packages/waku/src/lib/middleware/dev-server-impl.ts index f0003abc5..e9dcb47ba 100644 --- a/packages/waku/src/lib/middleware/dev-server-impl.ts +++ b/packages/waku/src/lib/middleware/dev-server-impl.ts @@ -27,6 +27,7 @@ import { rscManagedPlugin } from '../plugins/vite-plugin-rsc-managed.js'; import { rscDelegatePlugin } from '../plugins/vite-plugin-rsc-delegate.js'; import { mergeUserViteConfig } from '../utils/merge-vite-config.js'; import type { ClonableModuleNode, Middleware } from './types.js'; +import { fsRouterTypegenPlugin } from '../plugins/vite-plugin-fs-router-typegen.js'; // TODO there is huge room for refactoring in this file @@ -109,6 +110,7 @@ const createMainViteServer = ( rscIndexPlugin(config), rscTransformPlugin({ isClient: true, isBuild: false }), rscHmrPlugin(), + fsRouterTypegenPlugin(config), ], optimizeDeps: { include: ['react-server-dom-webpack/client', 'react-dom'], diff --git a/packages/waku/src/lib/plugins/vite-plugin-fs-router-typegen.ts b/packages/waku/src/lib/plugins/vite-plugin-fs-router-typegen.ts new file mode 100644 index 000000000..1df524e08 --- /dev/null +++ b/packages/waku/src/lib/plugins/vite-plugin-fs-router-typegen.ts @@ -0,0 +1,195 @@ +import type { Plugin } from 'vite'; +import { readdir, writeFile } from 'node:fs/promises'; +import { existsSync, readFileSync } from 'node:fs'; +import path from 'node:path'; +import { SRC_ENTRIES, SRC_PAGES } from '../constants.js'; +import { joinPath } from '../utils/path.js'; + +const srcToName = (src: string) => { + const split = src + .split('/') + .map((part) => (part[0]!.toUpperCase() + part.slice(1)).replace('-', '_')); + + if (src.endsWith('_layout.tsx')) { + return split.slice(0, -1).join('') + '_Layout'; + } else if (src.endsWith('index.tsx')) { + return split.slice(0, -1).join('') + 'Index'; + } else if (split.at(-1)?.startsWith('[...')) { + const fileName = split + .at(-1)! + .replace('.tsx', '') + .replace('[...', '') + .replace(']', ''); + return ( + split.slice(0, -1).join('') + + 'Wild' + + fileName[0]!.toUpperCase() + + fileName.slice(1) + ); + } else if (split.at(-1)?.startsWith('[')) { + const fileName = split + .at(-1)! + .replace('.tsx', '') + .replace('[', '') + .replace(']', ''); + return ( + split.slice(0, -1).join('') + + 'Slug' + + fileName[0]!.toUpperCase() + + fileName.slice(1) + ); + } else { + const fileName = split.at(-1)!.replace('.tsx', ''); + return ( + split.slice(0, -1).join('') + + fileName[0]!.toUpperCase() + + fileName.slice(1) + ); + } +}; + +export const fsRouterTypegenPlugin = (opts: { srcDir: string }): Plugin => { + let entriesFile: string | undefined; + let pagesDir: string | undefined; + let outputFile: string | undefined; + let formatter = (s: string): Promise => Promise.resolve(s); + return { + name: 'vite-plugin-fs-router-typegen', + apply: 'serve', + async configResolved(config) { + pagesDir = joinPath(config.root, opts.srcDir, SRC_PAGES); + entriesFile = joinPath(config.root, opts.srcDir, `${SRC_ENTRIES}.tsx`); + outputFile = joinPath(config.root, opts.srcDir, `${SRC_ENTRIES}.gen.tsx`); + + try { + const prettier = await import('prettier'); + // Get user's prettier config + const config = await prettier.resolveConfig(outputFile); + + formatter = (s) => + prettier.format(s, { ...config, parser: 'typescript' }); + } catch { + // ignore + } + }, + configureServer(server) { + if (!entriesFile || !pagesDir || !outputFile || existsSync(entriesFile)) { + return; + } + + // Recursively collect `.tsx` files in the given directory + const collectFiles = async (dir: string): Promise => { + if (!pagesDir) return []; + let results: string[] = []; + const files = await readdir(dir, { withFileTypes: true }); + + for (const file of files) { + const fullPath = path.join(dir, file.name); + if (file.isDirectory()) { + results = results.concat(await collectFiles(fullPath)); + } else if (file.isFile() && fullPath.endsWith('.tsx')) { + results.push(fullPath.replace(pagesDir, '')); + } + } + return results; + }; + + const fileExportsGetConfig = (filePath: string) => { + if (!pagesDir) return false; + const file = readFileSync(pagesDir + filePath).toString(); + + return ( + file.includes('const getConfig') || + file.includes('function getConfig') + ); + }; + + const generateFile = (filePaths: string[]): string => { + const fileInfo = []; + for (const filePath of filePaths) { + // where to import the component from + const src = filePath.slice(1); + const hasGetConfig = fileExportsGetConfig(filePath); + + if (filePath.endsWith('_layout.tsx')) { + fileInfo.push({ + type: 'layout', + path: filePath.replace('_layout.tsx', ''), + src, + hasGetConfig, + }); + } else if (filePath.endsWith('index.tsx')) { + fileInfo.push({ + type: 'page', + path: filePath.replace('index.tsx', ''), + src, + hasGetConfig, + }); + } else { + fileInfo.push({ + type: 'page', + path: filePath.replace('.tsx', ''), + src, + hasGetConfig, + }); + } + } + + let result = `import { createPages } from 'waku'; +import type { PathsForPages } from 'waku/router';\n\n`; + + for (const file of fileInfo) { + const moduleName = srcToName(file.src); + result += `import ${moduleName}${file.hasGetConfig ? `, { getConfig as ${moduleName}_getConfig }` : ''} from './${SRC_PAGES}/${file.src.replace('.tsx', '')}';\n`; + } + + result += `\nconst _pages = createPages(async (pagesFns) => [\n`; + + for (const file of fileInfo) { + const moduleName = srcToName(file.src); + result += ` pagesFns.${file.type === 'layout' ? 'createLayout' : 'createPage'}({ path: '${file.path}', component: ${moduleName}, ${file.hasGetConfig ? `...(await ${moduleName}_getConfig())` : `render: '${file.type === 'layout' ? 'static' : 'dynamic'}'`} }),\n`; + } + + result += `]); + + declare module 'waku/router' { + interface RouteConfig { + paths: PathsForPages; + } + } + + export default _pages; + `; + + return result; + }; + + const updateGeneratedFile = async () => { + if (!pagesDir || !outputFile) return; + const files = await collectFiles(pagesDir); + const formatted = await formatter(generateFile(files)); + await writeFile(outputFile, formatted, 'utf-8'); + }; + + server.watcher.add(opts.srcDir); + server.watcher.on('change', async (file) => { + if (file === outputFile) return; + + await updateGeneratedFile(); + }); + server.watcher.on('add', async (file) => { + if (file === outputFile) return; + + await updateGeneratedFile(); + }); + + server.watcher.on('', async (file) => { + if (file === outputFile) return; + + await updateGeneratedFile(); + }); + + void updateGeneratedFile(); + }, + }; +}; diff --git a/packages/waku/src/router/create-pages-utils/inferred-path-types.ts b/packages/waku/src/router/create-pages-utils/inferred-path-types.ts index 17ca42a4e..355d1c4d7 100644 --- a/packages/waku/src/router/create-pages-utils/inferred-path-types.ts +++ b/packages/waku/src/router/create-pages-utils/inferred-path-types.ts @@ -1,10 +1,12 @@ import type { PathWithoutSlug } from '../create-pages.js'; import type { Join, ReplaceAll, Split } from '../util-types.js'; +type ReadOnlyStringTupleList = readonly (readonly string[])[]; + type StaticSlugPage = { path: string; render: 'static'; - staticPaths: (string | string[])[]; + staticPaths: readonly string[] | ReadOnlyStringTupleList; }; type DynamicPage = { @@ -36,8 +38,8 @@ type ReplaceSlugSet< * the result will be `/foo/a/b` | `/foo/c/d`. */ type ReplaceHelper< - SplitPath extends string[], - StaticSlugs extends string[], + SplitPath extends readonly string[], + StaticSlugs extends readonly string[], // SlugCountArr is a counter for the number of slugs added to result so far SlugCountArr extends null[] = [], Result extends string[] = [], @@ -64,7 +66,7 @@ type ReplaceHelper< */ type ReplaceTupleStaticPaths< Path extends string, - StaticPathSet extends string[], + StaticPathSet extends readonly string[], > = StaticPathSet extends unknown ? Join, StaticPathSet>, '/'> : never; @@ -73,11 +75,13 @@ type ReplaceTupleStaticPaths< type CollectPathsForStaticSlugPage = Page extends { path: infer Path extends string; render: 'static'; - staticPaths: infer StaticPaths extends string[] | string[][]; + staticPaths: infer StaticPaths extends + | readonly string[] + | ReadOnlyStringTupleList; } - ? StaticPaths extends string[] + ? StaticPaths extends readonly string[] ? ReplaceSlugSet - : StaticPaths extends string[][] + : StaticPaths extends ReadOnlyStringTupleList ? ReplaceTupleStaticPaths : never : never; @@ -113,7 +117,7 @@ export type CollectPaths = EachPage extends unknown export type AnyPage = { path: string; render: 'static' | 'dynamic'; - staticPaths?: string[] | string[][]; + staticPaths?: readonly string[] | readonly (readonly string[])[]; }; /** diff --git a/packages/waku/src/router/create-pages.ts b/packages/waku/src/router/create-pages.ts index 323254266..2acf4936b 100644 --- a/packages/waku/src/router/create-pages.ts +++ b/packages/waku/src/router/create-pages.ts @@ -84,18 +84,18 @@ export type GetSlugs = _GetSlugs; export type StaticSlugRoutePathsTuple< T extends string, Slugs extends unknown[] = GetSlugs, - Result extends string[] = [], + Result extends readonly string[] = [], > = Slugs extends [] ? Result : Slugs extends [infer _, ...infer Rest] - ? StaticSlugRoutePathsTuple + ? StaticSlugRoutePathsTuple : never; type StaticSlugRoutePaths = HasWildcardInPath extends true - ? string[] | string[][] - : StaticSlugRoutePathsTuple extends [string] - ? string[] + ? readonly string[] | readonly string[][] + : StaticSlugRoutePathsTuple extends readonly [string] + ? readonly string[] : StaticSlugRoutePathsTuple[]; /** Remove Slug from Path */ diff --git a/packages/waku/tests/create-pages.test.ts b/packages/waku/tests/create-pages.test.ts index 612b4da85..295947597 100644 --- a/packages/waku/tests/create-pages.test.ts +++ b/packages/waku/tests/create-pages.test.ts @@ -875,7 +875,7 @@ describe('createPages', () => { render: 'static', path: '/test/[a]/[b]', // @ts-expect-error: staticPaths should be an array of strings or [string, string][] - staticPaths: [['w']], + staticPaths: [['w']] as const, component: () => null, }), ]); From f0a965965f7ba4d96b34be139831fc61eae12480 Mon Sep 17 00:00:00 2001 From: Tyler <26290074+thegitduck@users.noreply.github.com> Date: Thu, 26 Sep 2024 23:12:15 -0700 Subject: [PATCH 2/4] fix: changes from review --- .../src/entries.gen.tsx | 0 examples/01_template/src/pages/_layout.tsx | 2 +- examples/01_template/src/pages/about.tsx | 2 +- examples/01_template/src/pages/index.tsx | 2 +- examples/10_fs-router/src/entries.gen.tsx | 38 -------------- examples/10_fs-router/src/entries.tsx | 11 ++++ examples/10_fs-router/src/pages/layout.tsx | 7 --- packages/waku/src/lib/constants.ts | 1 - .../plugins/vite-plugin-fs-router-typegen.ts | 52 +++++++++++-------- 9 files changed, 43 insertions(+), 72 deletions(-) rename examples/{05_nossr => 01_template}/src/entries.gen.tsx (100%) delete mode 100644 examples/10_fs-router/src/entries.gen.tsx create mode 100644 examples/10_fs-router/src/entries.tsx delete mode 100644 examples/10_fs-router/src/pages/layout.tsx diff --git a/examples/05_nossr/src/entries.gen.tsx b/examples/01_template/src/entries.gen.tsx similarity index 100% rename from examples/05_nossr/src/entries.gen.tsx rename to examples/01_template/src/entries.gen.tsx diff --git a/examples/01_template/src/pages/_layout.tsx b/examples/01_template/src/pages/_layout.tsx index 8fc8d3a3f..fb1a97c9e 100644 --- a/examples/01_template/src/pages/_layout.tsx +++ b/examples/01_template/src/pages/_layout.tsx @@ -40,5 +40,5 @@ const getData = async () => { export const getConfig = async () => { return { render: 'static', - }; + } as const; }; diff --git a/examples/01_template/src/pages/about.tsx b/examples/01_template/src/pages/about.tsx index d946bb7ac..15d4c90e1 100644 --- a/examples/01_template/src/pages/about.tsx +++ b/examples/01_template/src/pages/about.tsx @@ -28,5 +28,5 @@ const getData = async () => { export const getConfig = async () => { return { render: 'static', - }; + } as const; }; diff --git a/examples/01_template/src/pages/index.tsx b/examples/01_template/src/pages/index.tsx index 68235c94b..889b9d5f4 100644 --- a/examples/01_template/src/pages/index.tsx +++ b/examples/01_template/src/pages/index.tsx @@ -31,5 +31,5 @@ const getData = async () => { export const getConfig = async () => { return { render: 'static', - }; + } as const; }; diff --git a/examples/10_fs-router/src/entries.gen.tsx b/examples/10_fs-router/src/entries.gen.tsx deleted file mode 100644 index 047b56b0d..000000000 --- a/examples/10_fs-router/src/entries.gen.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { createPages } from 'waku'; -import type { PathsForPages } from 'waku/router'; - -import _Layout from './pages/_layout'; -import Bar from './pages/bar'; -import FooIndex from './pages/foo/index'; -import Index from './pages/index'; -import Layout from './pages/layout'; -import NestedSlugName from './pages/nested/[name]'; - -const _pages = createPages(async (pagesFns) => [ - pagesFns.createLayout({ path: '/', component: _Layout, render: 'static' }), - pagesFns.createPage({ path: '/bar', component: Bar, render: 'dynamic' }), - pagesFns.createPage({ - path: '/foo/', - component: FooIndex, - render: 'dynamic', - }), - pagesFns.createPage({ path: '/', component: Index, render: 'dynamic' }), - pagesFns.createPage({ - path: '/layout', - component: Layout, - render: 'dynamic', - }), - pagesFns.createPage({ - path: '/nested/[name]', - component: NestedSlugName, - render: 'dynamic', - }), -]); - -declare module 'waku/router' { - interface RouteConfig { - paths: PathsForPages; - } -} - -export default _pages; diff --git a/examples/10_fs-router/src/entries.tsx b/examples/10_fs-router/src/entries.tsx new file mode 100644 index 000000000..eae54b1ec --- /dev/null +++ b/examples/10_fs-router/src/entries.tsx @@ -0,0 +1,11 @@ +import { fsRouter } from 'waku/router/server'; + +declare global { + interface ImportMeta { + readonly glob: any; + } +} + +export default fsRouter(import.meta.url, (file: string) => + import.meta.glob('./pages/**/*.tsx')[`./pages/${file}`]?.(), +); diff --git a/examples/10_fs-router/src/pages/layout.tsx b/examples/10_fs-router/src/pages/layout.tsx deleted file mode 100644 index 1b26ae095..000000000 --- a/examples/10_fs-router/src/pages/layout.tsx +++ /dev/null @@ -1,7 +0,0 @@ -const Layout = () => ( -
    -

    This is a page named Layout

    -
    -); - -export default Layout; diff --git a/packages/waku/src/lib/constants.ts b/packages/waku/src/lib/constants.ts index 87520cf05..69eeeb837 100644 --- a/packages/waku/src/lib/constants.ts +++ b/packages/waku/src/lib/constants.ts @@ -1,4 +1,3 @@ export const EXTENSIONS = ['.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs']; export const SRC_MAIN = 'main'; export const SRC_ENTRIES = 'entries'; -export const SRC_PAGES = 'pages'; diff --git a/packages/waku/src/lib/plugins/vite-plugin-fs-router-typegen.ts b/packages/waku/src/lib/plugins/vite-plugin-fs-router-typegen.ts index 1df524e08..5edd34441 100644 --- a/packages/waku/src/lib/plugins/vite-plugin-fs-router-typegen.ts +++ b/packages/waku/src/lib/plugins/vite-plugin-fs-router-typegen.ts @@ -2,21 +2,24 @@ import type { Plugin } from 'vite'; import { readdir, writeFile } from 'node:fs/promises'; import { existsSync, readFileSync } from 'node:fs'; import path from 'node:path'; -import { SRC_ENTRIES, SRC_PAGES } from '../constants.js'; +import { SRC_ENTRIES } from '../constants.js'; import { joinPath } from '../utils/path.js'; +const SRC_PAGES = 'pages'; + const srcToName = (src: string) => { const split = src .split('/') - .map((part) => (part[0]!.toUpperCase() + part.slice(1)).replace('-', '_')); + .map((part) => part[0]!.toUpperCase() + part.slice(1)); - if (src.endsWith('_layout.tsx')) { + if (split.at(-1) === '_layout.tsx') { return split.slice(0, -1).join('') + '_Layout'; - } else if (src.endsWith('index.tsx')) { + } else if (split.at(-1) === 'index.tsx') { return split.slice(0, -1).join('') + 'Index'; } else if (split.at(-1)?.startsWith('[...')) { const fileName = split .at(-1)! + .replace('-', '_') .replace('.tsx', '') .replace('[...', '') .replace(']', ''); @@ -29,6 +32,7 @@ const srcToName = (src: string) => { } else if (split.at(-1)?.startsWith('[')) { const fileName = split .at(-1)! + .replace('-', '_') .replace('.tsx', '') .replace('[', '') .replace(']', ''); @@ -39,7 +43,7 @@ const srcToName = (src: string) => { fileName.slice(1) ); } else { - const fileName = split.at(-1)!.replace('.tsx', ''); + const fileName = split.at(-1)!.replace('-', '_').replace('.tsx', ''); return ( split.slice(0, -1).join('') + fileName[0]!.toUpperCase() + @@ -49,7 +53,7 @@ const srcToName = (src: string) => { }; export const fsRouterTypegenPlugin = (opts: { srcDir: string }): Plugin => { - let entriesFile: string | undefined; + let entriesFilePossibilities: string[] | undefined; let pagesDir: string | undefined; let outputFile: string | undefined; let formatter = (s: string): Promise => Promise.resolve(s); @@ -58,7 +62,9 @@ export const fsRouterTypegenPlugin = (opts: { srcDir: string }): Plugin => { apply: 'serve', async configResolved(config) { pagesDir = joinPath(config.root, opts.srcDir, SRC_PAGES); - entriesFile = joinPath(config.root, opts.srcDir, `${SRC_ENTRIES}.tsx`); + entriesFilePossibilities = ['tsx', 'ts', 'js', 'jsx'].map((ext) => + joinPath(config.root, opts.srcDir, `${SRC_ENTRIES}.${ext}`), + ); outputFile = joinPath(config.root, opts.srcDir, `${SRC_ENTRIES}.gen.tsx`); try { @@ -73,21 +79,27 @@ export const fsRouterTypegenPlugin = (opts: { srcDir: string }): Plugin => { } }, configureServer(server) { - if (!entriesFile || !pagesDir || !outputFile || existsSync(entriesFile)) { + if ( + !entriesFilePossibilities || + !pagesDir || + !outputFile || + entriesFilePossibilities.some((entriesFile) => existsSync(entriesFile)) + ) { return; } // Recursively collect `.tsx` files in the given directory const collectFiles = async (dir: string): Promise => { if (!pagesDir) return []; - let results: string[] = []; - const files = await readdir(dir, { withFileTypes: true }); + const results: string[] = []; + const files = await readdir(dir, { + withFileTypes: true, + recursive: true, + }); for (const file of files) { const fullPath = path.join(dir, file.name); - if (file.isDirectory()) { - results = results.concat(await collectFiles(fullPath)); - } else if (file.isFile() && fullPath.endsWith('.tsx')) { + if (fullPath.endsWith('.tsx')) { results.push(fullPath.replace(pagesDir, '')); } } @@ -111,14 +123,14 @@ export const fsRouterTypegenPlugin = (opts: { srcDir: string }): Plugin => { const src = filePath.slice(1); const hasGetConfig = fileExportsGetConfig(filePath); - if (filePath.endsWith('_layout.tsx')) { + if (filePath === '/_layout.tsx') { fileInfo.push({ type: 'layout', path: filePath.replace('_layout.tsx', ''), src, hasGetConfig, }); - } else if (filePath.endsWith('index.tsx')) { + } else if (filePath === '/index.tsx') { fileInfo.push({ type: 'page', path: filePath.replace('index.tsx', ''), @@ -173,18 +185,12 @@ import type { PathsForPages } from 'waku/router';\n\n`; server.watcher.add(opts.srcDir); server.watcher.on('change', async (file) => { - if (file === outputFile) return; + if (!outputFile || outputFile.endsWith(file)) return; await updateGeneratedFile(); }); server.watcher.on('add', async (file) => { - if (file === outputFile) return; - - await updateGeneratedFile(); - }); - - server.watcher.on('', async (file) => { - if (file === outputFile) return; + if (!outputFile || outputFile.endsWith(file)) return; await updateGeneratedFile(); }); From 25c0ed19c28bb28ea8637fa49d8892003fc1afdd Mon Sep 17 00:00:00 2001 From: Tyler <26290074+thegitduck@users.noreply.github.com> Date: Sat, 28 Sep 2024 00:03:59 -0700 Subject: [PATCH 3/4] fix: changes from review --- .gitignore | 1 + .../ssr-catch-error/src/pages/_layout.tsx | 2 +- .../ssr-catch-error/src/pages/index.tsx | 2 +- .../ssr-catch-error/src/pages/invalid.tsx | 2 +- examples/01_template/src/entries.gen.tsx | 32 ------------------- examples/03_demo/src/pages/[slug].tsx | 2 +- examples/03_demo/src/pages/_layout.tsx | 2 +- examples/03_demo/src/pages/index.tsx | 2 +- examples/04_cssmodules/src/pages/_layout.tsx | 2 +- examples/04_cssmodules/src/pages/about.tsx | 2 +- examples/04_cssmodules/src/pages/index.tsx | 2 +- examples/10_fs-router/src/pages/_layout.tsx | 5 --- .../plugins/vite-plugin-fs-router-typegen.ts | 10 +++--- 13 files changed, 14 insertions(+), 52 deletions(-) delete mode 100644 examples/01_template/src/entries.gen.tsx diff --git a/.gitignore b/.gitignore index 410230c5c..77c0d84c7 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ dist .cache .nvmrc .node-version +entries.gen.tsx diff --git a/e2e/fixtures/ssr-catch-error/src/pages/_layout.tsx b/e2e/fixtures/ssr-catch-error/src/pages/_layout.tsx index 6b84445c1..484b7979c 100644 --- a/e2e/fixtures/ssr-catch-error/src/pages/_layout.tsx +++ b/e2e/fixtures/ssr-catch-error/src/pages/_layout.tsx @@ -19,5 +19,5 @@ export default async function RootLayout({ export const getConfig = async () => { return { render: 'static', - }; + } as const; }; diff --git a/e2e/fixtures/ssr-catch-error/src/pages/index.tsx b/e2e/fixtures/ssr-catch-error/src/pages/index.tsx index 86a3b5050..8e16eedda 100644 --- a/e2e/fixtures/ssr-catch-error/src/pages/index.tsx +++ b/e2e/fixtures/ssr-catch-error/src/pages/index.tsx @@ -12,5 +12,5 @@ export default async function HomePage() { export const getConfig = async () => { return { render: 'static', - }; + } as const; }; diff --git a/e2e/fixtures/ssr-catch-error/src/pages/invalid.tsx b/e2e/fixtures/ssr-catch-error/src/pages/invalid.tsx index f1bda111c..2293ac9d9 100644 --- a/e2e/fixtures/ssr-catch-error/src/pages/invalid.tsx +++ b/e2e/fixtures/ssr-catch-error/src/pages/invalid.tsx @@ -9,5 +9,5 @@ export default async function InvalidPage() { export const getConfig = async () => { return { render: 'dynamic', - }; + } as const; }; diff --git a/examples/01_template/src/entries.gen.tsx b/examples/01_template/src/entries.gen.tsx deleted file mode 100644 index 8697395d8..000000000 --- a/examples/01_template/src/entries.gen.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { createPages } from 'waku'; -import type { PathsForPages } from 'waku/router'; - -import _Layout, { getConfig as _Layout_getConfig } from './pages/_layout'; -import About, { getConfig as About_getConfig } from './pages/about'; -import Index, { getConfig as Index_getConfig } from './pages/index'; - -const _pages = createPages(async (pagesFns) => [ - pagesFns.createLayout({ - path: '/', - component: _Layout, - ...(await _Layout_getConfig()), - }), - pagesFns.createPage({ - path: '/about', - component: About, - ...(await About_getConfig()), - }), - pagesFns.createPage({ - path: '/', - component: Index, - ...(await Index_getConfig()), - }), -]); - -declare module 'waku/router' { - interface RouteConfig { - paths: PathsForPages; - } -} - -export default _pages; diff --git a/examples/03_demo/src/pages/[slug].tsx b/examples/03_demo/src/pages/[slug].tsx index 210ddb723..5e267d4df 100644 --- a/examples/03_demo/src/pages/[slug].tsx +++ b/examples/03_demo/src/pages/[slug].tsx @@ -78,5 +78,5 @@ export const getConfig = async () => { return { render: 'static', staticPaths: pokemonPaths, - }; + } as const; }; diff --git a/examples/03_demo/src/pages/_layout.tsx b/examples/03_demo/src/pages/_layout.tsx index 5fba424c4..716d5e113 100644 --- a/examples/03_demo/src/pages/_layout.tsx +++ b/examples/03_demo/src/pages/_layout.tsx @@ -40,5 +40,5 @@ const getData = async () => { export const getConfig = async () => { return { render: 'static', - }; + } as const; }; diff --git a/examples/03_demo/src/pages/index.tsx b/examples/03_demo/src/pages/index.tsx index 393d6c37d..495e28ea5 100644 --- a/examples/03_demo/src/pages/index.tsx +++ b/examples/03_demo/src/pages/index.tsx @@ -51,5 +51,5 @@ export default async function HomePage() { export const getConfig = async () => { return { render: 'dynamic', - }; + } as const; }; diff --git a/examples/04_cssmodules/src/pages/_layout.tsx b/examples/04_cssmodules/src/pages/_layout.tsx index 9fbb2f85d..c7d19ed12 100644 --- a/examples/04_cssmodules/src/pages/_layout.tsx +++ b/examples/04_cssmodules/src/pages/_layout.tsx @@ -38,5 +38,5 @@ const getData = async () => { export const getConfig = async () => { return { render: 'static', - }; + } as const; }; diff --git a/examples/04_cssmodules/src/pages/about.tsx b/examples/04_cssmodules/src/pages/about.tsx index d150ce754..632198ba2 100644 --- a/examples/04_cssmodules/src/pages/about.tsx +++ b/examples/04_cssmodules/src/pages/about.tsx @@ -30,5 +30,5 @@ const getData = async () => { export const getConfig = async () => { return { render: 'static', - }; + } as const; }; diff --git a/examples/04_cssmodules/src/pages/index.tsx b/examples/04_cssmodules/src/pages/index.tsx index 36e155566..b281805d4 100644 --- a/examples/04_cssmodules/src/pages/index.tsx +++ b/examples/04_cssmodules/src/pages/index.tsx @@ -32,5 +32,5 @@ const getData = async () => { export const getConfig = async () => { return { render: 'static', - }; + } as const; }; diff --git a/examples/10_fs-router/src/pages/_layout.tsx b/examples/10_fs-router/src/pages/_layout.tsx index 068f570da..01c77f7a0 100644 --- a/examples/10_fs-router/src/pages/_layout.tsx +++ b/examples/10_fs-router/src/pages/_layout.tsx @@ -67,11 +67,6 @@ const HomeLayout = ({ children }: { children: ReactNode }) => ( Nested / Qux -
  • - }> - A Page named Layout - -
  • {children} diff --git a/packages/waku/src/lib/plugins/vite-plugin-fs-router-typegen.ts b/packages/waku/src/lib/plugins/vite-plugin-fs-router-typegen.ts index 5edd34441..cd6293691 100644 --- a/packages/waku/src/lib/plugins/vite-plugin-fs-router-typegen.ts +++ b/packages/waku/src/lib/plugins/vite-plugin-fs-router-typegen.ts @@ -1,8 +1,7 @@ import type { Plugin } from 'vite'; import { readdir, writeFile } from 'node:fs/promises'; import { existsSync, readFileSync } from 'node:fs'; -import path from 'node:path'; -import { SRC_ENTRIES } from '../constants.js'; +import { SRC_ENTRIES, EXTENSIONS } from '../constants.js'; import { joinPath } from '../utils/path.js'; const SRC_PAGES = 'pages'; @@ -62,7 +61,7 @@ export const fsRouterTypegenPlugin = (opts: { srcDir: string }): Plugin => { apply: 'serve', async configResolved(config) { pagesDir = joinPath(config.root, opts.srcDir, SRC_PAGES); - entriesFilePossibilities = ['tsx', 'ts', 'js', 'jsx'].map((ext) => + entriesFilePossibilities = EXTENSIONS.map((ext) => joinPath(config.root, opts.srcDir, `${SRC_ENTRIES}.${ext}`), ); outputFile = joinPath(config.root, opts.srcDir, `${SRC_ENTRIES}.gen.tsx`); @@ -98,9 +97,8 @@ export const fsRouterTypegenPlugin = (opts: { srcDir: string }): Plugin => { }); for (const file of files) { - const fullPath = path.join(dir, file.name); - if (fullPath.endsWith('.tsx')) { - results.push(fullPath.replace(pagesDir, '')); + if (file.name.endsWith('.tsx')) { + results.push('/' + file.name); } } return results; From 7d4044268c6291cb92219ae83ee673f11a548bdd Mon Sep 17 00:00:00 2001 From: Tyler <26290074+thegitduck@users.noreply.github.com> Date: Sat, 28 Sep 2024 14:00:10 -0700 Subject: [PATCH 4/4] fix: typegen not finding entries.tsx file --- .../waku/src/lib/plugins/vite-plugin-fs-router-typegen.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/waku/src/lib/plugins/vite-plugin-fs-router-typegen.ts b/packages/waku/src/lib/plugins/vite-plugin-fs-router-typegen.ts index cd6293691..73dd33d9f 100644 --- a/packages/waku/src/lib/plugins/vite-plugin-fs-router-typegen.ts +++ b/packages/waku/src/lib/plugins/vite-plugin-fs-router-typegen.ts @@ -62,7 +62,7 @@ export const fsRouterTypegenPlugin = (opts: { srcDir: string }): Plugin => { async configResolved(config) { pagesDir = joinPath(config.root, opts.srcDir, SRC_PAGES); entriesFilePossibilities = EXTENSIONS.map((ext) => - joinPath(config.root, opts.srcDir, `${SRC_ENTRIES}.${ext}`), + joinPath(config.root, opts.srcDir, SRC_ENTRIES + ext), ); outputFile = joinPath(config.root, opts.srcDir, `${SRC_ENTRIES}.gen.tsx`); @@ -82,7 +82,10 @@ export const fsRouterTypegenPlugin = (opts: { srcDir: string }): Plugin => { !entriesFilePossibilities || !pagesDir || !outputFile || - entriesFilePossibilities.some((entriesFile) => existsSync(entriesFile)) + entriesFilePossibilities.some((entriesFile) => + existsSync(entriesFile), + ) || + !existsSync(pagesDir) ) { return; }