UNPKG

remix-feature-routes

Version:

remix router inspired by domain driven design

224 lines (219 loc) 7.74 kB
import fs from 'node:fs'; import path$1 from 'node:path'; import { cosmiconfig as cosmiconfig$1 } from 'cosmiconfig'; import { findUp } from 'find-up'; import { globSync } from 'glob'; import { ensureRootRouteExists, getRouteIds, getRouteManifest } from 'remix-custom-routes'; import { existsSync } from 'fs'; import { writeFile, rm } from 'fs/promises'; import { createRequire } from 'module'; import path from 'path'; import { pathToFileURL } from 'url'; const require = createRequire(import.meta.url); let importFresh; const loadJsSync = function loadJsSync2(filepath) { if (importFresh === void 0) { importFresh = require("import-fresh"); } return importFresh(filepath); }; const loadJs = async function loadJs2(filepath) { try { const { href } = pathToFileURL(filepath); const mod = await import(href); return mod.routeConfig; } catch (error) { try { return loadJsSync(filepath, "").routeConfig; } catch (requireError) { if (requireError.code === "ERR_REQUIRE_ESM" || requireError instanceof SyntaxError && requireError.message.includes("Cannot use import statement outside a module")) { throw error; } throw requireError; } } }; let typescript; const loadTs = async function loadTs2(filepath, content = "") { if (typescript === void 0) { typescript = (await import('typescript')).default; } const compiledFilepath = `${filepath.slice(0, -2)}mjs`; let transpiledContent; try { try { const config = resolveTsConfig(path.dirname(filepath)) ?? {}; config.compilerOptions = { ...config.compilerOptions, module: typescript.ModuleKind.ES2022, moduleResolution: typescript.ModuleResolutionKind.Bundler, target: typescript.ScriptTarget.ES2022, noEmit: false }; transpiledContent = typescript.transpileModule(content, config).outputText; await writeFile(compiledFilepath, transpiledContent); } catch (error) { error.message = `TypeScript Error in ${filepath}: ${error.message}`; throw error; } return await loadJs(compiledFilepath, transpiledContent); } finally { if (existsSync(compiledFilepath)) { await rm(compiledFilepath); } } }; function resolveTsConfig(directory) { const filePath = typescript.findConfigFile(directory, (fileName) => { return typescript.sys.fileExists(fileName); }); if (filePath !== void 0) { const { config, error } = typescript.readConfigFile(filePath, (path2) => typescript.sys.readFile(path2)); if (error) { throw new Error(`Error in ${filePath}: ${error.messageText.toString()}`); } return config; } return; } function printRouteManifest(routes) { let count = 0; function getNextLetter() { const quotient = Math.floor(count / 26); const remainder = count % 26; count++; return String.fromCharCode("a".charCodeAt(0) + remainder) + (quotient > 0 ? String.fromCharCode("a".charCodeAt(0) + quotient - 1) : ""); } const replacements = /* @__PURE__ */ new Map(); const routeData = Object.values(routes).map((route) => { let routePath = route.path || ""; let r = route; while (r?.parentId) { routePath = path$1.join(routes[r.parentId]?.path || ".", routePath || "."); r = routes[r.parentId]; } const params = {}; const exampleUrl = routePath.replace(/:\w+/g, (match) => { const replacement = replacements.get(match) || getNextLetter(); replacements.set(match, replacement); params[match.slice(1)] = `'${replacement}'`; return replacement; }); return { file: route.file, path: routePath === "." ? "" : routePath, params, exampleUrl: exampleUrl === "." ? "" : exampleUrl }; }); const longestFile = Math.max(...routeData.map((r) => r.file.length)); const longestPath = Math.max(...routeData.map((r) => r.path?.length || 0)); const longestURL = Math.max(...routeData.map((r) => r.exampleUrl?.length || 0)); const padding = 4; const lines = routeData.map((route) => { const filePadded = route.file.padEnd(longestFile + padding, " "); const pathPadded = (typeof route.path === "string" ? `/${route.path}` : "").padEnd(longestPath + padding, " "); const urlPadded = (typeof route.exampleUrl === "string" ? `/${route.exampleUrl}` : "").padEnd( longestURL + padding, " " ); const paramsString = Object.keys(route.params).length > 0 ? `{ ${Object.entries(route.params).map(([k, v]) => `${k}: ${v}`).join(", ")} }` : ""; return `${filePadded} ${pathPadded} ${urlPadded} ${paramsString}`; }); const longestLine = Math.max(...lines.map((x) => x.length)); console.log( `${"ROUTE".padEnd(longestFile + padding, " ")} ${"URL".padEnd( longestPath + padding, " " )} ${"EXAMPLE".padEnd(longestURL + padding, " ")} PARAMS` ); console.log("-".repeat(longestLine)); console.log(lines.join("\n") + "\n"); } const ignoreDomains = /* @__PURE__ */ new Set(["shared"]); const cosmiconfig = cosmiconfig$1("remix-feature-routes", { loaders: { ".mjs": loadJs, ".cjs": loadJs, ".js": loadJs, ".ts": loadTs } }); function getDomains(appDir) { return fs.readdirSync(appDir, { withFileTypes: true }).filter((x) => x.isDirectory()).filter((x) => !ignoreDomains.has(x.name)).filter( (x) => fs.lstatSync(path$1.join(appDir, x.name, "routes"), { throwIfNoEntry: false })?.isDirectory() ).map((x) => x.name); } async function getRouteConfig(domain) { const config = { basePath: domain }; if (fs.existsSync(`./app/${domain}/config.ts`)) { const loaded = await cosmiconfig.load(`./app/${domain}/config.ts`); Object.assign(config, loaded?.config); } if (config.basePath === "/") { config.basePath = `_${domain}`; } else { config.basePath = normalizeId(config.basePath); } return config; } function normalizeId(id) { return id.replace(/^\//, "").replaceAll("/", "."); } async function parseRouteIds(domain, routeIds) { const routeConfig = await getRouteConfig(domain); for (const route of routeIds) { route[1] = path$1.join(domain, route[1]); const basename = path$1.basename(route[0]); const id = `${routeConfig.basePath}.${basename}`; const segments = id.split("."); for (let idx = 0; idx < segments.length; idx++) { const segment = segments[idx]; if (segment === "_layout") { segments[idx] = ""; } else if (segment === "index") { segments[idx] = `_${segment}`; } else if (segment === "_") { segments[idx - 1] = `${segments[idx - 1]}_`; segments[idx] = ""; } else { segments[idx] = segment; } } route[0] = segments.filter(Boolean).join("."); } } function featureRoutes(options) { return async function routes() { const root = path$1.dirname(await findUp("package.json") || process.cwd()); const appDirectory = path$1.join(root, "app"); ensureRootRouteExists(appDirectory); const domains = getDomains(appDirectory); const routeIds = []; for (const domain of domains) { const files = globSync("routes/**/*.{js,jsx,ts,tsx,md,mdx}", { cwd: path$1.join(appDirectory, domain), ignore: options?.ignoredRouteFiles }); if (!files.length) continue; const domainRoutes = getRouteIds(files, { indexNames: ["index"] }); await parseRouteIds(domain, domainRoutes); routeIds.push(...domainRoutes); } routeIds.sort(([a], [b]) => b.length - a.length); const manifest = getRouteManifest(routeIds); if (process.env["DEBUG_FEATURE_ROUTES"]) { printRouteManifest(manifest); } return manifest; }; } export { featureRoutes };