remix-feature-routes
Version:
remix router inspired by domain driven design
224 lines (219 loc) • 7.74 kB
JavaScript
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 };