UNPKG

@moccona/vite-plugin-react-conventional-router

Version:

Vite plugin for react conventional router

398 lines (395 loc) 12.6 kB
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