UNPKG

vite-plugin-ra-pages

Version:

Vite plugin to automatically generate routes for React-Admin based on folder structure.

649 lines (631 loc) 16.4 kB
// src/index.ts import path6 from "path"; // src/scanDirectory/legacy.ts import { globSync } from "glob"; import path, { slash } from "slash-path"; var removeSufix = (name) => name.replace(/\/(Index|Page)\.(jsx|tsx)$/, ""); var globSlash = (pattern, options) => { const files = globSync(pattern, options); return files.map((path7) => slash(path7)); }; var getEntries = (root) => { const options = { cwd: root, ignore: "node_modules/**" }; const resources = globSlash("**/Index.@(jsx|tsx)", options).map(removeSufix); const others = globSlash("**/@(Page|PageIndex).@(jsx|tsx)", options).filter( (path7) => !resources.some((prefix) => path7.startsWith(prefix + "/")) ); return { resources, others }; }; var getRouteResource = ({ resource, root, key }) => { const files = globSlash("**/@(Page|Index).@(jsx|tsx)", { cwd: path.join(root, resource), ignore: "node_modules/**", nodir: true }); const list = files.find((it) => it.startsWith("Index.")); const create = files.find((it) => it.startsWith("create/Page.")); const withParam = files.filter((it) => it.startsWith("[id]/")); const edit = withParam.find((it) => it.startsWith("[id]/edit/Page.")); const show = withParam.find((it) => it.startsWith("[id]/show/Page.")); const others = files.filter( (it) => it !== list && it !== create && it !== edit && it !== show ); return { key, root, resource, list, show, create, edit, others }; }; var getConfigEntries = (root) => { const { resources, others } = getEntries(root); const routeResources = resources.map((resource, ix) => { return getRouteResource({ resource, root, key: `R_${ix + 1}` }); }); const size = routeResources.length + 1; const routeFiles = others.map((file, ix) => { return { key: `R_${ix + size}`, file, root }; }); return { resources: routeResources, others: routeFiles }; }; var legacy_default = getConfigEntries; // src/scanDirectory/index.ts import path3 from "slash-path"; // src/scanDirectory/v2.ts import { globSync as globSync2 } from "glob"; import path2, { slash as slash2 } from "slash-path"; var removeSufix2 = (name) => name.replace(/\/(LIST|INDEX|PAGE)\.(jsx|tsx)$/, ""); var globSlash2 = (pattern, options) => { const files = globSync2(pattern, options); return files.map((path7) => slash2(path7)); }; var getEntries2 = (root) => { const options = { cwd: root, ignore: "node_modules/**" }; const resources = globSlash2("**/LIST.@(jsx|tsx)", options).map(removeSufix2); const others = globSlash2("**/@(PAGE|INDEX).@(jsx|tsx)", options).filter( (path7) => !resources.some((prefix) => path7.startsWith(prefix + "/")) ); return { resources, others }; }; var getRouteResource2 = ({ resource, root, key }) => { const files = globSlash2("**/@(PAGE|INDEX|LIST).@(jsx|tsx)", { cwd: path2.join(root, resource), ignore: "node_modules/**", nodir: true }); console.log(">>>>", files); const list = files.find((it) => it.startsWith("LIST.")); const create = files.find((it) => it.startsWith("create/PAGE.")); const withParam = files.filter((it) => it.startsWith("[id]/")); const edit = withParam.find((it) => it.startsWith("[id]/edit/PAGE.")); const show = withParam.find((it) => it.startsWith("[id]/show/PAGE.")); const others = files.filter( (it) => it !== list && it !== create && it !== edit && it !== show ); return { key, root, resource, list, show, create, edit, others }; }; var getConfigEntries2 = (root) => { const { resources, others } = getEntries2(root); console.log(resources, others); const routeResources = resources.map((resource, ix) => { return getRouteResource2({ resource, root, key: `R_${ix + 1}` }); }); const size = routeResources.length + 1; const routeFiles = others.map((file, ix) => { return { key: `R_${ix + size}`, file, root }; }); return { resources: routeResources, others: routeFiles }; }; var v2_default = getConfigEntries2; // src/scanDirectory/index.ts var wrapper = { "legacy": legacy_default, "v2": v2_default }; var getConfigEntries3 = (root, raConfig) => { const scan = wrapper[raConfig.mode] ?? wrapper.legacy; return scan(root); }; var resolveImportFile = (pathFile, root) => { let importFile = path3.relative(root, path3.join(...pathFile)); if (!importFile.startsWith(".")) { importFile = `./${importFile}`; } return importFile; }; var parseRoutePath = (name) => name.replace(/\/(Index|Page)\.(jsx|tsx)$/, "").replace("[", ":").replace("]", ""); // src/writeCode/adminWrapper.ts import fs from "fs"; import path4 from "path"; // src/writeCode/el.ts var appendELAttr = (el, key, attrs, on) => { if (key) { Object.entries(attrs).forEach(([k, v]) => { if (typeof v === "function") { v = v(key); } if (v.startsWith("{") && v.endsWith("}")) { el.attrs[k] = v; } else if (v.startsWith("'") && v.endsWith("'")) { el.attrs[k] = v; } else if (v.startsWith('"') && v.endsWith('"')) { el.attrs[k] = v; } else { el.attrs[k] = `${key}.${v}`; } }); on(key); } }; var printImports = (root) => { const collected = []; const walk = (node) => { node.imports.filter((it) => it).forEach((imp) => collected.push(imp)); node.children.forEach((child) => walk(child)); }; walk(root); return collected.join("\n"); }; var printElement = (el, tab = " ") => { const tab1 = " ".padStart(el.tag.length + 2, " "); const attrs = Object.entries(el.attrs).map(([k, v]) => { if (k.startsWith("{") && k.endsWith("}")) { return k; } else if (v.startsWith("'") && v.endsWith("'")) { return `${k}=${v}`; } else if (v.startsWith('"') && v.endsWith('"')) { return `${k}=${v}`; } else if (v.startsWith("{") && v.endsWith("}")) { return `${k}=${v}`; } else { return `${k}={${v}}`; } }).join(` ${tab}${tab1}`); const openTag = attrs ? `<${el.tag} ${attrs}>` : `<${el.tag}>`; const children = el.children.map((child) => printElement(child, tab + " ")); return [`${tab}${openTag}`, ...children, `${tab}</${el.tag}>`].join("\n"); }; // src/writeCode/adminWrapper.ts var writeAdminWrapper = (file, config, raConfig) => { const dirout = path4.dirname(file); const wrappers = []; const addWrap = (key) => { wrappers.push( `const ${key}Wrapper = (props) => <${key}.default {...props}/>;` ); }; const nameWrap = (key, print = false) => print ? `{<${key}Wrapper/>}` : `{${key}Wrapper}`; const root = { tag: "Admin", attrs: { "{...props}": "" }, children: [], imports: [ "import React from 'react';", "import { CustomRoutes, Resource, usePermissions } from 'ra-core';", `import { Admin } from '${raConfig.raPkg}';`, "import { Route } from 'react-router-dom';" ] }; config.resources.map((rr) => { const files = [rr.list, rr.create, rr.show, rr.edit, ...rr.others].filter( (it) => it ); const keyImport = files.reduce((acc, file2, ix) => { acc[file2] = `${rr.key}_${ix + 1}`; return acc; }, {}); const el = { tag: raConfig.withRole ? "ResourceRole" : "Resource", attrs: {}, children: [], imports: Object.entries(keyImport).map(([file2, key]) => { const importFile = resolveImportFile( [rr.root, rr.resource, file2], dirout ); if (file2.includes("Index.")) { return `import * as ${key} from '${importFile}';`; } if (raConfig.lazyLoad) { return `const ${key} = { default: React.lazy(() => import('${importFile}')) };`; } return `import * as ${key} from '${importFile}';`; }) }; appendELAttr( el, keyImport[rr.list], { name: `'${rr.resource}'`, icon: "Icon", options: "Options", roles: "Roles", list: nameWrap }, addWrap ); appendELAttr( el, keyImport[rr.create], { create: nameWrap }, addWrap ); appendELAttr( el, keyImport[rr.show], { show: nameWrap }, addWrap ); appendELAttr( el, keyImport[rr.edit], { edit: nameWrap }, addWrap ); rr.others.forEach((file2) => { const key = keyImport[file2]; addWrap(key); const child = { tag: "Route", attrs: { path: `'${parseRoutePath(file2)}'`, element: nameWrap(key, true) }, children: [], imports: [] }; el.children.push(child); }); return el; }).forEach((it) => { root.children.push(it); }); const customRoutes = { tag: "CustomRoutes", attrs: {}, children: [], imports: [] }; root.children.push(customRoutes); config.others.forEach((rf) => { const importFile = resolveImportFile([rf.root, rf.file], dirout); if (raConfig.lazyLoad) { root.imports.push( `const ${rf.key} = { default: React.lazy(() => import('${importFile}')) };` ); } else { root.imports.push(`import * as ${rf.key} from '${importFile}';`); } if (rf.file.startsWith("Page.") || rf.file.startsWith("dashboard/Page.")) { root.attrs = { dashboard: `${rf.key}.default`, ...root.attrs }; } else { const child = { tag: "Route", attrs: { path: `'${parseRoutePath(rf.file)}'`, element: `<${rf.key}.default />` }, children: [], imports: [] }; customRoutes.children.push(child); } }); const code = ` // Generate ReactAdmin ${printImports(root)} ${wrappers.join("\n")} const ResourceRole = ({ roles = [], ...props }) => { const { permissions, isLoading } = usePermissions(); if (isLoading) return null; const hasPermission = roles.length !== 0 || roles.some((role) => permissions.includes(role)); if (!hasPermission) return null; return <Resource {...props} />; }; ResourceRole.raName = 'Resource'; ResourceRole.registerResource = ({ create, edit, icon, list, name, options, show, recordRepresentation, hasCreate, hasEdit, hasShow, }) => ({ name, options, hasList: !!list, hasCreate: !!create || !!hasCreate, hasEdit: !!edit || !!hasEdit, hasShow: !!show || !!hasShow, icon, recordRepresentation, }); const RAAdmin = (props)=>{ return ( ${printElement(root, " ")} ); }; export default RAAdmin; `; if (!fs.existsSync(dirout)) { fs.mkdirSync(dirout, { recursive: true }); } fs.writeFileSync(file, code); }; // src/writeCode/menuWrapper.ts import fs2 from "fs"; import path5 from "path"; var writeMenuWrapper = (file, config, raConfig) => { const dirout = path5.dirname(file); const wrappers = []; const addWrap = (key) => { wrappers.push( `const ${key}Wrapper = (props) => <${key}.default {...props}/>;` ); }; const nameWrap = (key, print = false) => print ? `{<${key}Wrapper/>}` : `{${key}Wrapper}`; const root = { tag: "Admin", attrs: { "{...props}": "" }, children: [], imports: [ "import React from 'react';", "import { Resource, usePermissions } from 'ra-core';", `import { Admin } from '${raConfig.raPkg}';`, "import { Route } from 'react-router-dom';" ] }; config.resources.map((rr) => { const files = [rr.list, rr.create, rr.show, rr.edit, ...rr.others].filter( (it) => it ); const keyImport = files.reduce((acc, file2, ix) => { acc[file2] = `${rr.key}_${ix + 1}`; return acc; }, {}); const el = { tag: "Resource", attrs: {}, children: [], imports: Object.entries(keyImport).map(([file2, key]) => { const importFile = resolveImportFile( [rr.root, rr.resource, file2], dirout ); if (raConfig.lazyLoad) { return `const ${key} = { default: React.lazy(() => import('${importFile}')) };`; } return `import * as ${key} from '${importFile}';`; }) }; appendELAttr( el, keyImport[rr.list], { name: `'${rr.resource}'`, icon: "Icon", options: "Options", list: nameWrap }, addWrap ); appendELAttr( el, keyImport[rr.create], { create: nameWrap }, addWrap ); appendELAttr( el, keyImport[rr.show], { show: nameWrap }, addWrap ); appendELAttr( el, keyImport[rr.edit], { edit: nameWrap }, addWrap ); rr.others.forEach((file2) => { const key = keyImport[file2]; addWrap(key); const child = { tag: "Route", attrs: { path: `'${parseRoutePath(file2)}'`, element: nameWrap(key, true) }, children: [], imports: [] }; el.children.push(child); }); return el; }).forEach((it) => { root.children.push(it); }); config.others.forEach((rf) => { const importFile = resolveImportFile([rf.root, rf.file], dirout); if (raConfig.lazyLoad) { root.imports.push( `const ${rf.key} = { default: React.lazy(() => import('${importFile}')) };` ); } else { root.imports.push(`import * as ${rf.key} from '${importFile}';`); } if (rf.file.startsWith("Page.")) { root.attrs = { dashboard: `${rf.key}.default`, ...root.attrs }; } else { const child = { tag: "Route", attrs: { path: `'${parseRoutePath(rf.file)}'`, element: `<${rf.key}.default />` }, children: [], imports: [] }; root.children.push(child); } }); const code = ` // Generate ReactAdmin ${printImports(root)} ${wrappers.join("\n")} const RAAdmin = (props)=>{ return ( ${printElement(root, " ")} ); }; export default RAAdmin; `; if (!fs2.existsSync(dirout)) { fs2.mkdirSync(dirout, { recursive: true }); } fs2.writeFileSync(file, code); }; // src/types.ts var ensureRAConfig = ({ root = process.cwd(), pageDir = "src/pages", cacheDir = ".ra", raAdminId = "ra-admin.jsx", raAdminFile = "ra-admin.jsx", raMenuId = "ra-menu.jsx", raMenuFile = "ra-menu.jsx", raPkg = "react-admin", mode = "legacy", lazyLoad = false, withRole = false //roleMapping = {}, }) => { return { root, raAdminId, raMenuId, pageDir, cacheDir, raAdminFile, raMenuFile, raPkg, mode, lazyLoad, withRole //roleMapping: { ...DEFAULT_ROLE_MAPPING, ...roleMapping }, }; }; // src/index.ts var raPages = (raOpts = {}) => { const raConfig = ensureRAConfig(raOpts); const rootPageDir = path6.resolve(raConfig.root, raConfig.pageDir); const raAdminFileFile = path6.resolve( raConfig.root, raConfig.cacheDir, raConfig.raAdminFile ); const raMenuFileFile = path6.resolve( raConfig.root, raConfig.cacheDir, raConfig.raMenuFile ); const shouldRegenerate = (file) => file.startsWith(rootPageDir); const regenerate = () => { const config = getConfigEntries3(rootPageDir, raConfig); writeAdminWrapper(raAdminFileFile, config, raConfig); writeMenuWrapper(raMenuFileFile, config, raConfig); }; regenerate(); return { name: "vite-plugin-ra-pages", enforce: "pre", config: () => { return { resolve: { alias: { [raConfig.raAdminId]: raAdminFileFile, [raConfig.raMenuId]: raMenuFileFile } } }; }, configureServer: (server) => { server.watcher.add(rootPageDir); server.watcher.on("add", (file) => { if (shouldRegenerate(file)) regenerate(); }); server.watcher.on("unlink", (file) => { if (shouldRegenerate(file)) regenerate(); }); server.watcher.on("change", (file) => { if (shouldRegenerate(file)) regenerate(); }); } }; }; var index_default = raPages; export { index_default as default, raPages };