Skip to content
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

feat: entries gen for fs router projects #886

Merged
merged 5 commits into from
Sep 29, 2024
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ dist
.cache
.nvmrc
.node-version
entries.gen.tsx
2 changes: 1 addition & 1 deletion e2e/fixtures/ssr-catch-error/src/pages/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ export default async function RootLayout({
export const getConfig = async () => {
return {
render: 'static',
};
} as const;
};
2 changes: 1 addition & 1 deletion e2e/fixtures/ssr-catch-error/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ export default async function HomePage() {
export const getConfig = async () => {
return {
render: 'static',
};
} as const;
};
2 changes: 1 addition & 1 deletion e2e/fixtures/ssr-catch-error/src/pages/invalid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ export default async function InvalidPage() {
export const getConfig = async () => {
return {
render: 'dynamic',
};
} as const;
};
2 changes: 1 addition & 1 deletion examples/01_template/src/pages/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,5 @@ const getData = async () => {
export const getConfig = async () => {
return {
render: 'static',
};
} as const;
tylerlaws0n marked this conversation as resolved.
Show resolved Hide resolved
};
2 changes: 1 addition & 1 deletion examples/01_template/src/pages/about.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,5 @@ const getData = async () => {
export const getConfig = async () => {
return {
render: 'static',
};
} as const;
};
2 changes: 1 addition & 1 deletion examples/01_template/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@ const getData = async () => {
export const getConfig = async () => {
return {
render: 'static',
};
} as const;
};
2 changes: 1 addition & 1 deletion examples/03_demo/src/pages/[slug].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,5 @@ export const getConfig = async () => {
return {
render: 'static',
staticPaths: pokemonPaths,
};
} as const;
};
2 changes: 1 addition & 1 deletion examples/03_demo/src/pages/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,5 @@ const getData = async () => {
export const getConfig = async () => {
return {
render: 'static',
};
} as const;
};
2 changes: 1 addition & 1 deletion examples/03_demo/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,5 @@ export default async function HomePage() {
export const getConfig = async () => {
return {
render: 'dynamic',
};
} as const;
};
2 changes: 1 addition & 1 deletion examples/04_cssmodules/src/pages/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,5 @@ const getData = async () => {
export const getConfig = async () => {
return {
render: 'static',
};
} as const;
};
2 changes: 1 addition & 1 deletion examples/04_cssmodules/src/pages/about.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,5 @@ const getData = async () => {
export const getConfig = async () => {
return {
render: 'static',
};
} as const;
};
2 changes: 1 addition & 1 deletion examples/04_cssmodules/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@ const getData = async () => {
export const getConfig = async () => {
return {
render: 'static',
};
} as const;
};
2 changes: 1 addition & 1 deletion examples/05_nossr/src/pages/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,5 @@ const getData = async () => {
export const getConfig = async () => {
return {
render: 'static',
};
} as const;
dai-shi marked this conversation as resolved.
Show resolved Hide resolved
};
2 changes: 1 addition & 1 deletion examples/05_nossr/src/pages/about.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,5 @@ const getData = async () => {
export const getConfig = async () => {
return {
render: 'static',
};
} as const;
tylerlaws0n marked this conversation as resolved.
Show resolved Hide resolved
};
2 changes: 1 addition & 1 deletion examples/05_nossr/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@ const getData = async () => {
export const getConfig = async () => {
return {
render: 'static',
};
} as const;
};
2 changes: 2 additions & 0 deletions packages/waku/src/lib/middleware/dev-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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'],
Expand Down
202 changes: 202 additions & 0 deletions packages/waku/src/lib/plugins/vite-plugin-fs-router-typegen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import type { Plugin } from 'vite';
import { readdir, writeFile } from 'node:fs/promises';
import { existsSync, readFileSync } from 'node:fs';
import { SRC_ENTRIES, EXTENSIONS } 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));

if (split.at(-1) === '_layout.tsx') {
return split.slice(0, -1).join('') + '_Layout';
} 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(']', '');
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('-', '_')
.replace('.tsx', '')
.replace('[', '')
.replace(']', '');
return (
split.slice(0, -1).join('') +
'Slug' +
fileName[0]!.toUpperCase() +
fileName.slice(1)
);
} else {
const fileName = split.at(-1)!.replace('-', '_').replace('.tsx', '');
return (
split.slice(0, -1).join('') +
fileName[0]!.toUpperCase() +
fileName.slice(1)
);
}
};

export const fsRouterTypegenPlugin = (opts: { srcDir: string }): Plugin => {
let entriesFilePossibilities: string[] | undefined;
let pagesDir: string | undefined;
let outputFile: string | undefined;
let formatter = (s: string): Promise<string> => Promise.resolve(s);
return {
name: 'vite-plugin-fs-router-typegen',
apply: 'serve',
async configResolved(config) {
pagesDir = joinPath(config.root, opts.srcDir, SRC_PAGES);
entriesFilePossibilities = EXTENSIONS.map((ext) =>
joinPath(config.root, opts.srcDir, SRC_ENTRIES + ext),
);
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 (
!entriesFilePossibilities ||
!pagesDir ||
!outputFile ||
entriesFilePossibilities.some((entriesFile) =>
existsSync(entriesFile),
) ||
!existsSync(pagesDir)
) {
return;
}

// Recursively collect `.tsx` files in the given directory
tylerlaws0n marked this conversation as resolved.
Show resolved Hide resolved
const collectFiles = async (dir: string): Promise<string[]> => {
if (!pagesDir) return [];
const results: string[] = [];
const files = await readdir(dir, {
withFileTypes: true,
recursive: true,
});

for (const file of files) {
if (file.name.endsWith('.tsx')) {
results.push('/' + file.name);
}
}
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 === '/_layout.tsx') {
fileInfo.push({
type: 'layout',
path: filePath.replace('_layout.tsx', ''),
src,
hasGetConfig,
});
} else if (filePath === '/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<typeof _pages>;
}
}

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 (!outputFile || outputFile.endsWith(file)) return;

await updateGeneratedFile();
});
server.watcher.on('add', async (file) => {
if (!outputFile || outputFile.endsWith(file)) return;

await updateGeneratedFile();
});

void updateGeneratedFile();
},
};
};
20 changes: 12 additions & 8 deletions packages/waku/src/router/create-pages-utils/inferred-path-types.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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[] = [],
Expand All @@ -64,7 +66,7 @@ type ReplaceHelper<
*/
type ReplaceTupleStaticPaths<
Path extends string,
StaticPathSet extends string[],
StaticPathSet extends readonly string[],
> = StaticPathSet extends unknown
? Join<ReplaceHelper<Split<Path, '/'>, StaticPathSet>, '/'>
: never;
Expand All @@ -73,11 +75,13 @@ type ReplaceTupleStaticPaths<
type CollectPathsForStaticSlugPage<Page extends StaticSlugPage> = 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<Path, StaticPaths[number]>
: StaticPaths extends string[][]
: StaticPaths extends ReadOnlyStringTupleList
? ReplaceTupleStaticPaths<Path, StaticPaths[number]>
: never
: never;
Expand Down Expand Up @@ -113,7 +117,7 @@ export type CollectPaths<EachPage extends AnyPage> = EachPage extends unknown
export type AnyPage = {
path: string;
render: 'static' | 'dynamic';
staticPaths?: string[] | string[][];
staticPaths?: readonly string[] | readonly (readonly string[])[];
};

/**
Expand Down
Loading
Loading