@jay-framework/stack-route-scanner 0.13.0 → 0.15.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/dist/index.d.mts +4 -29
- package/dist/{index.js → index.mjs} +65 -68
- package/package.json +6 -4
package/dist/index.d.mts
CHANGED
|
@@ -14,9 +14,9 @@ type JayRoute = {
|
|
|
14
14
|
jayHtmlPath: string;
|
|
15
15
|
compPath: string;
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* Explicit params declared via <script type="application/jay-params"> in the jay-html.
|
|
18
|
+
* Used by static override routes to provide param values.
|
|
19
|
+
* e.g., /products/ceramic-flower-vase declares { slug: 'ceramic-flower-vase' }.
|
|
20
20
|
*/
|
|
21
21
|
inferredParams?: Record<string, string>;
|
|
22
22
|
};
|
|
@@ -30,33 +30,8 @@ interface ScanFilesOptions {
|
|
|
30
30
|
* Static routes come before dynamic routes at the same position.
|
|
31
31
|
*/
|
|
32
32
|
declare function sortRoutesByPriority(routes: JayRoutes): JayRoutes;
|
|
33
|
-
/**
|
|
34
|
-
* Result of param inference for logging/debugging.
|
|
35
|
-
*/
|
|
36
|
-
interface ParamInferenceResult {
|
|
37
|
-
staticRoute: string;
|
|
38
|
-
dynamicRoute: string;
|
|
39
|
-
inferredParams: Record<string, string>;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Infer params for static routes based on sibling dynamic routes.
|
|
43
|
-
*
|
|
44
|
-
* For each fully-static route, find a sibling dynamic route and map
|
|
45
|
-
* the static segment values to the param names from the dynamic route.
|
|
46
|
-
*
|
|
47
|
-
* Example:
|
|
48
|
-
* /products/ceramic-flower-vase (static)
|
|
49
|
-
* /products/[slug] (dynamic sibling)
|
|
50
|
-
* → inferredParams: { slug: 'ceramic-flower-vase' }
|
|
51
|
-
*
|
|
52
|
-
* Returns the routes with inferred params added, plus an inference log.
|
|
53
|
-
*/
|
|
54
|
-
declare function inferParamsForStaticRoutes(routes: JayRoutes): {
|
|
55
|
-
routes: JayRoutes;
|
|
56
|
-
inferenceLog: ParamInferenceResult[];
|
|
57
|
-
};
|
|
58
33
|
declare function scanRoutes(baseDir: string, options: ScanFilesOptions): Promise<JayRoutes>;
|
|
59
34
|
|
|
60
35
|
declare function routeToExpressRoute(route: JayRoute): string;
|
|
61
36
|
|
|
62
|
-
export { type JayRoute, type JayRouteParam, JayRouteParamType, type JayRouteSegment, type JayRoutes, type
|
|
37
|
+
export { type JayRoute, type JayRouteParam, JayRouteParamType, type JayRouteSegment, type JayRoutes, type ScanFilesOptions, routeToExpressRoute, scanRoutes, sortRoutesByPriority };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { promises } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { parse } from "node-html-parser";
|
|
4
|
+
import YAML from "yaml";
|
|
5
|
+
import { getLogger } from "@jay-framework/logger";
|
|
6
6
|
var JayRouteParamType = /* @__PURE__ */ ((JayRouteParamType2) => {
|
|
7
7
|
JayRouteParamType2[JayRouteParamType2["single"] = 0] = "single";
|
|
8
8
|
JayRouteParamType2[JayRouteParamType2["catchAll"] = 1] = "catchAll";
|
|
@@ -30,13 +30,22 @@ function convertToRoutePath(BASE_DIR, jayHtmlPath, { jayHtmlFilename, compFilena
|
|
|
30
30
|
}
|
|
31
31
|
async function scanDirectory(BASE_DIR, directory, options) {
|
|
32
32
|
let routes = [];
|
|
33
|
-
const items = await
|
|
33
|
+
const items = await promises.readdir(directory, { withFileTypes: true });
|
|
34
34
|
for (const item of items) {
|
|
35
35
|
const fullPath = path.join(directory, item.name);
|
|
36
36
|
if (item.isDirectory()) {
|
|
37
37
|
routes = [...routes, ...await scanDirectory(BASE_DIR, fullPath, options)];
|
|
38
38
|
} else if (item.name === options.jayHtmlFilename) {
|
|
39
39
|
const route = convertToRoutePath(BASE_DIR, fullPath, options);
|
|
40
|
+
const { params, validations } = await parseJayParams(fullPath);
|
|
41
|
+
if (params) {
|
|
42
|
+
route.inferredParams = params;
|
|
43
|
+
}
|
|
44
|
+
if (validations.length > 0) {
|
|
45
|
+
for (const v of validations) {
|
|
46
|
+
getLogger().warn(`[route-scanner] ${route.rawRoute}: ${v}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
40
49
|
routes.push(route);
|
|
41
50
|
}
|
|
42
51
|
}
|
|
@@ -78,73 +87,60 @@ function compareRoutes(a, b) {
|
|
|
78
87
|
function sortRoutesByPriority(routes) {
|
|
79
88
|
return [...routes].sort(compareRoutes);
|
|
80
89
|
}
|
|
81
|
-
function
|
|
82
|
-
|
|
90
|
+
function dedentYaml(text) {
|
|
91
|
+
const lines = text.split("\n").filter((l) => l.trim().length > 0);
|
|
92
|
+
if (lines.length === 0)
|
|
93
|
+
return "";
|
|
94
|
+
const minIndent = Math.min(...lines.map((l) => l.match(/^\s*/)?.[0].length ?? 0));
|
|
95
|
+
return lines.map((l) => l.slice(minIndent)).join("\n");
|
|
83
96
|
}
|
|
84
|
-
function
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return
|
|
90
|
-
if (staticRoute.segments.length === 0)
|
|
91
|
-
return false;
|
|
92
|
-
for (let i = 0; i < staticRoute.segments.length; i++) {
|
|
93
|
-
const staticSeg = staticRoute.segments[i];
|
|
94
|
-
const dynSeg = dynamicRoute.segments[i];
|
|
95
|
-
if (typeof staticSeg !== "string")
|
|
96
|
-
return false;
|
|
97
|
-
if (typeof dynSeg === "string") {
|
|
98
|
-
if (staticSeg !== dynSeg)
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
97
|
+
async function parseJayParams(jayHtmlPath) {
|
|
98
|
+
let content;
|
|
99
|
+
try {
|
|
100
|
+
content = await promises.readFile(jayHtmlPath, "utf-8");
|
|
101
|
+
} catch {
|
|
102
|
+
return { params: void 0, validations: [] };
|
|
101
103
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const inferenceLog = [];
|
|
106
|
-
const dynamicRoutes = routes.filter(hasDynamicSegments);
|
|
107
|
-
const enrichedRoutes = routes.map((route) => {
|
|
108
|
-
if (!isFullyStaticRoute(route))
|
|
109
|
-
return route;
|
|
110
|
-
if (route.segments.length === 0)
|
|
111
|
-
return route;
|
|
112
|
-
const dynamicSibling = dynamicRoutes.find((dyn) => dynamicRouteCouldMatch(route, dyn));
|
|
113
|
-
if (!dynamicSibling)
|
|
114
|
-
return route;
|
|
115
|
-
const inferredParams = {};
|
|
116
|
-
for (let i = 0; i < route.segments.length; i++) {
|
|
117
|
-
const staticSeg = route.segments[i];
|
|
118
|
-
const dynSeg = dynamicSibling.segments[i];
|
|
119
|
-
if (typeof staticSeg === "string" && typeof dynSeg !== "string") {
|
|
120
|
-
inferredParams[dynSeg.name] = staticSeg;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
if (Object.keys(inferredParams).length === 0)
|
|
124
|
-
return route;
|
|
125
|
-
inferenceLog.push({
|
|
126
|
-
staticRoute: route.rawRoute,
|
|
127
|
-
dynamicRoute: dynamicSibling.rawRoute,
|
|
128
|
-
inferredParams
|
|
129
|
-
});
|
|
130
|
-
return { ...route, inferredParams };
|
|
104
|
+
const root = parse(content, {
|
|
105
|
+
comment: true,
|
|
106
|
+
blockTextElements: { script: true, style: true }
|
|
131
107
|
});
|
|
132
|
-
|
|
108
|
+
const head = root.querySelector("head");
|
|
109
|
+
const paramScripts = (head ?? root).querySelectorAll('script[type="application/jay-params"]');
|
|
110
|
+
if (paramScripts.length === 0)
|
|
111
|
+
return { params: void 0, validations: [] };
|
|
112
|
+
if (paramScripts.length > 1) {
|
|
113
|
+
return {
|
|
114
|
+
params: void 0,
|
|
115
|
+
validations: [
|
|
116
|
+
'Multiple <script type="application/jay-params"> tags found — expected at most one'
|
|
117
|
+
]
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const body = dedentYaml(paramScripts[0].textContent ?? "");
|
|
121
|
+
if (!body)
|
|
122
|
+
return { params: void 0, validations: [] };
|
|
123
|
+
try {
|
|
124
|
+
return { params: YAML.parse(body), validations: [] };
|
|
125
|
+
} catch (e) {
|
|
126
|
+
return {
|
|
127
|
+
params: void 0,
|
|
128
|
+
validations: [`Failed to parse jay-params YAML: ${e.message}`]
|
|
129
|
+
};
|
|
130
|
+
}
|
|
133
131
|
}
|
|
134
132
|
async function scanRoutes(baseDir, options) {
|
|
135
133
|
const BASE_DIR = path.resolve(baseDir);
|
|
136
134
|
const routes = await scanDirectory(BASE_DIR, BASE_DIR, options);
|
|
137
135
|
const sortedRoutes = sortRoutesByPriority(routes);
|
|
138
|
-
const
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
for (const
|
|
142
|
-
|
|
143
|
-
` ${entry.staticRoute} → params from ${entry.dynamicRoute}: ${JSON.stringify(entry.inferredParams)}`
|
|
144
|
-
);
|
|
136
|
+
const routesWithParams = sortedRoutes.filter((r) => r.inferredParams);
|
|
137
|
+
if (routesWithParams.length > 0) {
|
|
138
|
+
getLogger().info("[route-scanner] Routes with explicit params (jay-params):");
|
|
139
|
+
for (const route of routesWithParams) {
|
|
140
|
+
getLogger().info(` ${route.rawRoute} → ${JSON.stringify(route.inferredParams)}`);
|
|
145
141
|
}
|
|
146
142
|
}
|
|
147
|
-
return
|
|
143
|
+
return sortedRoutes;
|
|
148
144
|
}
|
|
149
145
|
function routeToExpressRoute(route) {
|
|
150
146
|
return "/" + route.segments.map((segment) => {
|
|
@@ -160,8 +156,9 @@ function routeToExpressRoute(route) {
|
|
|
160
156
|
}
|
|
161
157
|
}).join("/");
|
|
162
158
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
159
|
+
export {
|
|
160
|
+
JayRouteParamType,
|
|
161
|
+
routeToExpressRoute,
|
|
162
|
+
scanRoutes,
|
|
163
|
+
sortRoutesByPriority
|
|
164
|
+
};
|
package/package.json
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jay-framework/stack-route-scanner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
|
-
"main": "dist/index.
|
|
5
|
+
"main": "dist/index.mjs",
|
|
6
6
|
"types": "dist/index.d.mts",
|
|
7
7
|
"files": [
|
|
8
8
|
"dist",
|
|
9
9
|
"readme.md"
|
|
10
10
|
],
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@jay-framework/logger": "^0.
|
|
12
|
+
"@jay-framework/logger": "^0.15.0",
|
|
13
|
+
"node-html-parser": "^6.1.12",
|
|
14
|
+
"yaml": "^2.3.4"
|
|
13
15
|
},
|
|
14
16
|
"scripts": {
|
|
15
17
|
"build": "npm run build:js && npm run build:types",
|
|
@@ -23,7 +25,7 @@
|
|
|
23
25
|
"test:watch": "vitest"
|
|
24
26
|
},
|
|
25
27
|
"devDependencies": {
|
|
26
|
-
"@jay-framework/dev-environment": "^0.
|
|
28
|
+
"@jay-framework/dev-environment": "^0.15.0",
|
|
27
29
|
"@types/node": "^20.11.5",
|
|
28
30
|
"nodemon": "^3.0.3",
|
|
29
31
|
"replace-in-file": "^7.1.0",
|