@jay-framework/stack-route-scanner 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # Jay Stack Route Scanner
2
+
3
+ A utility for scanning a directory structure and generating route definitions for Jay stack applications. Supports static, parameterized, optional, and catch-all routes, and can convert them to Express-compatible route strings.
4
+
5
+ ## Overview
6
+
7
+ The route-scanner walks your pages directory and produces a list of route definitions based on the folder and file structure. It is used internally by the Jay stack dev server, but can also be used standalone for custom routing logic or integrations.
8
+
9
+ ## Features
10
+
11
+ - **Static routes** (e.g. `/about`)
12
+ - **Root route** (e.g. `/`)
13
+ - **Parameterized routes** (e.g. `/user/[id]` → `/user/:id`)
14
+ - **Optional parameter routes** (e.g. `/blog/[[slug]]` → `/blog/:slug?`)
15
+ - **Catch-all routes** (e.g. `/files/[...path]` → `/files/:path*`)
16
+ - **Nested routes** (arbitrary nesting of the above)
17
+ - **Express route conversion**
18
+
19
+ ## Usage
20
+
21
+ ```ts
22
+ import { scanRoutes, routeToExpressRoute } from '@jay-framework/stack-route-scanner';
23
+
24
+ const routes = await scanRoutes('./src/pages', {
25
+ jayHtmlFilename: 'page.jay-html',
26
+ compFilename: 'page.ts',
27
+ });
28
+
29
+ routes.forEach((route) => {
30
+ console.log('Route:', route.rawRoute, 'Express:', routeToExpressRoute(route));
31
+ });
32
+ ```
33
+
34
+ ## Supported Routing Patterns
35
+
36
+ Given a directory structure like:
37
+
38
+ ```
39
+ src/pages/
40
+ ├── page.jay-html // root route ('/')
41
+ ├── about/
42
+ │ └── page.jay-html // '/about'
43
+ ├── user/
44
+ │ └── [id]/
45
+ │ └── page.jay-html // '/user/[id]' → '/user/:id'
46
+ ├── blog/
47
+ │ └── [[slug]]/
48
+ │ └── page.jay-html // '/blog/[[slug]]' → '/blog/:slug?'
49
+ ├── files/
50
+ │ └── [...path]/
51
+ │ └── page.jay-html // '/files/[...path]' → '/files/:path*'
52
+ ```
53
+
54
+ ### Segment Syntax
55
+
56
+ - `segment/` — static segment (e.g. `/about`)
57
+ - `[param]/` — required parameter (e.g. `/user/:id`)
58
+ - `[[param]]/` — optional parameter (e.g. `/blog/:slug?`)
59
+ - `[...param]/` — catch-all parameter (e.g. `/files/:path*`)
60
+
61
+ ### Example Output
62
+
63
+ For the above structure, `scanRoutes` will produce route objects like:
64
+
65
+ ```js
66
+ [
67
+ { rawRoute: '', segments: [] }, // '/'
68
+ { rawRoute: '/about', segments: ['about'] },
69
+ { rawRoute: '/user/[id]', segments: ['user', { name: 'id', type: 'single' }] },
70
+ { rawRoute: '/blog/[[slug]]', segments: ['blog', { name: 'slug', type: 'optional' }] },
71
+ { rawRoute: '/files/[...path]', segments: ['files', { name: 'path', type: 'catchAll' }] },
72
+ ];
73
+ ```
74
+
75
+ And `routeToExpressRoute(route)` will convert these to:
76
+
77
+ - `/` (root)
78
+ - `/about`
79
+ - `/user/:id`
80
+ - `/blog/:slug?`
81
+ - `/files/:path*`
82
+
83
+ ## API
84
+
85
+ ### `scanRoutes(rootDir, options)`
86
+
87
+ - `rootDir`: Path to the pages directory
88
+ - `options.jayHtmlFilename`: Name of the HTML file to look for (default: `page.jay-html`)
89
+ - `options.compFilename`: Name of the component file to look for (default: `page.ts`)
90
+ - Returns: Array of route objects with `compPath`, `jayHtmlPath`, `rawRoute`, and `segments`
91
+
92
+ ### `routeToExpressRoute(route)`
93
+
94
+ - Converts a route object to an Express-compatible route string
@@ -0,0 +1,26 @@
1
+ declare enum JayRouteParamType {
2
+ single = 0,
3
+ catchAll = 1,
4
+ optional = 2
5
+ }
6
+ interface JayRouteParam {
7
+ name: string;
8
+ type: JayRouteParamType;
9
+ }
10
+ type JayRouteSegment = string | JayRouteParam;
11
+ type JayRoute = {
12
+ segments: JayRouteSegment[];
13
+ rawRoute: string;
14
+ jayHtmlPath: string;
15
+ compPath: string;
16
+ };
17
+ type JayRoutes = JayRoute[];
18
+ interface ScanFilesOptions {
19
+ jayHtmlFilename: string;
20
+ compFilename: string;
21
+ }
22
+ declare function scanRoutes(baseDir: string, options: ScanFilesOptions): Promise<JayRoutes>;
23
+
24
+ declare function routeToExpressRoute(route: JayRoute): string;
25
+
26
+ export { type JayRoute, type JayRouteParam, JayRouteParamType, type JayRouteSegment, type JayRoutes, type ScanFilesOptions, routeToExpressRoute, scanRoutes };
package/dist/index.js ADDED
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ var JayRouteParamType = /* @__PURE__ */ ((JayRouteParamType2) => {
6
+ JayRouteParamType2[JayRouteParamType2["single"] = 0] = "single";
7
+ JayRouteParamType2[JayRouteParamType2["catchAll"] = 1] = "catchAll";
8
+ JayRouteParamType2[JayRouteParamType2["optional"] = 2] = "optional";
9
+ return JayRouteParamType2;
10
+ })(JayRouteParamType || {});
11
+ const PARSE_PARAM = /^\[(\[)?(\.\.\.)?([^\]]+)\]?\]$/;
12
+ function convertToRoutePath(BASE_DIR, jayHtmlPath, { jayHtmlFilename, compFilename }) {
13
+ let rawRoute = jayHtmlPath.replace(BASE_DIR, "").replace(`/${jayHtmlFilename}`, "").replace("\\", "/");
14
+ const segments = rawRoute.split("/").filter((segment) => segment.length > 0).map((segment) => {
15
+ const match = segment.match(PARSE_PARAM);
16
+ if (!match)
17
+ return segment;
18
+ const isOptional = !!match[1];
19
+ const isCatchAll = !!match[2];
20
+ const name = match[3];
21
+ return {
22
+ name,
23
+ type: isOptional ? 2 : isCatchAll ? 1 : 0
24
+ /* single */
25
+ };
26
+ });
27
+ const compPath = jayHtmlPath.replace(jayHtmlFilename, compFilename);
28
+ return { segments, jayHtmlPath, compPath, rawRoute };
29
+ }
30
+ async function scanDirectory(BASE_DIR, directory, options) {
31
+ let routes = [];
32
+ const items = await fs.promises.readdir(directory, { withFileTypes: true });
33
+ for (const item of items) {
34
+ const fullPath = path.join(directory, item.name);
35
+ if (item.isDirectory()) {
36
+ routes = [...routes, ...await scanDirectory(BASE_DIR, fullPath, options)];
37
+ } else if (item.name === options.jayHtmlFilename) {
38
+ const route = convertToRoutePath(BASE_DIR, fullPath, options);
39
+ routes.push(route);
40
+ }
41
+ }
42
+ return routes;
43
+ }
44
+ async function scanRoutes(baseDir, options) {
45
+ const BASE_DIR = path.resolve(baseDir);
46
+ return await scanDirectory(BASE_DIR, BASE_DIR, options);
47
+ }
48
+ function routeToExpressRoute(route) {
49
+ return "/" + route.segments.map((segment) => {
50
+ if (typeof segment === "string")
51
+ return segment;
52
+ else {
53
+ if (segment.type === JayRouteParamType.single)
54
+ return `:${segment.name}`;
55
+ else if (segment.type === JayRouteParamType.optional)
56
+ return `:${segment.name}?`;
57
+ else
58
+ return `:${segment.name}*`;
59
+ }
60
+ }).join("/");
61
+ }
62
+ exports.JayRouteParamType = JayRouteParamType;
63
+ exports.routeToExpressRoute = routeToExpressRoute;
64
+ exports.scanRoutes = scanRoutes;
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@jay-framework/stack-route-scanner",
3
+ "version": "0.5.0",
4
+ "license": "Apache-2.0",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.mts",
7
+ "files": [
8
+ "dist",
9
+ "readme.md"
10
+ ],
11
+ "scripts": {
12
+ "build": "npm run build:js && npm run build:types",
13
+ "build:watch": "npm run build:js -- --watch & npm run build:types -- --watch",
14
+ "build:js": "vite build",
15
+ "build:types": "tsup lib/index.ts --dts-only --format esm",
16
+ "build:check-types": "tsc",
17
+ "clean": "rimraf dist",
18
+ "confirm": "npm run clean && npm run build && npm run build:check-types && npm run test",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest"
21
+ },
22
+ "devDependencies": {
23
+ "@jay-framework/dev-environment": "workspace:^",
24
+ "@types/node": "^20.11.5",
25
+ "nodemon": "^3.0.3",
26
+ "replace-in-file": "^7.1.0",
27
+ "rimraf": "^5.0.5",
28
+ "tsup": "^8.0.1",
29
+ "typescript": "^5.3.3",
30
+ "vite": "^5.0.11",
31
+ "vitest": "^1.2.1"
32
+ }
33
+ }