create-backlist 6.0.5 → 6.0.6
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/package.json +1 -1
- package/src/analyzer.js +108 -170
- package/src/scanner/analyzeFrontend.js +36 -7
package/package.json
CHANGED
package/src/analyzer.js
CHANGED
|
@@ -27,7 +27,6 @@ function readJSONSafe(p) {
|
|
|
27
27
|
// AUTH detection (for addAuth)
|
|
28
28
|
// -------------------------
|
|
29
29
|
function findAuthUsageInRepo(rootDir) {
|
|
30
|
-
// 1) package.json quick check
|
|
31
30
|
const pkgPath = path.join(rootDir, "package.json");
|
|
32
31
|
const pkg = readJSONSafe(pkgPath) || {};
|
|
33
32
|
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
@@ -44,7 +43,6 @@ function findAuthUsageInRepo(rootDir) {
|
|
|
44
43
|
|
|
45
44
|
if (authDeps.some((d) => deps[d])) return true;
|
|
46
45
|
|
|
47
|
-
// 2) Source scan for common auth identifiers
|
|
48
46
|
const scanDirs = ["src", "app", "pages", "components", "lib", "utils"]
|
|
49
47
|
.map((d) => path.join(rootDir, d))
|
|
50
48
|
.filter((d) => fs.existsSync(d));
|
|
@@ -86,7 +84,7 @@ function findAuthUsageInRepo(rootDir) {
|
|
|
86
84
|
}
|
|
87
85
|
|
|
88
86
|
// -------------------------
|
|
89
|
-
//
|
|
87
|
+
// Helper functions for frontend API scan
|
|
90
88
|
// -------------------------
|
|
91
89
|
function toTitleCase(str) {
|
|
92
90
|
if (!str) return "Default";
|
|
@@ -100,61 +98,64 @@ function normalizeRouteForBackend(urlValue) {
|
|
|
100
98
|
return urlValue.replace(/\{(\w+)\}/g, ":$1");
|
|
101
99
|
}
|
|
102
100
|
|
|
103
|
-
function
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return "Number";
|
|
110
|
-
case "BooleanLiteral":
|
|
111
|
-
return "Boolean";
|
|
112
|
-
case "NullLiteral":
|
|
113
|
-
return "String";
|
|
114
|
-
default:
|
|
115
|
-
return "String";
|
|
116
|
-
}
|
|
101
|
+
function extractPathParams(route) {
|
|
102
|
+
const params = [];
|
|
103
|
+
const re = /[:{]([a-zA-Z0-9_]+)[}]/g;
|
|
104
|
+
let m;
|
|
105
|
+
while ((m = re.exec(route))) params.push(m[1]);
|
|
106
|
+
return Array.from(new Set(params));
|
|
117
107
|
}
|
|
118
108
|
|
|
119
|
-
function
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
109
|
+
function extractQueryParamsFromUrl(urlValue) {
|
|
110
|
+
try {
|
|
111
|
+
const qIndex = urlValue.indexOf("?");
|
|
112
|
+
if (qIndex === -1) return [];
|
|
113
|
+
const qs = urlValue.slice(qIndex + 1);
|
|
114
|
+
return qs
|
|
115
|
+
.split("&")
|
|
116
|
+
.map((p) => p.split("=")[0])
|
|
117
|
+
.filter(Boolean);
|
|
118
|
+
} catch {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
132
122
|
|
|
133
|
-
|
|
134
|
-
|
|
123
|
+
function deriveControllerNameFromUrl(urlValue) {
|
|
124
|
+
const parts = String(urlValue || "").split("/").filter(Boolean);
|
|
125
|
+
const apiIndex = parts.indexOf("api");
|
|
126
|
+
let seg = null;
|
|
127
|
+
if (apiIndex >= 0) {
|
|
128
|
+
seg = parts[apiIndex + 1] || null;
|
|
129
|
+
if (seg && /^v\d+$/i.test(seg)) {
|
|
130
|
+
seg = parts[apiIndex + 2] || seg;
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
seg = parts[0] || null;
|
|
135
134
|
}
|
|
136
|
-
return
|
|
135
|
+
return toTitleCase(seg);
|
|
137
136
|
}
|
|
138
137
|
|
|
139
|
-
function
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (!declPath || !declPath.node) return null;
|
|
138
|
+
function deriveActionName(method, route) {
|
|
139
|
+
const cleaned = String(route).replace(/^\/api\//, "/").replace(/[/:{}-]/g, " ");
|
|
140
|
+
const last = cleaned.trim().split(/\s+/).filter(Boolean).pop() || "Action";
|
|
141
|
+
return `${String(method).toLowerCase()}${toTitleCase(last)}`;
|
|
142
|
+
}
|
|
145
143
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
144
|
+
// -------------------------
|
|
145
|
+
// Axios/Fetch detection
|
|
146
|
+
// -------------------------
|
|
147
|
+
function detectAxiosLikeMethod(node) {
|
|
148
|
+
if (!node.callee || node.callee.type !== "MemberExpression") return null;
|
|
149
|
+
const prop = node.callee.property;
|
|
150
|
+
if (!prop || prop.type !== "Identifier") return null;
|
|
151
|
+
const name = prop.name.toLowerCase();
|
|
152
|
+
if (!HTTP_METHODS.has(name)) return null;
|
|
153
|
+
return name.toUpperCase();
|
|
151
154
|
}
|
|
152
155
|
|
|
153
156
|
function getUrlValue(urlNode) {
|
|
154
157
|
if (!urlNode) return null;
|
|
155
|
-
|
|
156
158
|
if (urlNode.type === "StringLiteral") return urlNode.value;
|
|
157
|
-
|
|
158
159
|
if (urlNode.type === "TemplateLiteral") {
|
|
159
160
|
const quasis = urlNode.quasis || [];
|
|
160
161
|
const exprs = urlNode.expressions || [];
|
|
@@ -168,81 +169,52 @@ function getUrlValue(urlNode) {
|
|
|
168
169
|
}
|
|
169
170
|
return out;
|
|
170
171
|
}
|
|
171
|
-
|
|
172
172
|
return null;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const apiIndex = parts.indexOf("api");
|
|
186
|
-
|
|
187
|
-
let seg = null;
|
|
188
|
-
|
|
189
|
-
if (apiIndex >= 0) {
|
|
190
|
-
seg = parts[apiIndex + 1] || null;
|
|
191
|
-
if (seg && /^v\d+$/i.test(seg)) {
|
|
192
|
-
seg = parts[apiIndex + 2] || seg;
|
|
193
|
-
}
|
|
194
|
-
} else {
|
|
195
|
-
seg = parts[0] || null;
|
|
175
|
+
// -------------------------
|
|
176
|
+
// Extract request schema (simple)
|
|
177
|
+
function inferTypeFromNode(node) {
|
|
178
|
+
if (!node) return "String";
|
|
179
|
+
switch (node.type) {
|
|
180
|
+
case "StringLiteral": return "String";
|
|
181
|
+
case "NumericLiteral": return "Number";
|
|
182
|
+
case "BooleanLiteral": return "Boolean";
|
|
183
|
+
case "NullLiteral": return "String";
|
|
184
|
+
default: return "String";
|
|
196
185
|
}
|
|
197
|
-
|
|
198
|
-
return toTitleCase(seg);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function deriveActionName(method, route) {
|
|
202
|
-
const cleaned = String(route).replace(/^\/api\//, "/").replace(/[/:{}-]/g, " ");
|
|
203
|
-
const last = cleaned.trim().split(/\s+/).filter(Boolean).pop() || "Action";
|
|
204
|
-
return `${String(method).toLowerCase()}${toTitleCase(last)}`;
|
|
205
186
|
}
|
|
206
187
|
|
|
207
|
-
function
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
188
|
+
function extractObjectSchema(objExpr) {
|
|
189
|
+
const schema = {};
|
|
190
|
+
if (!objExpr || objExpr.type !== "ObjectExpression") return null;
|
|
191
|
+
for (const prop of objExpr.properties) {
|
|
192
|
+
if (prop.type !== "ObjectProperty") continue;
|
|
193
|
+
const key = prop.key.type === "Identifier" ? prop.key.name : prop.key.value;
|
|
194
|
+
schema[key] = inferTypeFromNode(prop.value);
|
|
195
|
+
}
|
|
196
|
+
return schema;
|
|
213
197
|
}
|
|
214
198
|
|
|
215
|
-
function
|
|
199
|
+
function resolveIdentifierToInit(pathObj, identifierName) {
|
|
216
200
|
try {
|
|
217
|
-
const
|
|
218
|
-
if (
|
|
219
|
-
const
|
|
220
|
-
return
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
.filter(Boolean);
|
|
201
|
+
const binding = pathObj.scope.getBinding(identifierName);
|
|
202
|
+
if (!binding) return null;
|
|
203
|
+
const decl = binding.path.node;
|
|
204
|
+
if (!decl) return null;
|
|
205
|
+
if (decl.type === "VariableDeclarator") return decl.init || null;
|
|
206
|
+
return null;
|
|
224
207
|
} catch {
|
|
225
|
-
return
|
|
208
|
+
return null;
|
|
226
209
|
}
|
|
227
210
|
}
|
|
228
211
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const prop = node.callee.property;
|
|
233
|
-
if (!prop || prop.type !== "Identifier") return null;
|
|
234
|
-
|
|
235
|
-
const name = prop.name.toLowerCase();
|
|
236
|
-
if (!HTTP_METHODS.has(name)) return null;
|
|
237
|
-
|
|
238
|
-
return name.toUpperCase();
|
|
239
|
-
}
|
|
240
|
-
|
|
212
|
+
// -------------------------
|
|
213
|
+
// Main frontend scanner
|
|
214
|
+
// -------------------------
|
|
241
215
|
async function analyzeFrontend(srcPath) {
|
|
242
216
|
if (!srcPath) throw new Error("analyzeFrontend: srcPath is required");
|
|
243
|
-
if (!fs.existsSync(srcPath)) {
|
|
244
|
-
throw new Error(`The source directory '${srcPath}' does not exist.`);
|
|
245
|
-
}
|
|
217
|
+
if (!fs.existsSync(srcPath)) throw new Error(`Source dir '${srcPath}' does not exist`);
|
|
246
218
|
|
|
247
219
|
const files = await glob(`${normalizeSlashes(srcPath)}/**/*.{js,ts,jsx,tsx}`, {
|
|
248
220
|
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**", "**/coverage/**"],
|
|
@@ -252,26 +224,15 @@ async function analyzeFrontend(srcPath) {
|
|
|
252
224
|
|
|
253
225
|
for (const file of files) {
|
|
254
226
|
let code;
|
|
255
|
-
try {
|
|
256
|
-
code = await fs.readFile(file, "utf-8");
|
|
257
|
-
} catch {
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
|
|
227
|
+
try { code = await fs.readFile(file, "utf-8"); } catch { continue; }
|
|
261
228
|
let ast;
|
|
262
|
-
try {
|
|
263
|
-
ast = parser.parse(code, { sourceType: "module", plugins: ["jsx", "typescript"] });
|
|
264
|
-
} catch {
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
229
|
+
try { ast = parser.parse(code, { sourceType: "module", plugins: ["jsx","typescript"] }); } catch { continue; }
|
|
267
230
|
|
|
268
231
|
traverse(ast, {
|
|
269
232
|
CallExpression(callPath) {
|
|
270
233
|
const node = callPath.node;
|
|
271
|
-
|
|
272
234
|
const isFetch = node.callee.type === "Identifier" && node.callee.name === "fetch";
|
|
273
235
|
const axiosMethod = detectAxiosLikeMethod(node);
|
|
274
|
-
|
|
275
236
|
if (!isFetch && !axiosMethod) return;
|
|
276
237
|
|
|
277
238
|
let urlValue = null;
|
|
@@ -281,44 +242,19 @@ async function analyzeFrontend(srcPath) {
|
|
|
281
242
|
if (isFetch) {
|
|
282
243
|
urlValue = getUrlValue(node.arguments[0]);
|
|
283
244
|
const optionsNode = node.arguments[1];
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (methodProp && methodProp.value.type === "StringLiteral") {
|
|
293
|
-
method = methodProp.value.value.toUpperCase();
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
if (["POST", "PUT", "PATCH"].includes(method)) {
|
|
297
|
-
const bodyProp = optionsNode.properties.find(
|
|
298
|
-
(p) =>
|
|
299
|
-
p.type === "ObjectProperty" &&
|
|
300
|
-
p.key.type === "Identifier" &&
|
|
301
|
-
p.key.name === "body"
|
|
302
|
-
);
|
|
303
|
-
|
|
304
|
-
if (bodyProp) {
|
|
305
|
-
const v = bodyProp.value;
|
|
306
|
-
|
|
307
|
-
if (
|
|
308
|
-
v.type === "CallExpression" &&
|
|
309
|
-
v.callee.type === "MemberExpression" &&
|
|
310
|
-
v.callee.object.type === "Identifier" &&
|
|
311
|
-
v.callee.object.name === "JSON" &&
|
|
312
|
-
v.callee.property.type === "Identifier" &&
|
|
313
|
-
v.callee.property.name === "stringify"
|
|
314
|
-
) {
|
|
245
|
+
if (optionsNode?.type === "ObjectExpression") {
|
|
246
|
+
const mProp = optionsNode.properties.find(p => p.key?.name==="method");
|
|
247
|
+
if (mProp?.value?.type==="StringLiteral") method = mProp.value.value.toUpperCase();
|
|
248
|
+
if (["POST","PUT","PATCH"].includes(method)) {
|
|
249
|
+
const bProp = optionsNode.properties.find(p => p.key?.name==="body");
|
|
250
|
+
if (bProp) {
|
|
251
|
+
const v = bProp.value;
|
|
252
|
+
if (v.type==="CallExpression" && v.callee.object?.name==="JSON" && v.callee.property?.name==="stringify") {
|
|
315
253
|
const arg0 = v.arguments[0];
|
|
316
|
-
|
|
317
|
-
if (arg0?.type
|
|
318
|
-
schemaFields = extractObjectSchema(arg0);
|
|
319
|
-
} else if (arg0?.type === "Identifier") {
|
|
254
|
+
if (arg0?.type==="ObjectExpression") schemaFields = extractObjectSchema(arg0);
|
|
255
|
+
else if (arg0?.type==="Identifier") {
|
|
320
256
|
const init = resolveIdentifierToInit(callPath, arg0.name);
|
|
321
|
-
if (init?.type
|
|
257
|
+
if (init?.type==="ObjectExpression") schemaFields = extractObjectSchema(init);
|
|
322
258
|
}
|
|
323
259
|
}
|
|
324
260
|
}
|
|
@@ -329,26 +265,24 @@ async function analyzeFrontend(srcPath) {
|
|
|
329
265
|
if (axiosMethod) {
|
|
330
266
|
method = axiosMethod;
|
|
331
267
|
urlValue = getUrlValue(node.arguments[0]);
|
|
332
|
-
|
|
333
|
-
if (["POST", "PUT", "PATCH"].includes(method)) {
|
|
268
|
+
if (["POST","PUT","PATCH"].includes(method)) {
|
|
334
269
|
const dataArg = node.arguments[1];
|
|
335
|
-
if (dataArg?.type
|
|
336
|
-
|
|
337
|
-
} else if (dataArg?.type === "Identifier") {
|
|
270
|
+
if (dataArg?.type==="ObjectExpression") schemaFields = extractObjectSchema(dataArg);
|
|
271
|
+
else if (dataArg?.type==="Identifier") {
|
|
338
272
|
const init = resolveIdentifierToInit(callPath, dataArg.name);
|
|
339
|
-
if (init?.type
|
|
273
|
+
if (init?.type==="ObjectExpression") schemaFields = extractObjectSchema(init);
|
|
340
274
|
}
|
|
341
275
|
}
|
|
342
276
|
}
|
|
343
277
|
|
|
344
|
-
const apiPath =
|
|
278
|
+
const apiPath = urlValue?.includes("/api/") ? urlValue.slice(urlValue.indexOf("/api/")) : null;
|
|
345
279
|
if (!apiPath) return;
|
|
346
280
|
|
|
347
281
|
const route = normalizeRouteForBackend(apiPath.split("?")[0]);
|
|
348
282
|
const controllerName = deriveControllerNameFromUrl(apiPath);
|
|
349
283
|
const actionName = deriveActionName(method, route);
|
|
350
|
-
|
|
351
284
|
const key = `${method}:${route}`;
|
|
285
|
+
|
|
352
286
|
if (!endpoints.has(key)) {
|
|
353
287
|
endpoints.set(key, {
|
|
354
288
|
path: apiPath,
|
|
@@ -358,12 +292,11 @@ async function analyzeFrontend(srcPath) {
|
|
|
358
292
|
actionName,
|
|
359
293
|
pathParams: extractPathParams(route),
|
|
360
294
|
queryParams: extractQueryParamsFromUrl(apiPath),
|
|
361
|
-
schemaFields,
|
|
362
295
|
requestBody: schemaFields ? { fields: schemaFields } : null,
|
|
363
296
|
sourceFile: normalizeSlashes(file),
|
|
364
297
|
});
|
|
365
298
|
}
|
|
366
|
-
}
|
|
299
|
+
}
|
|
367
300
|
});
|
|
368
301
|
}
|
|
369
302
|
|
|
@@ -371,20 +304,25 @@ async function analyzeFrontend(srcPath) {
|
|
|
371
304
|
}
|
|
372
305
|
|
|
373
306
|
// -------------------------
|
|
374
|
-
// Main analyze()
|
|
307
|
+
// Main analyze() for CLI
|
|
375
308
|
// -------------------------
|
|
376
|
-
function analyze(projectRoot = process.cwd()) {
|
|
309
|
+
async function analyze(projectRoot = process.cwd()) {
|
|
377
310
|
const rootDir = path.resolve(projectRoot);
|
|
311
|
+
const frontendSrc = ["src", "app", "pages"]
|
|
312
|
+
.map(d => path.join(rootDir,d))
|
|
313
|
+
.find(d => fs.existsSync(d));
|
|
314
|
+
|
|
315
|
+
const endpoints = frontendSrc ? await analyzeFrontend(frontendSrc) : [];
|
|
378
316
|
|
|
379
317
|
return {
|
|
380
318
|
rootDir: normalizeSlashes(rootDir),
|
|
381
319
|
hasAuth: findAuthUsageInRepo(rootDir),
|
|
382
|
-
// If CLI expects addAuth directly, keep both:
|
|
383
320
|
addAuth: findAuthUsageInRepo(rootDir),
|
|
321
|
+
endpoints
|
|
384
322
|
};
|
|
385
323
|
}
|
|
386
324
|
|
|
387
325
|
module.exports = {
|
|
388
326
|
analyze,
|
|
389
|
-
analyzeFrontend
|
|
390
|
-
};
|
|
327
|
+
analyzeFrontend
|
|
328
|
+
};
|
|
@@ -6,7 +6,7 @@ const path = require("path");
|
|
|
6
6
|
* Normalize paths (Windows → Unix style)
|
|
7
7
|
*/
|
|
8
8
|
function normalizeSlashes(p) {
|
|
9
|
-
return p.replace(/\\/g, "/");
|
|
9
|
+
return String(p || "").replace(/\\/g, "/");
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -40,6 +40,27 @@ function findAuthUsageInRepo(rootDir) {
|
|
|
40
40
|
return found;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Derive a controller name from URL
|
|
45
|
+
*/
|
|
46
|
+
function deriveControllerName(urlPath) {
|
|
47
|
+
if (!urlPath) return "Default";
|
|
48
|
+
const parts = urlPath.split("/").filter(Boolean);
|
|
49
|
+
const apiIndex = parts.indexOf("api");
|
|
50
|
+
if (apiIndex >= 0) return parts[apiIndex + 1] || "Default";
|
|
51
|
+
return parts[0] || "Default";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Derive an action name from method + URL
|
|
56
|
+
*/
|
|
57
|
+
function deriveActionName(method, urlPath) {
|
|
58
|
+
if (!urlPath) return `${method.toLowerCase()}Action`;
|
|
59
|
+
const cleaned = urlPath.replace(/^\/api\//, "/").replace(/[/:{}-]/g, " ");
|
|
60
|
+
const lastSegment = cleaned.trim().split(/\s+/).filter(Boolean).pop() || "Action";
|
|
61
|
+
return `${method.toLowerCase()}${lastSegment.charAt(0).toUpperCase()}${lastSegment.slice(1)}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
43
64
|
/**
|
|
44
65
|
* Analyze frontend source to extract API endpoints
|
|
45
66
|
* (axios / fetch based – simple & safe)
|
|
@@ -63,23 +84,31 @@ function analyzeFrontend(frontendDir) {
|
|
|
63
84
|
) {
|
|
64
85
|
const content = fs.readFileSync(fullPath, "utf8");
|
|
65
86
|
|
|
66
|
-
// axios
|
|
87
|
+
// --- axios ---
|
|
67
88
|
const axiosRegex = /axios\.(get|post|put|delete|patch)\(\s*['"`](.*?)['"`]/g;
|
|
68
89
|
let match;
|
|
69
90
|
while ((match = axiosRegex.exec(content)) !== null) {
|
|
91
|
+
const url = match[2].startsWith("/") ? match[2] : "/" + match[2];
|
|
92
|
+
const method = match[1].toUpperCase();
|
|
70
93
|
endpoints.push({
|
|
71
|
-
method
|
|
72
|
-
path:
|
|
94
|
+
method,
|
|
95
|
+
path: url,
|
|
96
|
+
controllerName: deriveControllerName(url),
|
|
97
|
+
actionName: deriveActionName(method, url),
|
|
73
98
|
source: normalizeSlashes(fullPath)
|
|
74
99
|
});
|
|
75
100
|
}
|
|
76
101
|
|
|
77
|
-
// fetch
|
|
102
|
+
// --- fetch ---
|
|
78
103
|
const fetchRegex = /fetch\(\s*['"`](.*?)['"`]/g;
|
|
79
104
|
while ((match = fetchRegex.exec(content)) !== null) {
|
|
105
|
+
const url = match[1].startsWith("/") ? match[1] : "/" + match[1];
|
|
106
|
+
const method = "GET";
|
|
80
107
|
endpoints.push({
|
|
81
|
-
method
|
|
82
|
-
path:
|
|
108
|
+
method,
|
|
109
|
+
path: url,
|
|
110
|
+
controllerName: deriveControllerName(url),
|
|
111
|
+
actionName: deriveActionName(method, url),
|
|
83
112
|
source: normalizeSlashes(fullPath)
|
|
84
113
|
});
|
|
85
114
|
}
|