vite-plugin-react-routes
Version:
A vite plugin support setup React Router by JSON config file.
191 lines (176 loc) • 6.25 kB
JavaScript
const fs = require('fs');
const path = require('path');
const vite = require('vite');
const esbuild = require('esbuild');
const jiti = require('jiti');
const upperCamelcase = require('uppercamelcase');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e["default"] : e; }
const fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
const path__default = /*#__PURE__*/_interopDefaultLegacy(path);
const jiti__default = /*#__PURE__*/_interopDefaultLegacy(jiti);
const upperCamelcase__default = /*#__PURE__*/_interopDefaultLegacy(upperCamelcase);
function winPath(path2) {
const isExtendedLengthPath = /^\\\\\?\\/.test(path2);
if (isExtendedLengthPath) {
return path2;
}
return path2.replace(/\\/g, "/");
}
function transformRelativePathToAbsPath(rPath, basePath = "./src") {
const joinedPath = path__default.join(basePath, rPath);
const targetPath = joinedPath.startsWith("/") ? joinedPath : `./${joinedPath}`;
const absPath = jiti__default(process.cwd(), {
extensions: [".js", ".mjs", ".cjs", ".ts", ".jsx", ".tsx"]
}).resolve(`${targetPath}`);
return winPath(absPath);
}
function transformAbsPathToComponentName(absPath) {
return upperCamelcase__default(absPath.replace(/^.*?:/, "").slice(1).replace(/\//g, "-"));
}
function transformAbsPathComponentPairsToImportCode(absPathComponentPairs) {
return absPathComponentPairs.map((item) => {
const [absPath, componentName] = item;
return `import ${componentName} from '${absPath}';`;
}).join("\n");
}
function trasnformRouteConfigItemToCode(routesConfig, pathComponentNameMap) {
const resultCodeItems = [];
routesConfig.forEach((routeConfig) => {
if (!routeConfig.path) {
throw new Error('"path" must be defined in route');
}
if (routeConfig.layout && routeConfig.children && routeConfig.children.length) {
const codeItem = `
{
path: '${routeConfig.path}',
children: ${trasnformRouteConfigItemToCode(routeConfig.children.map((item) => ({
layout: routeConfig.layout,
...item
})), pathComponentNameMap)},
}
`;
resultCodeItems.push(codeItem);
} else if (routeConfig.component) {
const ComponentName = pathComponentNameMap[routeConfig.component];
if (routeConfig.layout) {
const LayoutComponentName = pathComponentNameMap[routeConfig.layout];
const codeItem = `
{
path: '${routeConfig.path}',
element: (
<${LayoutComponentName}>
<${ComponentName} />
</${LayoutComponentName}>
),
}
`;
resultCodeItems.push(codeItem);
} else {
const codeItem = `
{
path: '${routeConfig.path}',
element: <${ComponentName} />,
}
`;
resultCodeItems.push(codeItem);
}
}
});
return `[${resultCodeItems.join(",\n")}]`;
}
function flatAllRelativePath(routesConfig) {
const pathResult = [];
routesConfig.forEach((routeConfig) => {
if (routeConfig.layout && routeConfig.children && routeConfig.children.length) {
const childrenResult = flatAllRelativePath(routeConfig.children);
pathResult.push(routeConfig.layout);
pathResult.push(...childrenResult);
} else if (routeConfig.component && routeConfig.path) {
pathResult.push(routeConfig.component);
}
});
return Array.from(/* @__PURE__ */ new Set([...pathResult]));
}
function transformRouteConfigToCode(routesConfig) {
const allRelativePaths = flatAllRelativePath(routesConfig);
const pathComponentNamePairs = allRelativePaths.map((relativePath) => {
const absPath = transformRelativePathToAbsPath(relativePath);
return [relativePath, absPath, transformAbsPathToComponentName(absPath)];
});
const relPathComponentNameMap = Object.fromEntries(pathComponentNamePairs.map((item) => [item[0], item[2]]));
const routesCode = trasnformRouteConfigItemToCode(routesConfig, relPathComponentNameMap);
const absPathComponentPairs = pathComponentNamePairs.map((item) => [item[1], item[2]]);
const importCode = transformAbsPathComponentPairsToImportCode(absPathComponentPairs);
return { routesCode, importCode };
}
function generateCodeByImportAndRouteCode(importCode, routeCode) {
return `
import React, { FC } from 'react';
import { HashRouter, BrowserRouter, useRoutes } from 'react-router-dom';
${importCode}
const InnerRoutes: FC = () => {
const element = useRoutes(${routeCode});
return element;
};
type RouterProps = {
mode?: 'hash' | 'browser';
};
const ReactRouter: FC<RouterProps> = ({ mode = 'hash' }) => {
if (mode === 'hash') {
return (
<HashRouter>
<InnerRoutes />
</HashRouter>
);
}
return (
<BrowserRouter>
<InnerRoutes />
</BrowserRouter>
);
};
export default ReactRouter;
`;
}
function generateCode(routesConfig) {
const { importCode, routesCode } = transformRouteConfigToCode(routesConfig);
return generateCodeByImportAndRouteCode(importCode, routesCode);
}
const virtualModuleId = "virtual:generated-react-router";
const resolvedVirutalModuleId = `\0${virtualModuleId}`;
function VitePluginReactRouter(userOptions) {
const { routesFile = "./routes.json" } = userOptions || {};
const routesStr = fs__default.readFileSync(path__default.resolve(process.cwd(), routesFile), "utf-8");
const routes = JSON.parse(routesStr);
const codeStr = generateCode(routes);
let command;
return {
name: "vite-plugin-react-router",
configResolved(config) {
command = config.command;
},
resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirutalModuleId;
}
return null;
},
load(id) {
if (id === resolvedVirutalModuleId) {
return codeStr;
}
return null;
},
async transform(code, id) {
if (id === resolvedVirutalModuleId) {
const esbuildResult = command === "serve" ? await vite.transformWithEsbuild(code, "/vite-plugin-react-router", {
loader: "tsx"
}) : await esbuild.transform(code, { loader: "tsx" });
return esbuildResult.code;
}
return null;
}
};
}
module.exports = VitePluginReactRouter;
;