@moccona/vite-plugin-react-conventional-router
Version:
Vite plugin for react conventional router
398 lines (395 loc) • 12.6 kB
JavaScript
import * as logger from '@moccona/logger';
import { createFilter } from '@rollup/pluginutils';
import nodepath2 from 'path';
import fg from 'fast-glob';
// src/index.ts
var DEFAULT_IGNORE_PATTERN = ["node_modules/**"];
var PLUGIN_NAME = "vite-plugin-conventional-router";
var PLUGIN_VIRTUAL_MODULE_NAME = "virtual:routes";
var PLUGIN_MAIN_PAGE_FILE = "index.tsx";
var LAYOUT_FILE_NAME = "layout";
var NOT_FOUND_FILE_NAME = "404";
var ERROR_BOUNDARY_FILE_NAME = "errorBoundary";
var LOADER_FILE_NAME = "loader";
var HANDLE_FILE_NAME = "handle";
var OPTIONAL_ROUTE_FLAG = "$";
var DYNAMIC_ROUTE_FLAG = "@";
var SPECIAL_PATH_SPLIT = ".";
var FILE_PATH_SEP = nodepath2.sep;
var ROUTE_PATH_SEP = "/";
function globSync(pattern, ignore) {
const files = fg.sync(pattern, {
deep: Infinity,
ignore: [...DEFAULT_IGNORE_PATTERN, ...ignore]
});
pluginlog.debug(
`Pattern: ${pattern}`,
"\n",
`Ignore: ${ignore}`,
"\n",
`Files: ${files.join("\n")}`,
`----------------------------------------------------`
);
return files;
}
var collectRoutePages = (pages, ignore = []) => {
let pageModules = [];
let routes = [];
for (const pattern of pages) {
let files = globSync(pattern, ignore);
pageModules = [
...pageModules,
...files.map((file) => nodepath2.resolve(file))
];
while (true) {
const group = files.map((file) => file[0]);
if (new Set(group).size > 1) {
break;
} else {
files = files.map((file) => file.slice(1));
}
}
routes = [...routes, ...files.map((file) => file).flat()];
}
return routes.map((s) => filePathToRoutePath(s)).map((route, index) => {
return {
path: route,
element: nodepath2.resolve(pageModules[index])
};
});
};
var validRouteFieldKeyRegexp = (fieldKey, filepath, options = {}) => {
if (fieldKey === LOADER_FILE_NAME || fieldKey === HANDLE_FILE_NAME) {
options.allowTs = true;
}
return new RegExp(
`^([\\w\\${OPTIONAL_ROUTE_FLAG}\\${DYNAMIC_ROUTE_FLAG}]+\\.){0,}(${fieldKey})(\\.tsx${options.allowTs ? "?" : ""})$`
).test(nodepath2.basename(filepath));
};
var isFieldKeyRoute = (routeA, routeB, fieldKey) => {
if (routeA.path === "/" || routeA.path === "") {
console.log(
"fieldKey",
fieldKey,
"\n",
"routeA ~>",
routeA,
"\n",
"routeB ~>",
routeB,
"\n"
);
}
if (nodepath2.dirname(routeA.element) === nodepath2.dirname(routeB.element)) {
const condition = validRouteFieldKeyRegexp(
fieldKey,
routeB.element
);
if (routeA.path === "" && routeB.path.split("/").length === 1) {
return condition;
}
return condition && routeB.path.split(ROUTE_PATH_SEP).length - routeA.path.split(ROUTE_PATH_SEP).length === 1;
}
return false;
};
var reserved_route_filed_keys = {
[LAYOUT_FILE_NAME]: LAYOUT_FILE_NAME,
[ERROR_BOUNDARY_FILE_NAME]: ERROR_BOUNDARY_FILE_NAME,
[LOADER_FILE_NAME]: LOADER_FILE_NAME,
[HANDLE_FILE_NAME]: HANDLE_FILE_NAME
};
function collectRouteFieldKeyRoute(routes) {
const testRoutePath = (routePath) => {
return Object.keys(reserved_route_filed_keys).map((fieldKey) => {
return validRouteFieldKeyRegexp(fieldKey, routePath);
}).some((result) => result);
};
return routes.filter(
(route) => testRoutePath(nodepath2.basename(route.element))
);
}
var reserved_root_field_keys = {
[NOT_FOUND_FILE_NAME]: NOT_FOUND_FILE_NAME,
[LAYOUT_FILE_NAME]: LAYOUT_FILE_NAME,
[LOADER_FILE_NAME]: LOADER_FILE_NAME,
[HANDLE_FILE_NAME]: HANDLE_FILE_NAME
};
var collectRootRouteRelatedRoute = (routes) => {
return Object.assign(
Object.keys(reserved_root_field_keys).reduce(
(object, fieldKey) => ({
...object,
[fieldKey]: routes.find((route) => route.path === fieldKey)
}),
{}
),
{
routes: routes.filter(
(route) => !Object.keys(reserved_root_field_keys).includes(route.path)
)
}
);
};
var arrangeRoutes = (isolateRoutes, parent, subRoutesPathAppendToParent, sideEffectRoutes = []) => {
const subs = isolateRoutes.filter(
(route) => isSubPath(parent.path, route.path)
);
const { handle, loader, errorBoundary, layout } = Object.keys(
reserved_route_filed_keys
).reduce(
(obj, fieldKey) => ({
...obj,
[fieldKey]: sideEffectRoutes.find((route) => {
return isFieldKeyRoute(parent, route, fieldKey);
})
}),
{}
);
subRoutesPathAppendToParent.push(
...subs.map((s) => ROUTE_PATH_SEP + s.path)
);
Object.assign(parent, {
path: ROUTE_PATH_SEP + parent.path,
loader: loader?.element,
handle: handle?.element,
ErrorBoundary: errorBoundary?.element,
children: subs.map(
(sub) => arrangeRoutes(
isolateRoutes,
sub,
subRoutesPathAppendToParent,
sideEffectRoutes
)
)
});
if (layout) {
const parentCopy = deepCopy(parent);
delete parent.path;
return Object.assign(parent, layout, {
path: parentCopy.path,
children: [parentCopy],
// Don't set error boundary in layout
ErrorBoundary: void 0
});
}
return parent;
};
var fileProtocol = (path) => {
return process.platform === "win32" ? new URL(`file://${path}`).href : path;
};
var stringifyRoutes = (routes, imports = [], lazy = false) => {
const code = routes.map((route) => {
const length_ = imports.length;
const { loader, handle, ErrorBoundary, action, element } = route;
if (loader)
imports.push(
`import loader${length_} from "${fileProtocol(loader)}";`
);
if (handle)
imports.push(
`import handle${length_} from "${fileProtocol(handle)}";`
);
if (ErrorBoundary)
imports.push(
`import ErrorBoundary${length_} from "${fileProtocol(ErrorBoundary)}";`
);
if (action)
imports.push(
`import action${length_} from "${fileProtocol(action)}";`
);
if (lazy) {
return `{
path: "${route.path}",
lazy: async () => {
const element = await import("${fileProtocol(element)}");
return {
Component: element.default,
shouldRevalidate: element.shouldRevalidate,
loader: ${loader ? `loader${length_}` : `element.loader`},
action: ${action ? `action${length_}` : `element.action`},
handle: ${handle ? `handle${length_}` : `element.handle`},
ErrorBoundary: ${ErrorBoundary ? `ErrorBoundary${length_}` : `element.ErrorBoundary`},
};
},
children: ${!route.children ? "[]" : stringifyRoutes(route.children, imports, lazy)}
}`;
} else {
return `{
path: "${route.path}",
shouldRevalidate: element${length_}.shouldRevalidate,
loader: ${loader ? `loader${length_}` : `element${length_}.loader`},
action: ${action ? `action${length_}` : `element${length_}.action`} ,
handle: ${handle ? `handle${length_}` : `element${length_}.handle`},
Component: element${length_}.default,
ErrorBoundary: ${ErrorBoundary ? `ErrorBoundary${length_}` : `element${length_}.ErrorBoundary`} ,
children: ${!route.children ? "[]" : stringifyRoutes(route.children, imports, lazy)}
}`;
}
}).join(",");
return `[${code}]`;
};
var deepCopy = (data) => JSON.parse(JSON.stringify(data));
var stripSlash = (filepath) => {
return filepath.replace(/^\//, "").replace(/\/$/, "");
};
var filePathToRoutePath = (filepath) => {
const extname = nodepath2.extname(filepath);
filepath = filepath.replace(extname, "").replaceAll(SPECIAL_PATH_SPLIT, FILE_PATH_SEP) + extname;
const path_ = filepath.endsWith(PLUGIN_MAIN_PAGE_FILE) ? stripSlash(filepath.replace(PLUGIN_MAIN_PAGE_FILE, "")) : stripSlash(filepath.replace(extname, ""));
return path_.split(ROUTE_PATH_SEP).map((seg) => {
if (seg.startsWith(DYNAMIC_ROUTE_FLAG)) {
return seg.replace(DYNAMIC_ROUTE_FLAG, ":");
}
if (seg.startsWith(OPTIONAL_ROUTE_FLAG)) {
const [, p] = new RegExp(`^\\${OPTIONAL_ROUTE_FLAG}(.+)`).exec(seg) ?? [];
return p ? `:${p}?` : seg;
}
return seg;
}).join(ROUTE_PATH_SEP);
};
var isSubPath = (parentPath, subPath) => {
if (parentPath !== "" && subPath.startsWith(parentPath) && subPath.split(ROUTE_PATH_SEP).length - parentPath.split(ROUTE_PATH_SEP).length === 1) {
return true;
}
return false;
};
// src/index.ts
var { createScopedLogger } = logger;
var pluginlog = createScopedLogger(PLUGIN_NAME);
var createFileFilter = (include = [], exclude = []) => createFilter(include, exclude);
var mergeDefaultOptions = (options) => {
const _opt_ = Object.assign(
{},
{
include: [],
exclude: [],
lazy: false
},
options
);
if (!Array.isArray(_opt_.include)) {
_opt_.include = [_opt_.include];
}
if (!Array.isArray(_opt_.exclude)) {
_opt_.exclude = [_opt_.exclude];
}
return _opt_;
};
function ConventionalRouter(options = {}) {
const _options = mergeDefaultOptions(options);
const filter = createFileFilter(_options.include, _options.exclude);
let devServer;
return {
name: PLUGIN_NAME,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
config(config, _env) {
return config;
},
/**
* Get vite dev server instance.
*/
configureServer(server) {
devServer = server;
},
/**
* Auto-gen route config access by vitural module "virtual:routes"
*/
resolveId(source) {
if (source === PLUGIN_VIRTUAL_MODULE_NAME) {
pluginlog.info("Read virtual routes");
return source;
}
return void 0;
},
/**
* Parse folder structure to generate code.
*/
load(id) {
if (id === PLUGIN_VIRTUAL_MODULE_NAME) {
const allRoutes = collectRoutePages(_options.include, _options.exclude);
const subRoutesPathAppendToParent = [];
const {
routes = [],
"404": notFoundRoute,
layout: rootLayoutRoute
} = collectRootRouteRelatedRoute(allRoutes);
const sideEffectRoutes = collectRouteFieldKeyRoute(routes);
if (notFoundRoute) {
subRoutesPathAppendToParent.push(`/${notFoundRoute.path}`);
}
const isolateRoutes = routes.filter(
(r) => !new Set(sideEffectRoutes.map((route) => route.element)).has(
r.element
)
);
const mapCallback = (r) => {
if (r.path.startsWith("/")) {
return r;
} else {
return {
...r,
path: `/${r.path}`
};
}
};
isolateRoutes.filter((r) => r.path.split("/").length === 1).forEach(
(route) => arrangeRoutes(
isolateRoutes,
route,
subRoutesPathAppendToParent,
sideEffectRoutes
)
);
const intermediaRoutes = isolateRoutes.filter(
(r) => !subRoutesPathAppendToParent.includes(r.path)
);
subRoutesPathAppendToParent.length = 0;
intermediaRoutes.filter((r) => r.path.split("/").length > 2).forEach(
(route) => arrangeRoutes(
intermediaRoutes,
route,
subRoutesPathAppendToParent,
sideEffectRoutes
)
);
let finalRoutes = intermediaRoutes.filter((r) => !subRoutesPathAppendToParent.includes(r.path)).map(mapCallback);
if (rootLayoutRoute) {
finalRoutes = [
{
...rootLayoutRoute,
path: "/",
children: finalRoutes
}
];
}
if (notFoundRoute) {
finalRoutes.push({ ...notFoundRoute, path: "*" });
}
const imports = [];
const routeString = stringifyRoutes(finalRoutes, imports, options.lazy);
return {
code: `
${imports.join("\n")}
const routes = ${routeString};
if(import.meta.env.DEV) {
console.log(routes);
}
export default routes;
`
};
}
return void 0;
},
watchChange(id, change) {
if (filter(id) && (change.event === "create" || change.event === "delete")) {
this.info(`Prepare restart because ${id} change`);
devServer.restart().catch((error) => {
this.warn(`Restart failed: ${error.message}`);
});
}
}
};
}
export { ConventionalRouter as default, pluginlog };
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map