Skip to content

Commit

Permalink
fix: changes from review
Browse files Browse the repository at this point in the history
  • Loading branch information
tylersayshi committed Sep 25, 2024
1 parent c67ee86 commit 95a2f03
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 185 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { createPages } from 'waku';
import type { PathsForPages } from 'waku/router';

import Layout, { getConfig as Layout_getConfig } from './src/pages/_layout';
import About, { getConfig as About_getConfig } from './src/pages/about';
import Index, { getConfig as Index_getConfig } from './src/pages/index';
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({
Expand Down
1 change: 1 addition & 0 deletions packages/waku/src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -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';
4 changes: 2 additions & 2 deletions packages/waku/src/lib/middleware/dev-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +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 { typegenPlugin } from '../plugins/vite-plugin-typegen.js';
import { typegenPlugin } from '../plugins/vite-plugin-fs-router-typegen.js';

// TODO there is huge room for refactoring in this file

Expand Down Expand Up @@ -110,7 +110,7 @@ const createMainViteServer = (
rscIndexPlugin(config),
rscTransformPlugin({ isClient: true, isBuild: false }),
rscHmrPlugin(),
typegenPlugin(),
typegenPlugin(config),
],
optimizeDeps: {
include: ['react-server-dom-webpack/client', 'react-dom'],
Expand Down
194 changes: 194 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,194 @@
import type { Plugin } from 'vite';
import { promises, readFileSync, existsSync } 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 typegenPlugin = (opts: { srcDir: string }): Plugin => {
let entriesFile: 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);
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<string[]> => {
if (!pagesDir) return [];
let results: string[] = [];
const files = await promises.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<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 promises.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();
},
};
};
Loading

0 comments on commit 95a2f03

Please sign in to comment.