@suss/packs 0.26.0 → 0.28.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/activerecord.d.ts +1 -0
- package/dist/axios.js +44 -3
- package/dist/chunk-SKGM3QL5.js +101 -0
- package/dist/express.js +11 -1
- package/dist/fastify.js +0 -1
- package/dist/prisma.js +14 -14
- package/dist/rails.js +614 -122
- package/dist/sqlalchemy.js +5 -93
- package/dist/sqlmodel.d.ts +1 -0
- package/dist/sqlmodel.js +113 -0
- package/package.json +15 -10
package/dist/rails.js
CHANGED
|
@@ -1,35 +1,177 @@
|
|
|
1
1
|
// ../framework/rails/dist/index.js
|
|
2
|
-
import
|
|
2
|
+
import fs4 from "fs";
|
|
3
|
+
import path4 from "path";
|
|
3
4
|
import { z } from "zod";
|
|
4
|
-
import { underscoreConstantPath } from "@suss/adapter-ruby";
|
|
5
|
+
import { underscoreConstantPath as underscoreConstantPath2 } from "@suss/adapter-ruby";
|
|
5
6
|
import fs from "fs";
|
|
7
|
+
import path from "path";
|
|
6
8
|
import {
|
|
7
9
|
bodyStatements,
|
|
8
10
|
field,
|
|
9
|
-
hashKeySymbolName,
|
|
10
11
|
parseRubySync,
|
|
12
|
+
underscoreConstantPath
|
|
13
|
+
} from "@suss/adapter-ruby";
|
|
14
|
+
import fs2 from "fs";
|
|
15
|
+
import path2 from "path";
|
|
16
|
+
import fs3 from "fs";
|
|
17
|
+
import path3 from "path";
|
|
18
|
+
import {
|
|
19
|
+
bodyStatements as bodyStatements2,
|
|
20
|
+
field as field2,
|
|
21
|
+
hashKeySymbolName,
|
|
22
|
+
nestedStatements,
|
|
23
|
+
parseRubySync as parseRubySync2,
|
|
11
24
|
stringValueOf,
|
|
12
25
|
symbolValue
|
|
13
26
|
} from "@suss/adapter-ruby";
|
|
14
|
-
|
|
15
|
-
|
|
27
|
+
var ENGINE_BASE_NAMES = /* @__PURE__ */ new Set(["Rails::Engine", "::Rails::Engine"]);
|
|
28
|
+
var CONSTANT_TYPES = /* @__PURE__ */ new Set(["constant", "scope_resolution"]);
|
|
29
|
+
function readEngines(roots, acronyms = []) {
|
|
30
|
+
const engines = [];
|
|
31
|
+
for (const root of roots) {
|
|
32
|
+
const routesFile = engineRoutesFile(root);
|
|
33
|
+
for (const file of engineFilesUnder(root)) {
|
|
34
|
+
const source = fs.readFileSync(file, "utf8");
|
|
35
|
+
const rootNode = parseRubySync(source).rootNode;
|
|
36
|
+
for (const found of engineClassesIn(rootNode, acronyms)) {
|
|
37
|
+
engines.push({ ...found, routesFile });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return engines;
|
|
42
|
+
}
|
|
43
|
+
function engineSourceFiles(root) {
|
|
44
|
+
const routesFile = engineRoutesFile(root);
|
|
45
|
+
return [
|
|
46
|
+
...engineFilesUnder(root),
|
|
47
|
+
...routesFile === null ? [] : [routesFile]
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
function engineRoutesFile(root) {
|
|
51
|
+
const routesFile = path.join(root, "config", "routes.rb");
|
|
52
|
+
return fs.existsSync(routesFile) ? routesFile : null;
|
|
53
|
+
}
|
|
54
|
+
function engineFilesUnder(root) {
|
|
55
|
+
const directory = path.join(root, "lib");
|
|
56
|
+
if (!fs.existsSync(directory)) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
return rubyFilesUnder(directory).filter(
|
|
60
|
+
(file) => fs.readFileSync(file, "utf8").includes("Rails::Engine")
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
function expandPathPattern(pattern) {
|
|
64
|
+
const segments = pattern.split(path.sep).filter((s) => s !== "");
|
|
65
|
+
const start = path.isAbsolute(pattern) ? path.sep : ".";
|
|
66
|
+
let candidates = [start];
|
|
67
|
+
for (const segment of segments) {
|
|
68
|
+
if (!segment.includes("*")) {
|
|
69
|
+
candidates = candidates.map((base) => path.join(base, segment));
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const matcher = new RegExp(
|
|
73
|
+
`^${segment.split("*").map(escapeRegExp).join(".*")}$`
|
|
74
|
+
);
|
|
75
|
+
candidates = candidates.flatMap(
|
|
76
|
+
(base) => fs.existsSync(base) ? fs.readdirSync(base).filter((name) => matcher.test(name)).sort().map((name) => path.join(base, name)) : []
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return candidates.filter((candidate) => fs.existsSync(candidate));
|
|
80
|
+
}
|
|
81
|
+
function escapeRegExp(text) {
|
|
82
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
83
|
+
}
|
|
84
|
+
function rubyFilesUnder(directory) {
|
|
85
|
+
return fs.readdirSync(directory, { withFileTypes: true, recursive: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".rb")).map((entry) => path.join(entry.parentPath, entry.name)).sort();
|
|
86
|
+
}
|
|
87
|
+
function engineClassesIn(node, acronyms, enclosing = []) {
|
|
88
|
+
const found = [];
|
|
89
|
+
for (const child of node.namedChildren) {
|
|
90
|
+
if (child === null) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (child.type === "class" && extendsEngine(child)) {
|
|
94
|
+
const name = definedName(child);
|
|
95
|
+
if (name !== void 0) {
|
|
96
|
+
found.push({
|
|
97
|
+
qualifiedName: [...enclosing, name].join("::"),
|
|
98
|
+
modulePrefix: isolatedNamespaceOf(child, acronyms)
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const nested = child.type === "module" || child.type === "class" ? [...enclosing, definedName(child) ?? ""] : enclosing;
|
|
104
|
+
found.push(...engineClassesIn(child, acronyms, nested));
|
|
105
|
+
}
|
|
106
|
+
return found;
|
|
107
|
+
}
|
|
108
|
+
function definedName(definition) {
|
|
109
|
+
return field(definition, "name")?.text.replace(/^::/, "");
|
|
110
|
+
}
|
|
111
|
+
function extendsEngine(classNode) {
|
|
112
|
+
const superclass = field(classNode, "superclass");
|
|
113
|
+
const base = superclass?.namedChildren.find(
|
|
114
|
+
(child) => child !== null && CONSTANT_TYPES.has(child.type)
|
|
115
|
+
);
|
|
116
|
+
return base !== void 0 && base !== null && ENGINE_BASE_NAMES.has(base.text);
|
|
117
|
+
}
|
|
118
|
+
function isolatedNamespaceOf(classNode, acronyms) {
|
|
119
|
+
const body = field(classNode, "body");
|
|
120
|
+
if (body === null) {
|
|
121
|
+
return "";
|
|
122
|
+
}
|
|
123
|
+
for (const statement of bodyStatements(body)) {
|
|
124
|
+
if (statement.type !== "call" || field(statement, "method")?.text !== "isolate_namespace") {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const arguments_ = field(statement, "arguments");
|
|
128
|
+
const argument = arguments_ === null ? void 0 : bodyStatements(arguments_)[0];
|
|
129
|
+
if (argument !== void 0 && CONSTANT_TYPES.has(argument.type)) {
|
|
130
|
+
return underscoreConstantPath(argument.text.replace(/^::/, ""), acronyms);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return "";
|
|
134
|
+
}
|
|
135
|
+
var ACRONYM_CALL = /\.acronym\s*\(?\s*(["'])([A-Za-z\d]+)\1/g;
|
|
136
|
+
function readAcronyms(configDirectory) {
|
|
137
|
+
const found = /* @__PURE__ */ new Set();
|
|
138
|
+
for (const file of inflectionFiles(configDirectory)) {
|
|
139
|
+
const source = fs2.readFileSync(file, "utf8");
|
|
140
|
+
for (const match of source.matchAll(ACRONYM_CALL)) {
|
|
141
|
+
found.add(match[2] ?? "");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
found.delete("");
|
|
145
|
+
return [...found].sort();
|
|
146
|
+
}
|
|
147
|
+
function inflectionFiles(configDirectory) {
|
|
148
|
+
const directory = path2.join(configDirectory, "config", "initializers");
|
|
149
|
+
if (!fs2.existsSync(directory)) {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
return fs2.readdirSync(directory, { withFileTypes: true, recursive: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".rb")).map((entry) => path2.join(entry.parentPath, entry.name)).filter(
|
|
153
|
+
(file) => fs2.readFileSync(file, "utf8").match(ACRONYM_CALL) !== null
|
|
154
|
+
).sort();
|
|
155
|
+
}
|
|
156
|
+
function readSimpleArgs(call, defaults = {}) {
|
|
157
|
+
const argumentList = field2(call, "arguments");
|
|
16
158
|
const positional = [];
|
|
17
|
-
const keyword = {};
|
|
159
|
+
const keyword = { ...defaults };
|
|
18
160
|
let hashRocketPair = null;
|
|
19
161
|
if (argumentList === null) {
|
|
20
162
|
return { positional, keyword, hashRocketPair };
|
|
21
163
|
}
|
|
22
|
-
for (const child of
|
|
164
|
+
for (const child of bodyStatements2(argumentList)) {
|
|
23
165
|
if (child.type !== "pair") {
|
|
24
166
|
positional.push(child);
|
|
25
167
|
continue;
|
|
26
168
|
}
|
|
27
|
-
const keyNode =
|
|
28
|
-
const valueNode =
|
|
169
|
+
const keyNode = field2(child, "key");
|
|
170
|
+
const valueNode = field2(child, "value");
|
|
29
171
|
if (keyNode === null || valueNode === null) {
|
|
30
172
|
continue;
|
|
31
173
|
}
|
|
32
|
-
const symbolKey = hashKeySymbolName(keyNode);
|
|
174
|
+
const symbolKey = hashKeySymbolName(keyNode) ?? symbolValue(keyNode);
|
|
33
175
|
if (symbolKey !== null) {
|
|
34
176
|
keyword[symbolKey] = valueNode;
|
|
35
177
|
} else {
|
|
@@ -38,11 +180,14 @@ function readSimpleArgs(call) {
|
|
|
38
180
|
}
|
|
39
181
|
return { positional, keyword, hashRocketPair };
|
|
40
182
|
}
|
|
41
|
-
function
|
|
183
|
+
function textValue(node, ctx) {
|
|
184
|
+
return stringValueOf(node, void 0, ctx.bindings);
|
|
185
|
+
}
|
|
186
|
+
function wordValue(node, ctx) {
|
|
42
187
|
if (node === void 0) {
|
|
43
188
|
return null;
|
|
44
189
|
}
|
|
45
|
-
return symbolValue(node) ??
|
|
190
|
+
return symbolValue(node) ?? textValue(node, ctx);
|
|
46
191
|
}
|
|
47
192
|
function splitControllerAction(text) {
|
|
48
193
|
const index = text.indexOf("#");
|
|
@@ -51,15 +196,15 @@ function splitControllerAction(text) {
|
|
|
51
196
|
}
|
|
52
197
|
return { controllerKey: text.slice(0, index), action: text.slice(index + 1) };
|
|
53
198
|
}
|
|
54
|
-
function readRouteTarget(args) {
|
|
55
|
-
const toText = args.keyword.to ?
|
|
199
|
+
function readRouteTarget(args, ctx) {
|
|
200
|
+
const toText = args.keyword.to ? textValue(args.keyword.to, ctx) : null;
|
|
56
201
|
if (toText !== null) {
|
|
57
202
|
const target = splitControllerAction(toText);
|
|
58
203
|
return target === null ? null : { ...target, path: null };
|
|
59
204
|
}
|
|
60
205
|
if (args.hashRocketPair !== null) {
|
|
61
|
-
const pathText =
|
|
62
|
-
const actionText =
|
|
206
|
+
const pathText = textValue(args.hashRocketPair.key, ctx);
|
|
207
|
+
const actionText = textValue(args.hashRocketPair.value, ctx);
|
|
63
208
|
const target = actionText !== null ? splitControllerAction(actionText) : null;
|
|
64
209
|
return target !== null && pathText !== null ? { ...target, path: pathText } : null;
|
|
65
210
|
}
|
|
@@ -70,7 +215,7 @@ function readSymbolList(node) {
|
|
|
70
215
|
return null;
|
|
71
216
|
}
|
|
72
217
|
if (node.type === "array") {
|
|
73
|
-
return
|
|
218
|
+
return bodyStatements2(node).map((child) => symbolValue(child)).filter((name) => name !== null);
|
|
74
219
|
}
|
|
75
220
|
const single = symbolValue(node);
|
|
76
221
|
return single === null ? null : [single];
|
|
@@ -96,10 +241,10 @@ function pluralize(word) {
|
|
|
96
241
|
if (/[^aeiou]y$/.test(word)) {
|
|
97
242
|
return `${word.slice(0, -1)}ies`;
|
|
98
243
|
}
|
|
99
|
-
if (/(
|
|
244
|
+
if (/(ss|us|is|x|z|ch|sh)$/.test(word)) {
|
|
100
245
|
return `${word}es`;
|
|
101
246
|
}
|
|
102
|
-
return `${word}s`;
|
|
247
|
+
return /s$/.test(word) ? word : `${word}s`;
|
|
103
248
|
}
|
|
104
249
|
function singularize(word) {
|
|
105
250
|
if (/ies$/.test(word)) {
|
|
@@ -111,27 +256,79 @@ function singularize(word) {
|
|
|
111
256
|
return /s$/.test(word) ? word.slice(0, -1) : word;
|
|
112
257
|
}
|
|
113
258
|
var RouteAccumulator = class {
|
|
259
|
+
/** `drawDirectory` is where `draw(:name)` finds `name.rb`. */
|
|
260
|
+
constructor(drawDirectory) {
|
|
261
|
+
this.drawDirectory = drawDirectory;
|
|
262
|
+
}
|
|
263
|
+
drawDirectory;
|
|
114
264
|
byKey = /* @__PURE__ */ new Map();
|
|
115
|
-
unread = /* @__PURE__ */ new
|
|
265
|
+
unread = /* @__PURE__ */ new Map();
|
|
266
|
+
/** Per file, each gem block call whose body was walked as though the call changed nothing about it. */
|
|
267
|
+
walkedBlocks = /* @__PURE__ */ new Map();
|
|
268
|
+
missingDrawn = [];
|
|
269
|
+
/** The block each `concern :name do ... end` declared, for `concerns` to replay. */
|
|
270
|
+
concerns = /* @__PURE__ */ new Map();
|
|
271
|
+
/** Files `draw(:name)` has already read, so a file drawing itself stops. */
|
|
272
|
+
drawn = /* @__PURE__ */ new Set();
|
|
273
|
+
/** Every engine a `mount` may refer to, by the class name it is written with. */
|
|
274
|
+
engines = /* @__PURE__ */ new Map();
|
|
275
|
+
/** Engines whose route set is being walked, so an engine that mounts itself stops. */
|
|
276
|
+
mounting = /* @__PURE__ */ new Set();
|
|
277
|
+
/** The file whose statements are being walked, as a gap writes it. */
|
|
278
|
+
file = "";
|
|
279
|
+
/** The first route written for an action wins, the way Rails matches the first route it declared. */
|
|
116
280
|
add(controllerKey, action, route) {
|
|
117
|
-
|
|
281
|
+
const key = `${controllerKey}#${action}`;
|
|
282
|
+
if (!this.byKey.has(key)) {
|
|
283
|
+
this.byKey.set(key, route);
|
|
284
|
+
}
|
|
118
285
|
}
|
|
119
286
|
recordUnread(callName) {
|
|
120
|
-
this.unread.
|
|
287
|
+
addUnderFile(this.unread, this.file, callName);
|
|
288
|
+
}
|
|
289
|
+
recordWalkedBlock(callName) {
|
|
290
|
+
addUnderFile(this.walkedBlocks, this.file, callName);
|
|
291
|
+
}
|
|
292
|
+
recordMissingDrawn(name) {
|
|
293
|
+
this.missingDrawn.push({ file: this.file, name });
|
|
121
294
|
}
|
|
122
295
|
routeFor(controllerKey, action) {
|
|
123
296
|
return this.byKey.get(`${controllerKey}#${action}`) ?? null;
|
|
124
297
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
298
|
+
/** Runs `walk` with gaps attributed to `file`, then goes back to the file being walked before. */
|
|
299
|
+
inFile(file, walk) {
|
|
300
|
+
const previous = this.file;
|
|
301
|
+
this.file = file;
|
|
302
|
+
walk();
|
|
303
|
+
this.file = previous;
|
|
304
|
+
}
|
|
305
|
+
gaps() {
|
|
306
|
+
const gaps = [];
|
|
307
|
+
for (const [file, unread] of this.unread) {
|
|
308
|
+
const names = [...unread].sort().join(", ");
|
|
309
|
+
gaps.push(
|
|
310
|
+
`${file} also declares ${names}, which this pack does not read; whatever those declarations route is missing from what suss reports`
|
|
311
|
+
);
|
|
128
312
|
}
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
313
|
+
for (const [file, walked] of this.walkedBlocks) {
|
|
314
|
+
const names = [...walked].sort().join(", ");
|
|
315
|
+
gaps.push(
|
|
316
|
+
`${file} wraps routes in ${names}, which this pack does not know; it read the routes inside as though the wrapper changed nothing about their path or controller`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
for (const { file, name } of this.missingDrawn) {
|
|
320
|
+
gaps.push(
|
|
321
|
+
`${file} draws ${name}, but there is no ${name}.rb under ${path3.basename(this.drawDirectory)}/ beside it to read; whatever that file routes is missing from what suss reports`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
return gaps;
|
|
133
325
|
}
|
|
134
326
|
};
|
|
327
|
+
function addUnderFile(byFile, file, name) {
|
|
328
|
+
const names = byFile.get(file) ?? /* @__PURE__ */ new Set();
|
|
329
|
+
names.add(name);
|
|
330
|
+
byFile.set(file, names);
|
|
331
|
+
}
|
|
135
332
|
function joinKey(prefix, segment) {
|
|
136
333
|
return prefix === "" ? segment : `${prefix}/${segment}`;
|
|
137
334
|
}
|
|
@@ -141,15 +338,20 @@ function joinPath(prefix, segment) {
|
|
|
141
338
|
return trimmed === "" ? base || "/" : `${base}/${trimmed}`;
|
|
142
339
|
}
|
|
143
340
|
function handleResourceCall(call, ctx, out, plural) {
|
|
144
|
-
const args = readSimpleArgs(call);
|
|
341
|
+
const args = readSimpleArgs(call, ctx.defaults);
|
|
145
342
|
const symbol = args.positional[0] ? symbolValue(args.positional[0]) : null;
|
|
146
343
|
if (symbol === null) {
|
|
147
344
|
return;
|
|
148
345
|
}
|
|
149
|
-
const controllerSegment = wordValue(args.keyword.controller) ?? (plural ? symbol : pluralize(symbol));
|
|
150
|
-
const
|
|
346
|
+
const controllerSegment = wordValue(args.keyword.controller, ctx) ?? (plural ? symbol : pluralize(symbol));
|
|
347
|
+
const moduleName = wordValue(args.keyword.module, ctx);
|
|
348
|
+
const modulePrefix = moduleName === null ? ctx.modulePrefix : joinKey(ctx.modulePrefix, moduleName);
|
|
349
|
+
const controllerKey = joinKey(modulePrefix, controllerSegment);
|
|
151
350
|
const prefixBase = ctx.resource !== void 0 ? ctx.resource.nestedBase : ctx.pathPrefix;
|
|
152
|
-
const base = joinPath(
|
|
351
|
+
const base = joinPath(
|
|
352
|
+
prefixBase,
|
|
353
|
+
wordValue(args.keyword.path, ctx) ?? symbol
|
|
354
|
+
);
|
|
153
355
|
const only = readSymbolList(args.keyword.only);
|
|
154
356
|
const except = readSymbolList(args.keyword.except);
|
|
155
357
|
const table = plural ? RESTFUL_ROUTES_PLURAL : RESTFUL_ROUTES_SINGULAR;
|
|
@@ -162,11 +364,6 @@ function handleResourceCall(call, ctx, out, plural) {
|
|
|
162
364
|
}
|
|
163
365
|
out.add(controllerKey, action, routeAt(base));
|
|
164
366
|
}
|
|
165
|
-
const block = field(call, "block");
|
|
166
|
-
const body = block !== null ? field(block, "body") : null;
|
|
167
|
-
if (body === null) {
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
367
|
const nestedBase = plural ? `${base}/:${singularize(symbol)}_id` : base;
|
|
171
368
|
const nested = {
|
|
172
369
|
controllerKey,
|
|
@@ -175,14 +372,20 @@ function handleResourceCall(call, ctx, out, plural) {
|
|
|
175
372
|
nestedBase,
|
|
176
373
|
ambientBase: nestedBase
|
|
177
374
|
};
|
|
178
|
-
|
|
375
|
+
const inside = { ...ctx, modulePrefix, resource: nested };
|
|
376
|
+
applyConcerns(readSymbolList(args.keyword.concerns) ?? [], inside, out);
|
|
377
|
+
const block = field2(call, "block");
|
|
378
|
+
const body = block !== null ? field2(block, "body") : null;
|
|
379
|
+
if (body !== null) {
|
|
380
|
+
walkBody(body, inside, out);
|
|
381
|
+
}
|
|
179
382
|
}
|
|
180
383
|
function handleOnBlock(call, ctx, out, isMember) {
|
|
181
384
|
if (ctx.resource === void 0) {
|
|
182
385
|
return;
|
|
183
386
|
}
|
|
184
|
-
const block =
|
|
185
|
-
const body = block !== null ?
|
|
387
|
+
const block = field2(call, "block");
|
|
388
|
+
const body = block !== null ? field2(block, "body") : null;
|
|
186
389
|
if (body === null) {
|
|
187
390
|
return;
|
|
188
391
|
}
|
|
@@ -199,35 +402,36 @@ function baseForOn(resource, on) {
|
|
|
199
402
|
return resource.ambientBase;
|
|
200
403
|
}
|
|
201
404
|
function handleVerb(call, ctx, out, method) {
|
|
202
|
-
const args = readSimpleArgs(call);
|
|
203
|
-
const
|
|
405
|
+
const args = readSimpleArgs(call, ctx.defaults);
|
|
406
|
+
const on = wordValue(args.keyword.on, ctx);
|
|
407
|
+
const base = ctx.resource === void 0 ? ctx.pathPrefix : baseForOn(ctx.resource, on);
|
|
408
|
+
const target = readRouteTarget(args, ctx);
|
|
204
409
|
if (target !== null) {
|
|
205
|
-
const literalPath = args.positional[0] ?
|
|
206
|
-
const
|
|
207
|
-
if (
|
|
410
|
+
const literalPath = args.positional[0] ? textValue(args.positional[0], ctx) : null;
|
|
411
|
+
const path5 = target.path ?? literalPath;
|
|
412
|
+
if (path5 !== null) {
|
|
208
413
|
out.add(joinKey(ctx.modulePrefix, target.controllerKey), target.action, {
|
|
209
414
|
method,
|
|
210
|
-
path: joinPath(
|
|
415
|
+
path: joinPath(base, path5)
|
|
211
416
|
});
|
|
212
417
|
}
|
|
213
418
|
return;
|
|
214
419
|
}
|
|
215
|
-
|
|
420
|
+
const segment = args.positional[0] ? wordValue(args.positional[0], ctx) : null;
|
|
421
|
+
if (segment === null) {
|
|
216
422
|
return;
|
|
217
423
|
}
|
|
218
|
-
const
|
|
219
|
-
|
|
424
|
+
const action = wordValue(args.keyword.action, ctx) ?? segment;
|
|
425
|
+
const controller = wordValue(args.keyword.controller, ctx);
|
|
426
|
+
const controllerKey = controller !== null ? joinKey(ctx.modulePrefix, controller) : ctx.resource?.controllerKey;
|
|
427
|
+
if (controllerKey === void 0) {
|
|
220
428
|
return;
|
|
221
429
|
}
|
|
222
|
-
|
|
223
|
-
out.add(ctx.resource.controllerKey, symbol, {
|
|
224
|
-
method,
|
|
225
|
-
path: `${base}/${symbol}`
|
|
226
|
-
});
|
|
430
|
+
out.add(controllerKey, action, { method, path: joinPath(base, segment) });
|
|
227
431
|
}
|
|
228
432
|
function handleRoot(call, ctx, out) {
|
|
229
|
-
const args = readSimpleArgs(call);
|
|
230
|
-
const target = readRouteTarget(args) ?? (args.positional[0] ? splitControllerAction(
|
|
433
|
+
const args = readSimpleArgs(call, ctx.defaults);
|
|
434
|
+
const target = readRouteTarget(args, ctx) ?? (args.positional[0] ? splitControllerAction(textValue(args.positional[0], ctx) ?? "") : null);
|
|
231
435
|
if (target !== null) {
|
|
232
436
|
out.add(joinKey(ctx.modulePrefix, target.controllerKey), target.action, {
|
|
233
437
|
method: "GET",
|
|
@@ -236,40 +440,146 @@ function handleRoot(call, ctx, out) {
|
|
|
236
440
|
}
|
|
237
441
|
}
|
|
238
442
|
function handleNamespace(call, ctx, out) {
|
|
239
|
-
const args = readSimpleArgs(call);
|
|
443
|
+
const args = readSimpleArgs(call, ctx.defaults);
|
|
240
444
|
const symbol = args.positional[0] ? symbolValue(args.positional[0]) : null;
|
|
241
|
-
const block =
|
|
242
|
-
const body = block !== null ?
|
|
445
|
+
const block = field2(call, "block");
|
|
446
|
+
const body = block !== null ? field2(block, "body") : null;
|
|
243
447
|
if (symbol === null || body === null) {
|
|
244
448
|
return;
|
|
245
449
|
}
|
|
246
|
-
walkBody(
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
450
|
+
walkBody(body, enterScope(ctx, symbol, symbol), out);
|
|
451
|
+
}
|
|
452
|
+
function enterScope(ctx, pathSegment, moduleName) {
|
|
453
|
+
const base = ctx.resource?.nestedBase ?? ctx.pathPrefix;
|
|
454
|
+
const { resource: _left, ...kept } = ctx;
|
|
455
|
+
return {
|
|
456
|
+
...kept,
|
|
457
|
+
pathPrefix: pathSegment !== null ? joinPath(base, pathSegment) : base,
|
|
458
|
+
modulePrefix: moduleName !== null ? joinKey(ctx.modulePrefix, moduleName) : ctx.modulePrefix
|
|
459
|
+
};
|
|
254
460
|
}
|
|
255
461
|
function handleScope(call, ctx, out) {
|
|
256
|
-
const args = readSimpleArgs(call);
|
|
257
|
-
const moduleName = wordValue(args.keyword.module);
|
|
258
|
-
const pathSegment = wordValue(args.keyword.path) ?? (args.positional[0] ? wordValue(args.positional[0]) : null);
|
|
259
|
-
const block =
|
|
260
|
-
const body = block !== null ?
|
|
462
|
+
const args = readSimpleArgs(call, ctx.defaults);
|
|
463
|
+
const moduleName = wordValue(args.keyword.module, ctx);
|
|
464
|
+
const pathSegment = wordValue(args.keyword.path, ctx) ?? (args.positional[0] ? wordValue(args.positional[0], ctx) : null);
|
|
465
|
+
const block = field2(call, "block");
|
|
466
|
+
const body = block !== null ? field2(block, "body") : null;
|
|
261
467
|
if (body === null) {
|
|
262
468
|
return;
|
|
263
469
|
}
|
|
264
|
-
walkBody(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
470
|
+
walkBody(body, enterScope(ctx, pathSegment, moduleName), out);
|
|
471
|
+
}
|
|
472
|
+
var LITERAL_LIST_TYPES = /* @__PURE__ */ new Set(["array", "string_array", "symbol_array"]);
|
|
473
|
+
var LOOP_METHODS = /* @__PURE__ */ new Set(["each", "each_with_index"]);
|
|
474
|
+
function replayLiteralLoop(call, ctx, out) {
|
|
475
|
+
const receiver = field2(call, "receiver");
|
|
476
|
+
const method = field2(call, "method")?.text;
|
|
477
|
+
const block = field2(call, "block");
|
|
478
|
+
const body = block !== null ? field2(block, "body") : null;
|
|
479
|
+
const parameters = block !== null ? field2(block, "parameters") : null;
|
|
480
|
+
if (receiver === null || !LITERAL_LIST_TYPES.has(receiver.type) || method === void 0 || !LOOP_METHODS.has(method) || body === null || parameters === null) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
const [element, index] = bodyStatements2(parameters).map((p) => p.text);
|
|
484
|
+
if (element === void 0) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const elements = bodyStatements2(receiver).map(
|
|
488
|
+
(node) => listElementWord(node, ctx)
|
|
489
|
+
);
|
|
490
|
+
elements.forEach((word, position) => {
|
|
491
|
+
if (word === null) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const bindings = new Map(ctx.bindings ?? []);
|
|
495
|
+
bindings.set(element, word);
|
|
496
|
+
if (index !== void 0) {
|
|
497
|
+
bindings.set(index, String(position));
|
|
498
|
+
}
|
|
499
|
+
walkBody(body, { ...ctx, bindings }, out);
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
function listElementWord(node, ctx) {
|
|
503
|
+
if (node.type === "bare_symbol") {
|
|
504
|
+
return node.text;
|
|
505
|
+
}
|
|
506
|
+
return wordValue(node, ctx);
|
|
507
|
+
}
|
|
508
|
+
function walkBlockInPlace(call, ctx, out) {
|
|
509
|
+
const block = field2(call, "block");
|
|
510
|
+
const body = block !== null ? field2(block, "body") : null;
|
|
511
|
+
if (body !== null) {
|
|
512
|
+
walkBody(body, ctx, out);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function handleWithOptions(call, ctx, out) {
|
|
516
|
+
const args = readSimpleArgs(call, ctx.defaults);
|
|
517
|
+
const block = field2(call, "block");
|
|
518
|
+
const body = block !== null ? field2(block, "body") : null;
|
|
519
|
+
if (body === null) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
walkBody(body, { ...ctx, defaults: args.keyword }, out);
|
|
523
|
+
}
|
|
524
|
+
function handleConcern(call, ctx, out) {
|
|
525
|
+
const args = readSimpleArgs(call, ctx.defaults);
|
|
526
|
+
const name = args.positional[0] ? symbolValue(args.positional[0]) : null;
|
|
527
|
+
const block = field2(call, "block");
|
|
528
|
+
const body = block !== null ? field2(block, "body") : null;
|
|
529
|
+
if (name !== null && body !== null) {
|
|
530
|
+
out.concerns.set(name, body);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function applyConcerns(names, ctx, out) {
|
|
534
|
+
for (const name of names) {
|
|
535
|
+
const body = out.concerns.get(name);
|
|
536
|
+
if (body !== void 0) {
|
|
537
|
+
walkBody(body, ctx, out);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
function handleConcerns(call, ctx, out) {
|
|
542
|
+
const args = readSimpleArgs(call, ctx.defaults);
|
|
543
|
+
const names = args.positional.map((node) => symbolValue(node)).filter((name) => name !== null);
|
|
544
|
+
applyConcerns(names, ctx, out);
|
|
545
|
+
}
|
|
546
|
+
function methodsOfVia(node) {
|
|
547
|
+
const names = readSymbolList(node) ?? [];
|
|
548
|
+
if (names.includes("all")) {
|
|
549
|
+
return ["*"];
|
|
550
|
+
}
|
|
551
|
+
return names.map((name) => name.toUpperCase());
|
|
552
|
+
}
|
|
553
|
+
function handleMatch(call, ctx, out) {
|
|
554
|
+
const via = readSimpleArgs(call, ctx.defaults).keyword.via;
|
|
555
|
+
for (const method of methodsOfVia(via)) {
|
|
556
|
+
handleVerb(call, ctx, out, method);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function drawDirectoryOf(routesFile) {
|
|
560
|
+
return path3.join(
|
|
561
|
+
path3.dirname(routesFile),
|
|
562
|
+
path3.basename(routesFile, path3.extname(routesFile))
|
|
271
563
|
);
|
|
272
564
|
}
|
|
565
|
+
function handleDraw(call, ctx, out) {
|
|
566
|
+
const args = readSimpleArgs(call, ctx.defaults);
|
|
567
|
+
const name = args.positional[0] ? wordValue(args.positional[0], ctx) : null;
|
|
568
|
+
if (name === null) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const file = path3.join(out.drawDirectory, `${name}.rb`);
|
|
572
|
+
if (out.drawn.has(file)) {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
out.drawn.add(file);
|
|
576
|
+
if (!fs3.existsSync(file)) {
|
|
577
|
+
out.recordMissingDrawn(name);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const tree = parseRubySync2(fs3.readFileSync(file, "utf8"));
|
|
581
|
+
walkBody(tree.rootNode, ctx, out);
|
|
582
|
+
}
|
|
273
583
|
var HANDLERS = {
|
|
274
584
|
resources: (call, ctx, out) => handleResourceCall(call, ctx, out, true),
|
|
275
585
|
resource: (call, ctx, out) => handleResourceCall(call, ctx, out, false),
|
|
@@ -282,59 +592,182 @@ var HANDLERS = {
|
|
|
282
592
|
post: (call, ctx, out) => handleVerb(call, ctx, out, "POST"),
|
|
283
593
|
patch: (call, ctx, out) => handleVerb(call, ctx, out, "PATCH"),
|
|
284
594
|
put: (call, ctx, out) => handleVerb(call, ctx, out, "PUT"),
|
|
285
|
-
delete: (call, ctx, out) => handleVerb(call, ctx, out, "DELETE")
|
|
595
|
+
delete: (call, ctx, out) => handleVerb(call, ctx, out, "DELETE"),
|
|
596
|
+
match: handleMatch,
|
|
597
|
+
draw: handleDraw,
|
|
598
|
+
constraints: walkBlockInPlace,
|
|
599
|
+
with_options: handleWithOptions,
|
|
600
|
+
concern: handleConcern,
|
|
601
|
+
concerns: handleConcerns,
|
|
602
|
+
mount: handleMount
|
|
286
603
|
};
|
|
604
|
+
function handleMount(call, ctx, out) {
|
|
605
|
+
const args = readSimpleArgs(call, ctx.defaults);
|
|
606
|
+
const target = args.positional[0] ?? args.hashRocketPair?.key;
|
|
607
|
+
const at = args.keyword.at ?? args.hashRocketPair?.value;
|
|
608
|
+
const name = target?.text.replace(/^::/, "");
|
|
609
|
+
const engine = name === void 0 ? void 0 : out.engines.get(name);
|
|
610
|
+
const mountPath = at === void 0 ? null : textValue(at, ctx);
|
|
611
|
+
if (name === void 0 || engine === void 0 || mountPath === null || out.mounting.has(name)) {
|
|
612
|
+
out.recordUnread("mount");
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (engine.body === null) {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
out.mounting.add(name);
|
|
619
|
+
out.inFile(engine.displayPath, () => {
|
|
620
|
+
walkBody(
|
|
621
|
+
engine.body,
|
|
622
|
+
{
|
|
623
|
+
pathPrefix: joinPath(ctx.pathPrefix, mountPath),
|
|
624
|
+
modulePrefix: engine.modulePrefix,
|
|
625
|
+
defaults: {}
|
|
626
|
+
},
|
|
627
|
+
out
|
|
628
|
+
);
|
|
629
|
+
});
|
|
630
|
+
out.mounting.delete(name);
|
|
631
|
+
}
|
|
287
632
|
function walkBody(body, ctx, out) {
|
|
288
|
-
|
|
289
|
-
|
|
633
|
+
walkStatements(bodyStatements2(body), ctx, out);
|
|
634
|
+
}
|
|
635
|
+
function walkStatements(statements, ctx, out) {
|
|
636
|
+
for (const statement of statements) {
|
|
637
|
+
if (statement.type !== "call") {
|
|
638
|
+
walkStatements(nestedStatements(statement), ctx, out);
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
if (field2(statement, "receiver") !== null) {
|
|
642
|
+
replayLiteralLoop(statement, ctx, out);
|
|
290
643
|
continue;
|
|
291
644
|
}
|
|
292
|
-
const name =
|
|
645
|
+
const name = field2(statement, "method")?.text;
|
|
293
646
|
if (name === void 0) {
|
|
294
647
|
continue;
|
|
295
648
|
}
|
|
296
649
|
const handler = HANDLERS[name];
|
|
297
|
-
if (handler
|
|
298
|
-
out
|
|
650
|
+
if (handler !== void 0) {
|
|
651
|
+
handler(statement, ctx, out);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
if (blockWrittenInRoutesGrammar(statement)) {
|
|
655
|
+
out.recordWalkedBlock(name);
|
|
656
|
+
walkBlockInPlace(statement, ctx, out);
|
|
299
657
|
continue;
|
|
300
658
|
}
|
|
301
|
-
|
|
659
|
+
out.recordUnread(name);
|
|
302
660
|
}
|
|
303
661
|
}
|
|
304
|
-
function
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
662
|
+
function blockWrittenInRoutesGrammar(call) {
|
|
663
|
+
const block = field2(call, "block");
|
|
664
|
+
const body = block !== null ? field2(block, "body") : null;
|
|
665
|
+
if (body === null) {
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
return bodyStatements2(body).some(
|
|
669
|
+
(statement) => statement.type === "call" && field2(statement, "receiver") === null && Object.hasOwn(HANDLERS, field2(statement, "method")?.text ?? "")
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
var DECLARATION_ORDER = {
|
|
673
|
+
prepend: 0,
|
|
674
|
+
draw: 1,
|
|
675
|
+
append: 2
|
|
676
|
+
};
|
|
677
|
+
function isDeclarationKind(name) {
|
|
678
|
+
return name in DECLARATION_ORDER;
|
|
679
|
+
}
|
|
680
|
+
function routeDeclarations(root, displayPath) {
|
|
681
|
+
const found = [];
|
|
682
|
+
const visit = (node) => {
|
|
683
|
+
const declaration = asRouteDeclaration(node, displayPath);
|
|
684
|
+
if (declaration !== null) {
|
|
685
|
+
found.push(declaration);
|
|
686
|
+
return;
|
|
308
687
|
}
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
688
|
+
for (const child of node.namedChildren) {
|
|
689
|
+
if (child !== null) {
|
|
690
|
+
visit(child);
|
|
691
|
+
}
|
|
313
692
|
}
|
|
693
|
+
};
|
|
694
|
+
visit(root);
|
|
695
|
+
return found;
|
|
696
|
+
}
|
|
697
|
+
function asRouteDeclaration(node, displayPath) {
|
|
698
|
+
if (node.type !== "call") {
|
|
699
|
+
return null;
|
|
314
700
|
}
|
|
315
|
-
|
|
701
|
+
const kind = field2(node, "method")?.text;
|
|
702
|
+
const receiver = field2(node, "receiver");
|
|
703
|
+
const block = field2(node, "block");
|
|
704
|
+
const body = block !== null ? field2(block, "body") : null;
|
|
705
|
+
if (kind === void 0 || !isDeclarationKind(kind) || receiver === null || receiver.type !== "call" || field2(receiver, "method")?.text !== "routes" || body === null) {
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
const owner = field2(receiver, "receiver");
|
|
709
|
+
if (owner === null) {
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
return { owner: owner.text.replace(/^::/, ""), kind, body, displayPath };
|
|
713
|
+
}
|
|
714
|
+
function isApplication(owner) {
|
|
715
|
+
return owner === "Rails.application" || /(^|::)Application$/.test(owner);
|
|
316
716
|
}
|
|
317
|
-
function
|
|
318
|
-
if (!
|
|
717
|
+
function parseDeclarations(source) {
|
|
718
|
+
if (!fs3.existsSync(source.file)) {
|
|
719
|
+
return [];
|
|
720
|
+
}
|
|
721
|
+
const tree = parseRubySync2(fs3.readFileSync(source.file, "utf8"));
|
|
722
|
+
return routeDeclarations(tree.rootNode, source.displayPath);
|
|
723
|
+
}
|
|
724
|
+
function readRoutes(input) {
|
|
725
|
+
if (!fs3.existsSync(input.routesFile.file)) {
|
|
319
726
|
return {
|
|
320
727
|
routeFor: () => null,
|
|
321
728
|
fileFound: false,
|
|
322
729
|
gaps: [
|
|
323
|
-
`${displayPath} does not exist, so this run assumes each action's path and method from Rails' RESTful naming convention instead of reading it from routing`
|
|
730
|
+
`${input.routesFile.displayPath} does not exist, so this run assumes each action's path and method from Rails' RESTful naming convention instead of reading it from routing`
|
|
324
731
|
]
|
|
325
732
|
};
|
|
326
733
|
}
|
|
327
|
-
const
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
734
|
+
const out = new RouteAccumulator(drawDirectoryOf(input.routesFile.file));
|
|
735
|
+
const engineDeclarations = input.engines.flatMap(
|
|
736
|
+
(engine) => engine.routesFile === null ? [] : parseDeclarations({
|
|
737
|
+
file: engine.routesFile,
|
|
738
|
+
displayPath: engine.displayPath
|
|
739
|
+
})
|
|
740
|
+
);
|
|
741
|
+
for (const engine of input.engines) {
|
|
742
|
+
out.engines.set(engine.qualifiedName, {
|
|
743
|
+
modulePrefix: engine.modulePrefix,
|
|
744
|
+
body: engineDeclarations.find(
|
|
745
|
+
(declaration) => declaration.owner === engine.qualifiedName
|
|
746
|
+
)?.body ?? null,
|
|
747
|
+
displayPath: engine.displayPath
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
const applicationDeclarations = [
|
|
751
|
+
...parseDeclarations(input.routesFile),
|
|
752
|
+
...input.routesFiles.flatMap(parseDeclarations),
|
|
753
|
+
...engineDeclarations
|
|
754
|
+
].filter((declaration) => isApplication(declaration.owner));
|
|
755
|
+
const ordered = applicationDeclarations.map((declaration, position) => ({ declaration, position })).sort(
|
|
756
|
+
(a, b) => DECLARATION_ORDER[a.declaration.kind] - DECLARATION_ORDER[b.declaration.kind] || a.position - b.position
|
|
757
|
+
);
|
|
758
|
+
for (const { declaration } of ordered) {
|
|
759
|
+
out.inFile(declaration.displayPath, () => {
|
|
760
|
+
walkBody(
|
|
761
|
+
declaration.body,
|
|
762
|
+
{ pathPrefix: "", modulePrefix: "", defaults: {} },
|
|
763
|
+
out
|
|
764
|
+
);
|
|
765
|
+
});
|
|
333
766
|
}
|
|
334
767
|
return {
|
|
335
768
|
routeFor: (controllerKey, action) => out.routeFor(controllerKey, action),
|
|
336
769
|
fileFound: true,
|
|
337
|
-
gaps: out.
|
|
770
|
+
gaps: out.gaps()
|
|
338
771
|
};
|
|
339
772
|
}
|
|
340
773
|
var RACK_STATUS_CODE_NAMES = {
|
|
@@ -424,7 +857,21 @@ var optionsSchema = z.object({
|
|
|
424
857
|
*/
|
|
425
858
|
inheritedMethodNames: z.array(z.string()).optional(),
|
|
426
859
|
/** Where this project's routes live, relative to `configDirectory` when there is one. Every Rails app scaffolds this at `config/routes.rb`. */
|
|
427
|
-
routesFile: z.string().optional()
|
|
860
|
+
routesFile: z.string().optional(),
|
|
861
|
+
/**
|
|
862
|
+
* Directories the project keeps its own engines under, each one the
|
|
863
|
+
* root a `Rails::Engine` subclass has its `lib/` and `config/routes.rb`
|
|
864
|
+
* in. A `*` in a segment matches any one directory, so `plugins/*`
|
|
865
|
+
* covers every plugin. Relative to `configDirectory`.
|
|
866
|
+
*/
|
|
867
|
+
engineRoots: z.array(z.string()).optional(),
|
|
868
|
+
/**
|
|
869
|
+
* Files beyond the routes file that add routes through a
|
|
870
|
+
* `Rails.application.routes.draw`, `.append` or `.prepend` block,
|
|
871
|
+
* a plugin's `plugin.rb` being the usual one. Same `*` rule as
|
|
872
|
+
* `engineRoots`. Relative to `configDirectory`.
|
|
873
|
+
*/
|
|
874
|
+
routesFiles: z.array(z.string()).optional()
|
|
428
875
|
}).strict();
|
|
429
876
|
var RAILS_ROOT_CLASS_NAMES = [
|
|
430
877
|
"ActionController::Base",
|
|
@@ -479,9 +926,9 @@ var RAILS_CONTROLLER_METHODS = [
|
|
|
479
926
|
"controller_name",
|
|
480
927
|
"controller_path"
|
|
481
928
|
];
|
|
482
|
-
function controllerKeyFromQualified(qualifiedName) {
|
|
929
|
+
function controllerKeyFromQualified(qualifiedName, acronyms) {
|
|
483
930
|
const withoutSuffix = qualifiedName.endsWith("Controller") ? qualifiedName.slice(0, -"Controller".length) : qualifiedName;
|
|
484
|
-
return
|
|
931
|
+
return underscoreConstantPath2(withoutSuffix, acronyms);
|
|
485
932
|
}
|
|
486
933
|
function conventionalRoute(controllerKey, actionName) {
|
|
487
934
|
const template = RESTFUL_ACTIONS[actionName];
|
|
@@ -495,10 +942,10 @@ function conventionalRoute(controllerKey, actionName) {
|
|
|
495
942
|
};
|
|
496
943
|
}
|
|
497
944
|
function resolveAgainst(configDirectory, value) {
|
|
498
|
-
if (configDirectory === void 0 ||
|
|
945
|
+
if (configDirectory === void 0 || path4.isAbsolute(value)) {
|
|
499
946
|
return value;
|
|
500
947
|
}
|
|
501
|
-
return
|
|
948
|
+
return path4.resolve(configDirectory, value);
|
|
502
949
|
}
|
|
503
950
|
function railsFramework(options = {}) {
|
|
504
951
|
const root = resolveAgainst(options.configDirectory, options.root ?? "app");
|
|
@@ -506,12 +953,27 @@ function railsFramework(options = {}) {
|
|
|
506
953
|
options.configDirectory,
|
|
507
954
|
options.routesFile ?? "config/routes.rb"
|
|
508
955
|
);
|
|
956
|
+
const displayPathOf = (file) => options.configDirectory === void 0 ? file : path4.relative(options.configDirectory, file);
|
|
957
|
+
const engineRoots = () => expandPatterns(options.configDirectory, options.engineRoots);
|
|
958
|
+
const extraRoutesFiles = () => expandPatterns(options.configDirectory, options.routesFiles);
|
|
959
|
+
const acronyms = options.configDirectory === void 0 ? [] : readAcronyms(options.configDirectory);
|
|
960
|
+
const routesInput = () => ({
|
|
961
|
+
routesFile: {
|
|
962
|
+
file: routesFile,
|
|
963
|
+
displayPath: options.routesFile ?? "config/routes.rb"
|
|
964
|
+
},
|
|
965
|
+
engines: readEngines(engineRoots(), acronyms).map((engine) => ({
|
|
966
|
+
...engine,
|
|
967
|
+
displayPath: engine.routesFile === null ? engine.qualifiedName : displayPathOf(engine.routesFile)
|
|
968
|
+
})),
|
|
969
|
+
routesFiles: extraRoutesFiles().map((file) => ({
|
|
970
|
+
file,
|
|
971
|
+
displayPath: displayPathOf(file)
|
|
972
|
+
}))
|
|
973
|
+
});
|
|
509
974
|
let table;
|
|
510
975
|
const routeTable = () => {
|
|
511
|
-
table ??=
|
|
512
|
-
routesFile,
|
|
513
|
-
options.routesFile ?? "config/routes.rb"
|
|
514
|
-
);
|
|
976
|
+
table ??= readRoutes(routesInput());
|
|
515
977
|
return table;
|
|
516
978
|
};
|
|
517
979
|
const pattern = {
|
|
@@ -522,6 +984,7 @@ function railsFramework(options = {}) {
|
|
|
522
984
|
],
|
|
523
985
|
root,
|
|
524
986
|
pathConvention: "railsUnderscore",
|
|
987
|
+
acronyms,
|
|
525
988
|
ancestryRootClassNames: [...RAILS_ROOT_CLASS_NAMES],
|
|
526
989
|
defaultStatusCode: 200,
|
|
527
990
|
responseStatusCalls: RESPONSE_STATUS_CALLS,
|
|
@@ -533,7 +996,7 @@ function railsFramework(options = {}) {
|
|
|
533
996
|
routesFile,
|
|
534
997
|
filters: CONTROLLER_FILTERS,
|
|
535
998
|
routeFor: (controllerQualifiedName, actionName) => {
|
|
536
|
-
const key = controllerKeyFromQualified(controllerQualifiedName);
|
|
999
|
+
const key = controllerKeyFromQualified(controllerQualifiedName, acronyms);
|
|
537
1000
|
const found = routeTable();
|
|
538
1001
|
return found.fileFound ? found.routeFor(key, actionName) : conventionalRoute(key, actionName);
|
|
539
1002
|
},
|
|
@@ -543,21 +1006,50 @@ function railsFramework(options = {}) {
|
|
|
543
1006
|
name: "rails",
|
|
544
1007
|
protocol: "http",
|
|
545
1008
|
discovery: [pattern],
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
|
|
1009
|
+
// Every file routing or naming is read from decides an action's
|
|
1010
|
+
// binding without being walked, so the cache key has to read them
|
|
1011
|
+
// here. This runs before the grammar loads, so nothing parses Ruby.
|
|
1012
|
+
discoveryInputs: () => [
|
|
1013
|
+
routesFile,
|
|
1014
|
+
...drawableRoutesFiles(routesFile),
|
|
1015
|
+
...engineRoots().flatMap(engineSourceFiles),
|
|
1016
|
+
...extraRoutesFiles(),
|
|
1017
|
+
...options.configDirectory === void 0 ? [] : inflectionFiles(options.configDirectory)
|
|
1018
|
+
]
|
|
549
1019
|
};
|
|
550
1020
|
}
|
|
1021
|
+
function expandPatterns(configDirectory, patterns) {
|
|
1022
|
+
return (patterns ?? []).flatMap(
|
|
1023
|
+
(pattern) => expandPathPattern(resolveAgainst(configDirectory, pattern))
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
function drawableRoutesFiles(routesFile) {
|
|
1027
|
+
const directory = drawDirectoryOf(routesFile);
|
|
1028
|
+
if (!fs4.existsSync(directory)) {
|
|
1029
|
+
return [];
|
|
1030
|
+
}
|
|
1031
|
+
return fs4.readdirSync(directory).filter((name) => name.endsWith(".rb")).sort().map((name) => path4.join(directory, name));
|
|
1032
|
+
}
|
|
551
1033
|
var declares = {
|
|
552
1034
|
kind: "framework",
|
|
553
1035
|
package: "@suss/framework-rails",
|
|
554
|
-
|
|
1036
|
+
// An app that depends on the Rails gems one at a time has railties in
|
|
1037
|
+
// its Gemfile and no rails.
|
|
1038
|
+
dependencies: [
|
|
1039
|
+
{ ecosystem: "rubygems", name: "rails" },
|
|
1040
|
+
{ ecosystem: "rubygems", name: "railties" }
|
|
1041
|
+
],
|
|
555
1042
|
reads: "Rails controller actions (Ruby), bound to the method and path `config/routes.rb` gives each one; an action the routes file does not reach is discovered with no boundary.",
|
|
556
1043
|
configuration: {
|
|
557
1044
|
file: "suss.rails.json",
|
|
558
|
-
example: {
|
|
1045
|
+
example: {
|
|
1046
|
+
root: "app",
|
|
1047
|
+
routesFile: "config/routes.rb",
|
|
1048
|
+
engineRoots: ["engines/*"],
|
|
1049
|
+
routesFiles: ["plugins/*/plugin.rb"]
|
|
1050
|
+
},
|
|
559
1051
|
required: false,
|
|
560
|
-
why: "the app directory a controller is defined under and the routes file suss reads each action's method and path from
|
|
1052
|
+
why: "the app directory a controller is defined under and the routes file suss reads each action's method and path from; rails new scaffolds both at these paths. engineRoots lists where the project keeps engines it mounts, and routesFiles any other file that adds routes, when it has either."
|
|
561
1053
|
}
|
|
562
1054
|
};
|
|
563
1055
|
var index_default = railsFramework;
|