rwsdk-tools
Version:
A collection of utility tools for working with the RWSDK (Redwood SDK)
200 lines (168 loc) • 6.37 kB
text/typescript
import * as fs from 'fs/promises';
import * as path from 'path';
interface RouteMatch {
route: string;
startIndex: number;
endIndex: number;
}
interface ImportInfo {
originalName?: string;
path: string;
isRouteFile?: boolean;
}
async function findMatchingBracket(content: string, startIndex: number): Promise<number> {
let count = 1;
let i = startIndex;
while (count > 0 && i < content.length) {
if (content[i] === '[') count++;
if (content[i] === ']') count--;
i++;
}
return i - 1;
}
async function resolveImportPath(importPath: string, currentFilePath?: string): Promise<string> {
// Convert relative path to absolute
let absolutePath = importPath;
if (importPath.startsWith('@/')) {
absolutePath = path.join(process.cwd(), 'src', importPath.slice(2));
} else if (importPath.startsWith('./') || importPath.startsWith('../')) {
const basePath = currentFilePath ? path.dirname(currentFilePath) : process.cwd();
absolutePath = path.join(basePath, importPath);
}
// Try different extensions if no extension is provided
if (!absolutePath.endsWith('.ts') && !absolutePath.endsWith('.tsx')) {
// Try .ts extension first
try {
await fs.access(absolutePath + '.ts');
return absolutePath + '.ts';
} catch (error) {
// Try .tsx extension
try {
await fs.access(absolutePath + '.tsx');
return absolutePath + '.tsx';
} catch (error) {
// If both fail, return the original path with .ts extension
return absolutePath + '.ts';
}
}
}
return absolutePath;
}
async function findImportedRoutes(content: string, filePath?: string): Promise<string[]> {
const importRegex = /import\s*{\s*([^}]+)\s*}\s*from\s*["']([^"']+)["']/g;
const spreadRegex = /\.\.\.([\w]+)\s*,?/g;
const prefixVarRegex = /prefix\(\s*["']([^"']+)["']\s*,\s*([\w]+)\s*\)/g;
const importedRoutes: string[] = [];
let match;
// Find all imports
const imports = new Map<string, ImportInfo>();
while ((match = importRegex.exec(content)) !== null) {
const [, importNames, importPath] = match;
const names = importNames.split(',').map(n => n.trim());
for (const name of names) {
if (name.includes(' as ')) {
const [originalName, alias] = name.split(' as ').map(n => n.trim());
imports.set(alias, { originalName, path: importPath });
} else {
imports.set(name, { path: importPath });
}
}
}
// Process spread operators (both inside arrays and directly in the render function)
while ((match = spreadRegex.exec(content)) !== null) {
const variableName = match[1];
const importInfo = imports.get(variableName);
if (importInfo) {
try {
const absolutePath = await resolveImportPath(importInfo.path, filePath);
const importedContent = await fs.readFile(absolutePath, 'utf-8');
const newRoutes = await extractRoutesFromContent(importedContent, '', absolutePath);
importedRoutes.push(...newRoutes);
} catch (error) {
console.warn(`Warning: Could not process imported routes from ${importInfo.path}:`, error);
}
}
}
// Process prefix with variable references (e.g., prefix("/user", userRoutes))
while ((match = prefixVarRegex.exec(content)) !== null) {
const prefixPath = match[1];
const variableName = match[2];
const importInfo = imports.get(variableName);
if (importInfo) {
try {
const absolutePath = await resolveImportPath(importInfo.path, filePath);
const importedContent = await fs.readFile(absolutePath, 'utf-8');
const newRoutes = await extractRoutesFromContent(importedContent, prefixPath, absolutePath);
importedRoutes.push(...newRoutes);
} catch (error) {
console.warn(`Warning: Could not process imported routes from ${importInfo.path} with prefix ${prefixPath}:`, error);
}
}
}
return importedRoutes;
}
async function extractRoutesFromContent(content: string, prefix = '', filePath?: string): Promise<string[]> {
const routes: string[] = [];
const routeRegex = /route\("([^"]+)"/g;
const indexRegex = /index\(/g;
const prefixRegex = /prefix\("([^"]+)",\s*\[/g;
let match;
// Process imported routes first
const importedRoutes = await findImportedRoutes(content, filePath);
routes.push(...importedRoutes);
// Find all prefixed route blocks
while ((match = prefixRegex.exec(content)) !== null) {
const prefixPath = match[1];
const startIndex = match.index + match[0].length;
const endIndex = await findMatchingBracket(content, startIndex);
const nestedContent = content.substring(startIndex, endIndex);
// Recursively process nested routes with combined prefix
const nestedRoutes = await extractRoutesFromContent(
nestedContent,
prefix + prefixPath,
filePath
);
routes.push(...nestedRoutes);
}
// Find all regular routes
while ((match = routeRegex.exec(content)) !== null) {
// Skip if this route is part of a prefix block (we already processed those)
const beforeMatch = content.substring(0, match.index);
const lastPrefixIndex = beforeMatch.lastIndexOf('prefix(');
const lastBracketIndex = beforeMatch.lastIndexOf(']');
if (lastPrefixIndex === -1 || lastBracketIndex > lastPrefixIndex) {
const route = prefix + match[1];
routes.push(route);
}
}
// Find index routes (which map to "/")
if (indexRegex.test(content)) {
routes.push(prefix + "/");
}
// Remove duplicates
return [...new Set(routes)];
}
async function generateLinksFile(routes: string[]) {
const content = `import { defineLinks } from "rwsdk/router";
export const link = defineLinks(${JSON.stringify(routes, null, 2)});
`;
await fs.writeFile(
path.resolve(process.cwd(), 'src/app/shared/links.ts'),
content,
'utf-8'
);
}
async function main() {
try {
const workerPath = path.resolve(process.cwd(), 'src/worker.tsx');
const workerContent = await fs.readFile(workerPath, 'utf-8');
const routes = await extractRoutesFromContent(workerContent, '', workerPath);
await generateLinksFile(routes);
console.log('✨ Successfully generated links.ts');
} catch (error) {
console.error('Error generating routes:', error);
process.exit(1);
}
}
main();