New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More β†’
Socket
Sign inDemoInstall
Socket

remix-flat-routes

Package Overview
Dependencies
Maintainers
1
Versions
47
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

remix-flat-routes - npm Package Compare versions

Comparing version 0.4.8 to 0.5.0

12

CHANGELOG.md
# CHANGELOG
## v0.5.0
- πŸ”¨ Update flatRoutes with new features
- Uses same function as Remix core
- Allows to maintain extended flat-routes function
- Customizations passed in `options`
- Add support for "hybrid" routes
- Add support for extended route filenames
- Add support for multiple route folders
- Add support for custom param prefix character
- Add support for custom base path
## v0.4.7

@@ -4,0 +16,0 @@

32

dist/index.d.ts

@@ -0,5 +1,8 @@

import type { DefineRouteFunction, RouteManifest } from '@remix-run/dev/config/routes';
declare type RouteInfo = {
id: string;
path: string;
file: string;
name: string;
segments: string[];
parentId?: string;

@@ -9,5 +12,2 @@ index?: boolean;

};
interface RouteManifest {
[key: string]: RouteInfo;
}
declare type DefineRouteOptions = {

@@ -20,5 +20,6 @@ caseSensitive?: boolean;

};
declare type DefineRouteFunction = (path: string | undefined, file: string, optionsOrChildren?: DefineRouteOptions | DefineRouteChildren, children?: DefineRouteChildren) => void;
export declare type VisitFilesFunction = (dir: string, visitor: (file: string) => void, baseDir?: string) => void;
declare type FlatRoutesOptions = {
export declare type FlatRoutesOptions = {
routeDir?: string | string[];
defineRoutes?: DefineRoutesFunction;
basePath?: string;

@@ -28,7 +29,22 @@ visitFiles?: VisitFilesFunction;

ignoredRouteFiles?: string[];
routeRegex?: RegExp;
};
export declare type DefineRoutesFunction = (callback: (route: DefineRouteFunction) => void) => any;
export default function flatRoutes(baseDir: string, defineRoutes: DefineRoutesFunction, options?: FlatRoutesOptions): RouteManifest;
export declare function getRouteInfo(baseDir: string, routeFile: string, basePath?: string, paramsPrefixChar?: string): RouteInfo | null;
export type { DefineRouteFunction, DefineRouteOptions, DefineRouteChildren, RouteManifest, RouteInfo, };
export { flatRoutes };
export type { DefineRouteFunction, DefineRouteOptions, DefineRouteChildren, RouteManifest, RouteInfo, };
export default function flatRoutes(routeDir: string | string[], defineRoutes: DefineRoutesFunction, options?: FlatRoutesOptions): RouteManifest;
export declare function isRouteModuleFile(filename: string, routeRegex: RegExp): boolean;
export declare function isIndexRoute(routeId: string): boolean;
export declare function getRouteInfo(routeDir: string, file: string, options: FlatRoutesOptions): {
id: string;
path: string;
file: string;
name: string;
segments: string[];
index: boolean;
};
export declare function createRoutePath(routeSegments: string[], index: boolean, options: FlatRoutesOptions): string | undefined;
export declare function getRouteSegments(name: string, paramPrefixChar?: string): string[];
export declare function defaultVisitFiles(dir: string, visitor: (file: string) => void, baseDir?: string): void;
export declare function createRouteId(file: string): string;
export declare function normalizeSlashes(file: string): string;

@@ -29,56 +29,26 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
exports.flatRoutes = exports.getRouteInfo = void 0;
exports.normalizeSlashes = exports.createRouteId = exports.defaultVisitFiles = exports.getRouteSegments = exports.createRoutePath = exports.getRouteInfo = exports.isIndexRoute = exports.isRouteModuleFile = exports.flatRoutes = void 0;
const fs = __importStar(require("fs"));
const minimatch_1 = __importDefault(require("minimatch"));
const path = __importStar(require("path"));
const util_1 = require("./util");
function flatRoutes(baseDir, defineRoutes, options = {}) {
const defaultOptions = {
routeDir: 'routes',
basePath: '/',
paramPrefixChar: '$',
routeRegex: /\/((index|route|layout|page)|(_[a-zA-Z0-9_$.-]+)|([a-zA-Z0-9_$.\[\]-]+\.route))\.(ts|tsx|js|jsx|md|mdx)$/,
};
const defaultDefineRoutes = undefined;
function flatRoutes(routeDir, defineRoutes, options = defaultOptions) {
var _a;
const routeMap = new Map();
const parentMap = new Map();
const visitor = (options === null || options === void 0 ? void 0 : options.visitFiles) || util_1.visitFiles;
const ignoredFilePatterns = (_a = options === null || options === void 0 ? void 0 : options.ignoredRouteFiles) !== null && _a !== void 0 ? _a : [];
// initialize root route
routeMap.set('root', {
path: '',
file: 'root.tsx',
name: 'root',
parentId: '',
index: false,
const routes = _flatRoutes('app', (_a = options.ignoredRouteFiles) !== null && _a !== void 0 ? _a : [], {
...options,
routeDir,
defineRoutes,
});
let routes = defineRoutes(route => {
visitor(`app/${baseDir}`, routeFile => {
let file = `app/${baseDir}/${routeFile}`;
let absoluteFile = path.resolve(file);
if (ignoredFilePatterns.some(pattern => (0, minimatch_1.default)(absoluteFile, pattern, { dot: true }))) {
return;
}
const routeInfo = getRouteInfo(baseDir, routeFile, options.basePath, options.paramPrefixChar);
if (!routeInfo)
return;
routeMap.set(routeInfo.name, routeInfo);
});
// setup parent map
for (let [name, route] of routeMap) {
if (name === 'root')
continue;
let parentRoute = getParentRoute(routeMap, name);
if (parentRoute) {
let parent = parentMap.get(parentRoute);
if (!parent) {
parent = {
routeInfo: routeMap.get(parentRoute),
children: [],
};
parentMap.set(parentRoute, parent);
}
parent.children.push(route);
}
// update undefined parentIds to 'root'
Object.values(routes).forEach(route => {
if (route.parentId === undefined) {
route.parentId = 'root';
}
// start with root
getRoutes(parentMap, 'root', route);
});
// don't return root since remix already provides it
if (routes) {
delete routes.root;
}
// HACK: Update the route ids for index routes to work around

@@ -112,98 +82,282 @@ // a bug in Remix as of v1.7.5. Need this until PR #4560 is merged.

}
function isIgnoredRouteFile(file, ignoredRouteFiles) {
return ignoredRouteFiles.some(ignoredFile => file.endsWith(ignoredFile));
}
function getParentRoute(routeMap, name) {
var parentName = name.substring(0, name.lastIndexOf('.'));
if (parentName === '') {
return 'root';
// this function uses the same signature as the one used in core remix
// this way we can continue to enhance this package and still maintain
// compatibility with remix
function _flatRoutes(appDir, ignoredFilePatternsOrOptions, options) {
var _a, _b, _c, _d;
// get options
let ignoredFilePatterns = [];
if (ignoredFilePatternsOrOptions &&
!Array.isArray(ignoredFilePatternsOrOptions)) {
options = ignoredFilePatternsOrOptions;
}
if (routeMap.has(parentName)) {
return parentName;
else {
ignoredFilePatterns = ignoredFilePatternsOrOptions !== null && ignoredFilePatternsOrOptions !== void 0 ? ignoredFilePatternsOrOptions : [];
}
return getParentRoute(routeMap, parentName);
}
function getRoutes(parentMap, parent, route) {
let parentRoute = parentMap.get(parent);
if (parentRoute && parentRoute.children) {
const routeOptions = {
caseSensitive: false,
index: parentRoute.routeInfo.index,
};
const routeChildren = () => {
for (let child of parentRoute.children) {
getRoutes(parentMap, child.name, route);
let path = child.path.substring(parentRoute.routeInfo.path.length);
if (path.startsWith('/'))
path = path.substring(1);
route(path, child.file, { index: child.index });
}
};
route(parentRoute.routeInfo.path, parentRoute.routeInfo.file, routeOptions, routeChildren);
if (!options) {
options = defaultOptions;
}
}
function getRouteInfo(baseDir, routeFile, basePath, paramsPrefixChar) {
let url = basePath !== null && basePath !== void 0 ? basePath : '';
if (url.startsWith('/')) {
url = url.substring(1);
let routeMap = new Map();
let nameMap = new Map();
let routeDirs = Array.isArray(options.routeDir)
? options.routeDir
: [(_a = options.routeDir) !== null && _a !== void 0 ? _a : 'routes'];
let defineRoutes = (_b = options.defineRoutes) !== null && _b !== void 0 ? _b : defaultDefineRoutes;
if (!defineRoutes) {
throw new Error('You must provide a defineRoutes function');
}
// get extension
let ext = path.extname(routeFile);
// only process valid route files
if (!['.js', '.jsx', '.ts', '.tsx', '.md', '.mdx'].includes(ext)) {
return null;
let visitFiles = (_c = options.visitFiles) !== null && _c !== void 0 ? _c : defaultVisitFiles;
let routeRegex = (_d = options.routeRegex) !== null && _d !== void 0 ? _d : defaultOptions.routeRegex;
for (let routeDir of routeDirs) {
visitFiles(path.join(appDir, routeDir), file => {
if (ignoredFilePatterns &&
ignoredFilePatterns.some(pattern => (0, minimatch_1.default)(file, pattern, { dot: true }))) {
return;
}
if (isRouteModuleFile(file, routeRegex)) {
let routeInfo = getRouteInfo(routeDir, file, options);
routeMap.set(routeInfo.id, routeInfo);
nameMap.set(routeInfo.name, routeInfo);
return;
}
});
}
// remove extension from name and normalize path separators
let name = routeFile
.substring(0, routeFile.length - ext.length)
.replace(path.win32.sep, '/');
if (name.includes('/')) {
// route flat-folder so only process index/layout routes
if (['/index', '/_index', '/_layout', '/_route', '.route'].every(suffix => !name.endsWith(suffix))) {
// ignore non-index routes
return null;
// update parentIds for all routes
Array.from(routeMap.values()).forEach(routeInfo => {
let parentId = findParentRouteId(routeInfo, nameMap);
routeInfo.parentId = parentId;
});
let uniqueRoutes = new Map();
// Then, recurse through all routes using the public defineRoutes() API
function defineNestedRoutes(defineRoute, parentId) {
var _a, _b, _c;
let childRoutes = Array.from(routeMap.values()).filter(routeInfo => routeInfo.parentId === parentId);
let parentRoute = parentId ? routeMap.get(parentId) : undefined;
let parentRoutePath = (_a = parentRoute === null || parentRoute === void 0 ? void 0 : parentRoute.path) !== null && _a !== void 0 ? _a : '/';
for (let childRoute of childRoutes) {
let routePath = (_c = (_b = childRoute === null || childRoute === void 0 ? void 0 : childRoute.path) === null || _b === void 0 ? void 0 : _b.slice(parentRoutePath.length)) !== null && _c !== void 0 ? _c : '';
// remove leading slash
if (routePath.startsWith('/')) {
routePath = routePath.slice(1);
}
let index = childRoute.index;
let fullPath = childRoute.path;
let uniqueRouteId = (fullPath || '') + (index ? '?index' : '');
if (uniqueRouteId) {
if (uniqueRoutes.has(uniqueRouteId)) {
throw new Error(`Path ${JSON.stringify(fullPath)} defined by route ${JSON.stringify(childRoute.id)} conflicts with route ${JSON.stringify(uniqueRoutes.get(uniqueRouteId))}`);
}
else {
uniqueRoutes.set(uniqueRouteId, childRoute.id);
}
}
if (index) {
let invalidChildRoutes = Object.values(routeMap).filter(routeInfo => routeInfo.parentId === childRoute.id);
if (invalidChildRoutes.length > 0) {
throw new Error(`Child routes are not allowed in index routes. Please remove child routes of ${childRoute.id}`);
}
defineRoute(routePath, routeMap.get(childRoute.id).file, {
index: true,
});
}
else {
defineRoute(routePath, routeMap.get(childRoute.id).file, () => {
defineNestedRoutes(defineRoute, childRoute.id);
});
}
}
if (name.endsWith('.route')) {
// convert docs/readme.route to docs.readme/_index
name = name.replace(/[\/\\]/g, '.').replace(/\.route$/, '/_index');
}
name = path.dirname(name);
}
let routeSegments = (0, util_1.getRouteSegments)(name);
for (let i = 0; i < routeSegments.length; i++) {
let routeSegment = routeSegments[i];
url = appendPathSegment(url, routeSegment, paramsPrefixChar);
let routes = defineRoutes(defineNestedRoutes);
return routes;
}
const routeModuleExts = ['.js', '.jsx', '.ts', '.tsx', '.md', '.mdx'];
const serverRegex = /\.server\.(ts|tsx|js|jsx|md|mdx)$/;
const indexRouteRegex = /((^|[.])(index|_index))(\/[^\/]+)?$|(\/_?index\/)/;
function isRouteModuleFile(filename, routeRegex) {
// flat files only need correct extension
let isFlatFile = !filename.includes(path.sep);
if (isFlatFile) {
return routeModuleExts.includes(path.extname(filename));
}
return {
path: url,
file: path.join(baseDir, routeFile),
name,
//parent: parent will be calculated after all routes are defined,
index: routeSegments.at(-1) === 'index' || routeSegments.at(-1) === '_index',
let isRoute = routeRegex.test(filename);
if (isRoute) {
// check to see if it ends in .server.tsx because you may have
// a _route.tsx and and _route.server.tsx and only the _route.tsx
// file should be considered a route
let isServer = serverRegex.test(filename);
return !isServer;
}
return false;
}
exports.isRouteModuleFile = isRouteModuleFile;
function isIndexRoute(routeId) {
return indexRouteRegex.test(routeId);
}
exports.isIndexRoute = isIndexRoute;
function getRouteInfo(routeDir, file, options) {
let filePath = path.join(routeDir, file);
let routeId = createRouteId(filePath);
let routeIdWithoutRoutes = routeId.slice(routeDir.length + 1);
let index = isIndexRoute(routeIdWithoutRoutes);
let routeSegments = getRouteSegments(routeIdWithoutRoutes, options.paramPrefixChar);
let routePath = createRoutePath(routeSegments, index, options);
let routeInfo = {
id: routeId,
path: routePath,
file: filePath,
name: routeSegments.join('/'),
segments: routeSegments,
index,
};
return routeInfo;
}
exports.getRouteInfo = getRouteInfo;
function appendPathSegment(url, segment, paramsPrefixChar = '$') {
if (segment) {
if (['index', '_index'].some(name => segment === name)) {
// index routes don't affect the the path
return url;
}
// create full path starting with /
function createRoutePath(routeSegments, index, options) {
var _a, _b;
let result = '';
let basePath = (_a = options.basePath) !== null && _a !== void 0 ? _a : '/';
let paramPrefixChar = (_b = options.paramPrefixChar) !== null && _b !== void 0 ? _b : '$';
if (index) {
// replace index with blank
routeSegments[routeSegments.length - 1] = '';
}
for (let i = 0; i < routeSegments.length; i++) {
let segment = routeSegments[i];
// skip pathless layout segments
if (segment.startsWith('_')) {
// handle pathless route (not included in url)
return url;
continue;
}
// remove trailing slash
if (segment.endsWith('_')) {
// handle parent override
segment = segment.substring(0, segment.length - 1);
segment = segment.slice(0, -1);
}
if (segment.startsWith(paramsPrefixChar)) {
// handle params
segment = segment === paramsPrefixChar ? '*' : `:${segment.substring(1)}`;
// handle param segments: $ => *, $id => :id
if (segment.startsWith(paramPrefixChar)) {
if (segment === paramPrefixChar) {
result += `/*`;
}
else {
result += `/:${segment.slice(1)}`;
}
// handle optional segments: (segment) => segment?
}
if (url)
url += '/';
url += segment;
else if (segment.startsWith('(')) {
result += `/${segment.slice(1, segment.length - 1)}?`;
}
else {
result += `/${segment}`;
}
}
return url;
if (basePath !== '/') {
result = basePath + result;
}
return result || undefined;
}
exports.createRoutePath = createRoutePath;
function findParentRouteId(routeInfo, nameMap) {
let parentName = routeInfo.segments.slice(0, -1).join('/');
while (parentName) {
if (nameMap.has(parentName)) {
return nameMap.get(parentName).id;
}
parentName = parentName.substring(0, parentName.lastIndexOf('/'));
}
return undefined;
}
function getRouteSegments(name, paramPrefixChar = '$') {
let routeSegments = [];
let index = 0;
let routeSegment = '';
let state = 'START';
let subState = 'NORMAL';
// do not remove segments ending in .route
// since these would be part of the route directory name
// docs/readme.route.tsx => docs/readme
if (!name.endsWith('.route')) {
// remove last segment since this should just be the
// route filename and we only want the directory name
// docs/_layout.tsx => docs
let last = name.lastIndexOf('/');
if (last >= 0) {
name = name.substring(0, last);
}
}
let pushRouteSegment = (routeSegment) => {
if (routeSegment) {
routeSegments.push(routeSegment);
}
};
while (index < name.length) {
let char = name[index];
switch (state) {
case 'START':
// process existing segment
if (routeSegment.includes(paramPrefixChar) &&
!routeSegment.startsWith(paramPrefixChar)) {
throw new Error(`Route params must start with prefix char ${paramPrefixChar}: ${routeSegment}`);
}
if (routeSegment.includes('(') &&
!routeSegment.startsWith('(') &&
!routeSegment.endsWith(')')) {
throw new Error(`Optional routes must start and end with parentheses: ${routeSegment}`);
}
pushRouteSegment(routeSegment);
routeSegment = '';
state = 'PATH';
continue; // restart without advancing index
case 'PATH':
if (isPathSeparator(char) && subState === 'NORMAL') {
state = 'START';
break;
}
else if (char === '[') {
subState = 'ESCAPE';
break;
}
else if (char === ']') {
subState = 'NORMAL';
break;
}
routeSegment += char;
break;
}
index++; // advance to next character
}
// process remaining segment
pushRouteSegment(routeSegment);
// strip trailing .route segment
if (routeSegments.at(-1) === 'route') {
routeSegments = routeSegments.slice(0, -1);
}
return routeSegments;
}
exports.getRouteSegments = getRouteSegments;
const pathSeparatorRegex = /[/\\.]/;
function isPathSeparator(char) {
return pathSeparatorRegex.test(char);
}
function defaultVisitFiles(dir, visitor, baseDir = dir) {
for (let filename of fs.readdirSync(dir)) {
let file = path.resolve(dir, filename);
let stat = fs.lstatSync(file);
if (stat.isDirectory()) {
defaultVisitFiles(file, visitor, baseDir);
}
else if (stat.isFile()) {
visitor(path.relative(baseDir, file));
}
}
}
exports.defaultVisitFiles = defaultVisitFiles;
function createRouteId(file) {
return normalizeSlashes(stripFileExtension(file));
}
exports.createRouteId = createRouteId;
function normalizeSlashes(file) {
return file.split(path.win32.sep).join('/');
}
exports.normalizeSlashes = normalizeSlashes;
function stripFileExtension(file) {
return file.replace(/\.[a-z0-9]+$/i, '');
}

@@ -29,6 +29,6 @@ "use strict";

const path = __importStar(require("path"));
const util_1 = require("./util");
const index_1 = require("./index");
function migrate(sourceDir, targetDir, options = { convention: 'flat-files' }) {
var visitor = options.convention === 'flat-files' ? flatFiles : flatFolders;
(0, util_1.visitFiles)(sourceDir, visitor(sourceDir, targetDir));
(0, index_1.defaultVisitFiles)(sourceDir, visitor(sourceDir, targetDir));
}

@@ -69,3 +69,3 @@ exports.migrate = migrate;

.map(pathSegment => {
const routeSegments = (0, util_1.getRouteSegments)(pathSegment);
const routeSegments = (0, index_1.getRouteSegments)(pathSegment);
return getFlatRoute(routeSegments);

@@ -72,0 +72,0 @@ })

{
"name": "remix-flat-routes",
"version": "0.4.8",
"version": "0.5.0",
"description": "Package for generating routes using flat convention",

@@ -5,0 +5,0 @@ "main": "dist/index.js",

@@ -11,2 +11,118 @@ # Remix Flat Routes

## βœ¨πŸŽ‰ New in v0.5.0
### Integration with Remix Core
Remix flat routes will be a core feature in a future version of Remix. This will be enabled using a config option.
I plan to continue to maintain this package in the future to enable enhancements that will not be in the Remix core version. To simplify maintenance, I expose all enhancements via the `options` parameter.
### Hybrid Routes
You can now use nested folders for your route names, yet still keep the colocation feature of flat routes.
If you have a large app, its not uncommon to have routes nested many levels deep. With default flat routes, the folder name is the entire route path: `some.really.long.route.edit/index.tsx`
Often you may have several parent layouts like `_public` or `admin`. Instead of having to repeat the name in every route, you can create top-level folders, then nest your routes under them. This way you can still take advantage of flat folders with colocation.
**Before**
```shell
❯ tree app/routes-folders
app/routes-folders
β”œβ”€β”€ _index
β”‚ └── page.tsx
β”œβ”€β”€ _public
β”‚ └── _layout.tsx
β”œβ”€β”€ _public.about
β”‚ └── index.tsx
β”œβ”€β”€ _public.contact[.jpg]
β”‚ └── index.tsx
β”œβ”€β”€ test.$
β”‚ β”œβ”€β”€ _route.server.tsx
β”‚ └── _route.tsx
β”œβ”€β”€ users
β”‚ β”œβ”€β”€ _layout.tsx
β”‚ └── users.css
β”œβ”€β”€ users.$userId
β”‚ β”œβ”€β”€ _route.tsx
β”‚ └── avatar.png
β”œβ”€β”€ users.$userId_.edit
β”‚ └── _route.tsx
└── users._index
└── index.tsx
```
**After**
```shell
❯ tree app/routes-hybrid
app/routes-hybrid
β”œβ”€β”€ _index
β”‚ └── index.tsx
β”œβ”€β”€ _public
β”‚ β”œβ”€β”€ _layout.tsx
β”‚ β”œβ”€β”€ about
β”‚ β”‚ └── _route.tsx
β”‚ └── contact[.jpg]
β”‚ └── _route.tsx
β”œβ”€β”€ test.$
β”‚ └── _route.tsx
└── users
β”œβ”€β”€ $userId
β”‚ β”œβ”€β”€ _route.tsx
β”‚ └── avatar.png
β”œβ”€β”€ $userId_.edit
β”‚ └── _route.tsx
β”œβ”€β”€ _index
β”‚ └── index.tsx
β”œβ”€β”€ _layout.tsx
└── users.css
```
### Extended Route Filenames
In addition to the standard `index | route | page | layout` names, any file that has a `_` prefix will be treated as the route file. This will make it easier to find a specific route instead of looking through a bunch of `index.tsx` files. This was inspired by [SolidStart](https://start.solidjs.com/core-concepts/routing) "Renaming Index" feature.
So instead of
```
_public.about/index.tsx
_public.contact/index.tsx
_public.privacy/index.tsx
```
You can name them
```
_public.about/_about.tsx
_public.contact/_contact.tsx
_public.privacy/_privacy.tsx
```
### Multiple Route Folders
You can now pass in additional route folders besides the default `routes` folder. These routes will be merged into a single namespace, so you can have routes in one folder that will use shared routes from another.
### Custom Param Prefix
You can override the default param prefix of `$`. Some shells use the `$` prefix for variables, and this can be an issue due to shell expansion. Use any character that is a valid filename, for example: `^`
```
users.^userId.tsx => users/:userId
test.^.tsx => test/*
```
### Custom Base Path
You can override the default base path of `/`. This will prepend your base path to the root path.
### Optional Route Segments
React Router will introduce a new feature for optional route segments. To use optional segments in flat routes, simply wrap your route name in `()`.
```
parent.(optional).tsx => parent/optional?
```
## πŸ›  Installation

@@ -13,0 +129,0 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚑️ by Socket Inc