@sentry/remix
Version:
Official Sentry SDK for Remix
244 lines (204 loc) • 7.19 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
const fs = require('fs');
const path = require('path');
let manifestCache = null;
let lastAppDirPath = null;
/**
* Check if a file is a route file
*/
function isRouteFile(filename) {
return filename.endsWith('.tsx') || filename.endsWith('.ts') || filename.endsWith('.jsx') || filename.endsWith('.js');
}
/**
* Convert Remix route file paths to parameterized paths at build time.
*
* Examples:
* - index.tsx -> /
* - users.tsx -> /users
* - users.$id.tsx -> /users/:id
* - users.$id.posts.$postId.tsx -> /users/:id/posts/:postId
* - $.tsx -> /:*
* - docs.$.tsx -> /docs/:*
* - users/$id.tsx (nested folder) -> /users/:id
* - users/$id/posts.tsx (nested folder) -> /users/:id/posts
* - users/index.tsx (nested folder) -> /users
* - _layout.tsx -> null (pathless layout route, not URL-addressable)
* - _auth.tsx -> null (pathless layout route, not URL-addressable)
*
* @param filename - The route filename or path (can include directory separators for nested routes)
* @returns Object containing the parameterized path and whether it's dynamic, or null for pathless layout routes
* @internal Exported for testing purposes
*/
function convertRemixRouteToPath(filename) {
// Remove file extension
const basename = filename.replace(/\.(tsx?|jsx?)$/, '');
// Handle root index route
if (basename === 'index' || basename === '_index') {
return { path: '/', isDynamic: false };
}
const normalizedBasename = basename.replace(/[/\\]/g, '.');
const segments = normalizedBasename.split('.');
const pathSegments = [];
let isDynamic = false;
let isIndexRoute = false;
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
if (!segment) {
continue;
}
if (segment.startsWith('_') && segment !== '_index') {
continue;
}
// Handle '_index' segments at the end (always skip - indicates an index route)
if (segment === '_index' && i === segments.length - 1) {
isIndexRoute = true;
continue;
}
// Handle 'index' segments at the end (skip only if there are path segments,
// otherwise root index is handled by the early return above)
if (segment === 'index' && i === segments.length - 1 && pathSegments.length > 0) {
isIndexRoute = true;
continue;
}
if (segment === '$') {
pathSegments.push(':*');
isDynamic = true;
continue;
}
if (segment.startsWith('$')) {
const paramName = segment.substring(1);
pathSegments.push(`:${paramName}`);
isDynamic = true;
} else if (segment !== 'index') {
pathSegments.push(segment);
}
}
// If all segments were skipped AND it's not an index route,
// it's a pathless layout route (like _layout.tsx, _auth.tsx) - exclude from manifest
if (pathSegments.length === 0 && !isIndexRoute) {
return null;
}
const path = pathSegments.length > 0 ? `/${pathSegments.join('/')}` : '/';
return { path, isDynamic };
}
/**
* Build a regex pattern for a dynamic route
*/
function buildRegexForDynamicRoute(routePath) {
const segments = routePath.split('/').filter(Boolean);
const regexSegments = [];
const paramNames = [];
for (const segment of segments) {
if (segment.startsWith(':')) {
const paramName = segment.substring(1);
if (paramName.endsWith('*')) {
const cleanParamName = paramName.slice(0, -1);
paramNames.push(cleanParamName);
regexSegments.push('(.+)');
} else {
paramNames.push(paramName);
regexSegments.push('([^/]+)');
}
} else {
regexSegments.push(segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
}
}
const pattern = `^/${regexSegments.join('/')}$`;
return { regex: pattern, paramNames };
}
/**
* Scan the routes directory and build the manifest, recursively processing subdirectories
*
* @param routesDir - The directory to scan for route files
* @param prefix - Path prefix for nested routes (used internally for recursion)
* @returns Object containing arrays of dynamic and static routes
*/
function scanRoutesDirectory(
routesDir,
prefix = '',
) {
const dynamicRoutes = [];
const staticRoutes = [];
try {
if (!fs.existsSync(routesDir)) {
return { dynamicRoutes, staticRoutes };
}
const entries = fs.readdirSync(routesDir);
for (const entry of entries) {
const fullPath = path.join(routesDir, entry);
const stat = fs.lstatSync(fullPath);
if (stat.isDirectory()) {
const nestedPrefix = prefix ? `${prefix}/${entry}` : entry;
const nested = scanRoutesDirectory(fullPath, nestedPrefix);
dynamicRoutes.push(...nested.dynamicRoutes);
staticRoutes.push(...nested.staticRoutes);
} else if (stat.isFile() && isRouteFile(entry)) {
const routeName = prefix ? `${prefix}/${entry}` : entry;
const result = convertRemixRouteToPath(routeName);
// Skip pathless layout routes (e.g., _layout.tsx, _auth.tsx)
if (result === null) {
continue;
}
const { path: routePath, isDynamic } = result;
if (isDynamic) {
const { regex, paramNames } = buildRegexForDynamicRoute(routePath);
dynamicRoutes.push({
path: routePath,
regex,
paramNames,
});
} else {
staticRoutes.push({
path: routePath,
});
}
}
}
} catch (error) {
// eslint-disable-next-line no-console
console.warn('Error building Remix route manifest:', error);
}
return { dynamicRoutes, staticRoutes };
}
/**
* Scans Remix routes directory and generates a manifest containing all static
* and dynamic routes with their regex patterns for client-side route parameterization.
*
* @param options - Configuration options
* @param options.appDirPath - Path to the app directory (where routes folder is located)
* @param options.rootDir - The root directory of the project (defaults to process.cwd())
* @returns A RouteManifest containing arrays of dynamic and static routes
*/
function createRemixRouteManifest(options) {
const rootDir = options?.rootDir || process.cwd();
let appDirPath;
if (options?.appDirPath) {
appDirPath = options.appDirPath;
} else {
const maybeAppDirPath = path.join(rootDir, 'app');
if (fs.existsSync(maybeAppDirPath) && fs.lstatSync(maybeAppDirPath).isDirectory()) {
appDirPath = maybeAppDirPath;
}
}
if (!appDirPath) {
return {
dynamicRoutes: [],
staticRoutes: [],
};
}
if (manifestCache && lastAppDirPath === appDirPath) {
return manifestCache;
}
const routesDir = path.join(appDirPath, 'routes');
const { dynamicRoutes, staticRoutes } = scanRoutesDirectory(routesDir);
const manifest = {
dynamicRoutes,
staticRoutes,
};
manifestCache = manifest;
lastAppDirPath = appDirPath;
return manifest;
}
exports.convertRemixRouteToPath = convertRemixRouteToPath;
exports.createRemixRouteManifest = createRemixRouteManifest;
//# sourceMappingURL=createRemixRouteManifest.js.map