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
JavaScript
// 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
};