@suss/packs 0.34.0 → 0.34.1
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.js +19 -1
- package/dist/aiohttp.js +20 -9
- package/dist/axios.js +23 -10
- package/dist/faraday.js +4 -1
- package/dist/fastapi.js +102 -0
- package/dist/fetch.js +9 -2
- package/dist/httpx.js +5 -1
- package/dist/nestjs-rest.js +159 -9
- package/dist/net-http.js +4 -1
- package/dist/prisma.js +105 -19
- package/dist/rails.js +1747 -106
- package/dist/requests.js +25 -16
- package/package.json +10 -10
package/dist/rails.js
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
// ../framework/rails/dist/index.js
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import { z } from "zod";
|
|
2
|
+
import fs5 from "fs";
|
|
3
|
+
import path5 from "path";
|
|
4
|
+
import { z as z15 } from "zod";
|
|
5
5
|
import { underscoreConstantPath as underscoreConstantPath2 } from "@suss/adapter-ruby";
|
|
6
6
|
import fs from "fs";
|
|
7
7
|
import path from "path";
|
|
8
|
+
import fs2 from "fs";
|
|
9
|
+
import path2 from "path";
|
|
8
10
|
import {
|
|
9
11
|
bodyStatements,
|
|
12
|
+
children,
|
|
10
13
|
field,
|
|
11
14
|
parseRubySync,
|
|
12
15
|
underscoreConstantPath
|
|
13
16
|
} from "@suss/adapter-ruby";
|
|
14
|
-
import fs2 from "fs";
|
|
15
|
-
import path2 from "path";
|
|
16
17
|
import fs3 from "fs";
|
|
17
18
|
import path3 from "path";
|
|
19
|
+
import fs4 from "fs";
|
|
20
|
+
import path4 from "path";
|
|
18
21
|
import {
|
|
19
22
|
bodyStatements as bodyStatements2,
|
|
23
|
+
children as children2,
|
|
20
24
|
field as field2,
|
|
21
25
|
hashKeySymbolName,
|
|
22
26
|
nestedStatements,
|
|
@@ -24,6 +28,1446 @@ import {
|
|
|
24
28
|
stringValueOf,
|
|
25
29
|
symbolValue
|
|
26
30
|
} from "@suss/adapter-ruby";
|
|
31
|
+
|
|
32
|
+
// ../ir-core/dist/chunk-A46EUIKC.js
|
|
33
|
+
import { z as z13 } from "zod";
|
|
34
|
+
import { z as z12 } from "zod";
|
|
35
|
+
import { z } from "zod";
|
|
36
|
+
import { z as z2 } from "zod";
|
|
37
|
+
import { z as z3 } from "zod";
|
|
38
|
+
import { z as z4 } from "zod";
|
|
39
|
+
import { z as z5 } from "zod";
|
|
40
|
+
import { z as z6 } from "zod";
|
|
41
|
+
import { z as z7 } from "zod";
|
|
42
|
+
import { z as z9 } from "zod";
|
|
43
|
+
import { z as z8 } from "zod";
|
|
44
|
+
import { z as z10 } from "zod";
|
|
45
|
+
import { z as z11 } from "zod";
|
|
46
|
+
function gqlIdentityKey(typeName, fieldName) {
|
|
47
|
+
return `gql:${typeName}.${fieldName}`;
|
|
48
|
+
}
|
|
49
|
+
function fnIdentityKey(packageName, exportPath) {
|
|
50
|
+
return `fn:${packageName}::${exportPath.join(".")}`;
|
|
51
|
+
}
|
|
52
|
+
function busIdentityKey(messageBus, subject) {
|
|
53
|
+
return `bus:${messageBus} ${subject}`;
|
|
54
|
+
}
|
|
55
|
+
function unitIdentityKey(deploymentTarget, instanceName) {
|
|
56
|
+
return `unit:${deploymentTarget} ${instanceName}`;
|
|
57
|
+
}
|
|
58
|
+
function metricIdentityKey(metricSystem, metricType) {
|
|
59
|
+
return `metric:${metricSystem} ${metricType}`;
|
|
60
|
+
}
|
|
61
|
+
function ioIdentityKey(target) {
|
|
62
|
+
return `io:${target}`;
|
|
63
|
+
}
|
|
64
|
+
function defineBoundarySemantics(definition) {
|
|
65
|
+
return definition;
|
|
66
|
+
}
|
|
67
|
+
var FunctionCallSemanticsSchema = z.object({
|
|
68
|
+
name: z.literal("function-call"),
|
|
69
|
+
/**
|
|
70
|
+
* Where the function lives inside this repository. It is either the
|
|
71
|
+
* name of a module the project lists in `suss.json`, such as
|
|
72
|
+
* `"billing"`, or a module path, such as the file a server action is
|
|
73
|
+
* written in. A name has no separator in it and a path always does.
|
|
74
|
+
* Packs that don't do cross-module pairing can leave it unset.
|
|
75
|
+
*/
|
|
76
|
+
module: z.string().optional(),
|
|
77
|
+
/** Named export within the module, when applicable. */
|
|
78
|
+
exportName: z.string().optional(),
|
|
79
|
+
/**
|
|
80
|
+
* Package name, as written in `package.json`, when this identity
|
|
81
|
+
* refers to a public package export, for example
|
|
82
|
+
* `"@suss/behavioral-ir"`. Packs that resolve a package's public
|
|
83
|
+
* surface (the `packageExports` discovery variant) set it alongside
|
|
84
|
+
* `exportPath`. It is different from `module`, which is a
|
|
85
|
+
* repo-relative module path used for pairing inside one repo.
|
|
86
|
+
*/
|
|
87
|
+
package: z.string().optional(),
|
|
88
|
+
/**
|
|
89
|
+
* Path to the exported binding within the package, starting with the
|
|
90
|
+
* sub-path key when one is used. Examples:
|
|
91
|
+
* `["parseSummary"]`: a root export
|
|
92
|
+
* `["schemas", "BoundaryBindingSchema"]`: the sub-path `./schemas`
|
|
93
|
+
*
|
|
94
|
+
* The first segment is the sub-path without the leading `./`, and
|
|
95
|
+
* `"."` is left out. The last segment is the exported name.
|
|
96
|
+
*/
|
|
97
|
+
exportPath: z.array(z.string()).optional()
|
|
98
|
+
});
|
|
99
|
+
function isModuleName(module) {
|
|
100
|
+
return !module.includes("/") && !module.includes("\\");
|
|
101
|
+
}
|
|
102
|
+
var functionCallSemantics = defineBoundarySemantics({
|
|
103
|
+
name: "function-call",
|
|
104
|
+
schema: FunctionCallSemanticsSchema,
|
|
105
|
+
// A call inside the process does not produce a span, and the
|
|
106
|
+
// conventions have no attributes for it.
|
|
107
|
+
semconv: {},
|
|
108
|
+
behavior: {
|
|
109
|
+
/** A call returns a value, with no status. */
|
|
110
|
+
exchangesHttpResponses: false,
|
|
111
|
+
leavesTheProcess: false,
|
|
112
|
+
reportsUnpairedItself: false,
|
|
113
|
+
/**
|
|
114
|
+
* In-repo units such as components have no key on purpose. They
|
|
115
|
+
* pair through call edges instead of the keyed pass, so a missing
|
|
116
|
+
* key does not make them unreachable.
|
|
117
|
+
*/
|
|
118
|
+
canPair: () => true,
|
|
119
|
+
rewritePaths(semantics, rewrite) {
|
|
120
|
+
if (semantics.module === void 0 || isModuleName(semantics.module)) {
|
|
121
|
+
return semantics;
|
|
122
|
+
}
|
|
123
|
+
return { ...semantics, module: rewrite(semantics.module) };
|
|
124
|
+
},
|
|
125
|
+
/**
|
|
126
|
+
* `"fn:<package>::<exportPath>"` when both are set, and
|
|
127
|
+
* `"fn:<module>::<exportName>"` for an in-repo unit that gives its
|
|
128
|
+
* module, as a server action does. Components and plain handlers
|
|
129
|
+
* give neither and have no key.
|
|
130
|
+
*/
|
|
131
|
+
identityKey(semantics) {
|
|
132
|
+
if (semantics.package !== void 0 && semantics.exportPath !== void 0 && semantics.exportPath.length > 0) {
|
|
133
|
+
return fnIdentityKey(semantics.package, semantics.exportPath);
|
|
134
|
+
}
|
|
135
|
+
if (semantics.module !== void 0 && semantics.exportName !== void 0) {
|
|
136
|
+
return fnIdentityKey(semantics.module, [semantics.exportName]);
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
var GraphqlOperationSemanticsSchema = z2.object({
|
|
143
|
+
name: z2.literal("graphql-operation"),
|
|
144
|
+
/** Optional operation name. An anonymous query or mutation leaves it unset. */
|
|
145
|
+
operationName: z2.string().optional(),
|
|
146
|
+
operationType: z2.enum(["query", "mutation", "subscription"])
|
|
147
|
+
});
|
|
148
|
+
var graphqlOperationSemantics = defineBoundarySemantics({
|
|
149
|
+
name: "graphql-operation",
|
|
150
|
+
schema: GraphqlOperationSemanticsSchema,
|
|
151
|
+
semconv: {
|
|
152
|
+
operationName: { name: "graphql.operation.name" },
|
|
153
|
+
operationType: { name: "graphql.operation.type" }
|
|
154
|
+
},
|
|
155
|
+
behavior: {
|
|
156
|
+
/**
|
|
157
|
+
* An operation gets a data-and-errors document back, which the
|
|
158
|
+
* GraphQL contract checker reads. Status codes do not apply.
|
|
159
|
+
*/
|
|
160
|
+
exchangesHttpResponses: false,
|
|
161
|
+
leavesTheProcess: true,
|
|
162
|
+
reportsUnpairedItself: false,
|
|
163
|
+
identityKey: () => null,
|
|
164
|
+
/** No key on purpose. The dedicated pass pairs an operation by its document. */
|
|
165
|
+
canPair: () => true,
|
|
166
|
+
/** `query GetUser`. Only for display, since the contract checker does the pairing. */
|
|
167
|
+
displayLabel(semantics) {
|
|
168
|
+
return `${semantics.operationType} ${semantics.operationName ?? "<anonymous>"}`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
var GraphqlResolverSemanticsSchema = z3.object({
|
|
173
|
+
name: z3.literal("graphql-resolver"),
|
|
174
|
+
/**
|
|
175
|
+
* GraphQL type the resolver attaches to: "Query", "Mutation",
|
|
176
|
+
* "Subscription", or an object-type name. Null when the source never
|
|
177
|
+
* says which one, as with a `@Resolver()` class that has no argument.
|
|
178
|
+
*/
|
|
179
|
+
typeName: z3.string().min(1).nullable(),
|
|
180
|
+
/** Field name on that type. */
|
|
181
|
+
fieldName: z3.string()
|
|
182
|
+
});
|
|
183
|
+
var graphqlResolverSemantics = defineBoundarySemantics({
|
|
184
|
+
name: "graphql-resolver",
|
|
185
|
+
schema: GraphqlResolverSemanticsSchema,
|
|
186
|
+
// The conventions describe the operation a client sent. They have no
|
|
187
|
+
// attribute for the resolver the server ran for one field of it.
|
|
188
|
+
semconv: {},
|
|
189
|
+
behavior: {
|
|
190
|
+
/** A resolver returns a field value, with no status. */
|
|
191
|
+
exchangesHttpResponses: false,
|
|
192
|
+
// A resolver runs because a client outside the process sent a query,
|
|
193
|
+
// even though the server calls it in its own process.
|
|
194
|
+
leavesTheProcess: true,
|
|
195
|
+
reportsUnpairedItself: false,
|
|
196
|
+
/**
|
|
197
|
+
* `"gql:<TypeName>.<fieldName>"`, or null when the type is null or
|
|
198
|
+
* the field is empty, which is how a resolver reports that the
|
|
199
|
+
* source never gave them.
|
|
200
|
+
*/
|
|
201
|
+
identityKey(semantics) {
|
|
202
|
+
if (semantics.typeName === null || semantics.fieldName === "") {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
return gqlIdentityKey(semantics.typeName, semantics.fieldName);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
var IoSemanticsSchema = z4.object({
|
|
210
|
+
name: z4.literal("io"),
|
|
211
|
+
/** The stream, such as `stdout` or `stderr`. Null when the source does not say which. */
|
|
212
|
+
target: z4.string().nullable()
|
|
213
|
+
});
|
|
214
|
+
var ioSemantics = defineBoundarySemantics({
|
|
215
|
+
name: "io",
|
|
216
|
+
schema: IoSemanticsSchema,
|
|
217
|
+
// The semantic conventions have no attribute for a process's own streams.
|
|
218
|
+
semconv: {},
|
|
219
|
+
behavior: {
|
|
220
|
+
exchangesHttpResponses: false,
|
|
221
|
+
leavesTheProcess: true,
|
|
222
|
+
reportsUnpairedItself: false,
|
|
223
|
+
identityKey(semantics) {
|
|
224
|
+
return semantics.target === null ? null : ioIdentityKey(semantics.target);
|
|
225
|
+
},
|
|
226
|
+
// Nothing declares the reading side, so a write has nothing to meet.
|
|
227
|
+
canPair: () => false
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
function parseChannel(channel) {
|
|
231
|
+
const hash = channel.indexOf("#");
|
|
232
|
+
if (hash === -1) {
|
|
233
|
+
return { bus: null, subject: channel };
|
|
234
|
+
}
|
|
235
|
+
return { bus: channel.slice(0, hash), subject: channel.slice(hash + 1) };
|
|
236
|
+
}
|
|
237
|
+
function busesAgree(a, b) {
|
|
238
|
+
return a === null || b === null || a === b;
|
|
239
|
+
}
|
|
240
|
+
function channelsPair(a, b) {
|
|
241
|
+
const left = parseChannel(a);
|
|
242
|
+
const right = parseChannel(b);
|
|
243
|
+
return left.subject === right.subject && busesAgree(left.bus, right.bus);
|
|
244
|
+
}
|
|
245
|
+
var MessageBusSemanticsSchema = z5.object({
|
|
246
|
+
name: z5.literal("message-bus"),
|
|
247
|
+
messageBus: z5.enum([
|
|
248
|
+
// The conventions write these two AWS buses differently, one with an
|
|
249
|
+
// underscore and one with a dot. Both are as the conventions give them.
|
|
250
|
+
"aws_sqs",
|
|
251
|
+
"aws.sns",
|
|
252
|
+
"s3",
|
|
253
|
+
"eventbridge",
|
|
254
|
+
// A Kinesis stream and a Firehose delivery stream are different
|
|
255
|
+
// APIs with different destinations, so a stream named `orders` and
|
|
256
|
+
// a delivery stream named `orders` are not the same channel.
|
|
257
|
+
"aws_kinesis",
|
|
258
|
+
"aws_firehose",
|
|
259
|
+
"gcp_pubsub",
|
|
260
|
+
"bullmq",
|
|
261
|
+
"kafka",
|
|
262
|
+
"nats",
|
|
263
|
+
"cloudflare-queues",
|
|
264
|
+
"cloudflare-cron",
|
|
265
|
+
"cloudflare-tail"
|
|
266
|
+
]),
|
|
267
|
+
/**
|
|
268
|
+
* A stable channel identifier: a CloudFormation logical id, a queue or
|
|
269
|
+
* topic name, a subject pattern, or `bus#detailType`. Null when this
|
|
270
|
+
* source does not say which channel, as with a send whose queue the
|
|
271
|
+
* code picks at run time, or a receive whose queue is set in the
|
|
272
|
+
* event-source mapping.
|
|
273
|
+
*/
|
|
274
|
+
channel: z5.string().min(1).nullable()
|
|
275
|
+
});
|
|
276
|
+
var messageBusSemantics = defineBoundarySemantics({
|
|
277
|
+
name: "message-bus",
|
|
278
|
+
schema: MessageBusSemanticsSchema,
|
|
279
|
+
semconv: {
|
|
280
|
+
messageBus: { name: "messaging.system" },
|
|
281
|
+
channel: { name: "messaging.destination.name" }
|
|
282
|
+
},
|
|
283
|
+
behavior: {
|
|
284
|
+
/** A message goes onto the channel and nothing comes back. */
|
|
285
|
+
exchangesHttpResponses: false,
|
|
286
|
+
leavesTheProcess: true,
|
|
287
|
+
/**
|
|
288
|
+
* `checkMessageBus` reports every channel, unused ones included, so
|
|
289
|
+
* the generic unmatched list leaves them out.
|
|
290
|
+
*/
|
|
291
|
+
reportsUnpairedItself: true,
|
|
292
|
+
/**
|
|
293
|
+
* A channel that is null here can still pair. The dedicated pass
|
|
294
|
+
* follows environment variable chains before comparing, so a
|
|
295
|
+
* consumer whose channel is set at run time meets the template that
|
|
296
|
+
* declares it.
|
|
297
|
+
*/
|
|
298
|
+
canPair: () => true,
|
|
299
|
+
/**
|
|
300
|
+
* `"bus:<messageBus> <subject>"`, or null when the channel is null
|
|
301
|
+
* or its subject is empty.
|
|
302
|
+
*
|
|
303
|
+
* The key has the subject and leaves out the bus, so a template
|
|
304
|
+
* writing `default#order.placed` and a handler writing
|
|
305
|
+
* `order.placed` land in one bucket, where `sidesAgree` compares the
|
|
306
|
+
* buses. Keying on the bus would separate a side that cannot know
|
|
307
|
+
* its bus from the side that can.
|
|
308
|
+
*
|
|
309
|
+
* The bus technology stays in, because a queue and an event router
|
|
310
|
+
* are different destinations even with the same subject. Case stays
|
|
311
|
+
* too, because AWS compares detail-types and queue ids byte for byte.
|
|
312
|
+
*/
|
|
313
|
+
identityKey(semantics) {
|
|
314
|
+
if (semantics.channel === null) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
const { subject } = parseChannel(semantics.channel);
|
|
318
|
+
if (subject === "") {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
return busIdentityKey(semantics.messageBus, subject);
|
|
322
|
+
},
|
|
323
|
+
sidesAgree(a, b) {
|
|
324
|
+
if (a.channel === null || b.channel === null) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
return channelsPair(a.channel, b.channel);
|
|
328
|
+
},
|
|
329
|
+
/**
|
|
330
|
+
* The whole channel, bus included, where the identity key has only
|
|
331
|
+
* the subject. A reader looking at unmatched channels needs to see
|
|
332
|
+
* which bus each one was on.
|
|
333
|
+
*/
|
|
334
|
+
displayLabel(semantics) {
|
|
335
|
+
if (semantics.channel === null) {
|
|
336
|
+
return `bus:${semantics.messageBus} (channel named at runtime)`;
|
|
337
|
+
}
|
|
338
|
+
return `bus:${semantics.messageBus} ${semantics.channel}`;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
var MetricSemanticsSchema = z6.object({
|
|
343
|
+
name: z6.literal("metric"),
|
|
344
|
+
/** The system the series is stored in, such as `cloud-monitoring` or `cloudwatch`. */
|
|
345
|
+
metricSystem: z6.string(),
|
|
346
|
+
/**
|
|
347
|
+
* The type string the system uses for the metric, written the same
|
|
348
|
+
* way on both sides. Null when the reader could not work the string
|
|
349
|
+
* out, as with a query it could not parse. A null type pairs with
|
|
350
|
+
* nothing, so it cannot pair with an unrelated metric that happens to
|
|
351
|
+
* share its source text.
|
|
352
|
+
*/
|
|
353
|
+
metricType: z6.string().nullable()
|
|
354
|
+
});
|
|
355
|
+
var metricSemantics = defineBoundarySemantics({
|
|
356
|
+
name: "metric",
|
|
357
|
+
schema: MetricSemanticsSchema,
|
|
358
|
+
// OpenTelemetry identifies a metric by its instrument name. It has no
|
|
359
|
+
// attribute for the metric's system or its type string.
|
|
360
|
+
semconv: {},
|
|
361
|
+
behavior: {
|
|
362
|
+
exchangesHttpResponses: false,
|
|
363
|
+
leavesTheProcess: true,
|
|
364
|
+
reportsUnpairedItself: false,
|
|
365
|
+
identityKey(semantics) {
|
|
366
|
+
if (semantics.metricType === null) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
return metricIdentityKey(semantics.metricSystem, semantics.metricType);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
var HOLE_SPLIT = /(\{[^}]*\})/;
|
|
374
|
+
function isHole(piece) {
|
|
375
|
+
return piece.startsWith("{") && piece.endsWith("}");
|
|
376
|
+
}
|
|
377
|
+
function parseBoundaryName(name) {
|
|
378
|
+
const parts = name.split(HOLE_SPLIT).filter((piece) => piece !== "").map(
|
|
379
|
+
(piece) => isHole(piece) ? { type: "hole", label: piece.slice(1, -1) } : { type: "text", text: piece }
|
|
380
|
+
);
|
|
381
|
+
const only = parts.length === 1 ? parts[0] : void 0;
|
|
382
|
+
if (only !== void 0 && only.type === "hole") {
|
|
383
|
+
return { type: "reference", path: only.label.split(".") };
|
|
384
|
+
}
|
|
385
|
+
if (parts.some((part) => part.type === "hole")) {
|
|
386
|
+
return { type: "pattern", parts };
|
|
387
|
+
}
|
|
388
|
+
return { type: "literal", value: name };
|
|
389
|
+
}
|
|
390
|
+
function patternHole(label) {
|
|
391
|
+
return `{${label}}`;
|
|
392
|
+
}
|
|
393
|
+
function namesNothing(name) {
|
|
394
|
+
return parseBoundaryName(name).type === "reference";
|
|
395
|
+
}
|
|
396
|
+
function referenceOf(name) {
|
|
397
|
+
if (name.type !== "reference") {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
const root = name.path[0];
|
|
401
|
+
if (root === void 0 || name.path.some((part) => part === "")) {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
return { root, fields: name.path.slice(1) };
|
|
405
|
+
}
|
|
406
|
+
function referenceFromName(name) {
|
|
407
|
+
return referenceOf(parseBoundaryName(name));
|
|
408
|
+
}
|
|
409
|
+
var SCHEME_ORIGIN = /^[a-zA-Z][a-zA-Z\d+.-]*:\/\/[^/]*/;
|
|
410
|
+
var PROTOCOL_RELATIVE_ORIGIN = /^\/\/[^/]*/;
|
|
411
|
+
function pathAfterOrigin(text) {
|
|
412
|
+
const withoutOrigin = text.replace(SCHEME_ORIGIN, "").replace(PROTOCOL_RELATIVE_ORIGIN, "");
|
|
413
|
+
const cut = withoutOrigin.search(/[?#]/);
|
|
414
|
+
return cut === -1 ? withoutOrigin : withoutOrigin.slice(0, cut);
|
|
415
|
+
}
|
|
416
|
+
var PRIVATE_SUFFIXES = [
|
|
417
|
+
"local",
|
|
418
|
+
"internal",
|
|
419
|
+
"localhost",
|
|
420
|
+
"lan",
|
|
421
|
+
"home.arpa",
|
|
422
|
+
"svc",
|
|
423
|
+
"test",
|
|
424
|
+
"example",
|
|
425
|
+
"invalid",
|
|
426
|
+
"example.com",
|
|
427
|
+
"example.net",
|
|
428
|
+
"example.org"
|
|
429
|
+
];
|
|
430
|
+
var PRIVATE_IPV4 = /^(?:127\.|10\.|192\.168\.|169\.254\.|172\.(?:1[6-9]|2\d|3[01])\.|0\.0\.0\.0$)/;
|
|
431
|
+
function hostnameOf(host) {
|
|
432
|
+
const lower = host.toLowerCase();
|
|
433
|
+
const name = lower.startsWith("[") ? lower.slice(0, lower.indexOf("]") + 1) : lower.split(":")[0] ?? lower;
|
|
434
|
+
return name.replace(/\.$/, "");
|
|
435
|
+
}
|
|
436
|
+
function hostCanBeOwn(host) {
|
|
437
|
+
const name = hostnameOf(host);
|
|
438
|
+
if (name === "[::1]" || /^\[f[cd]/.test(name) || PRIVATE_IPV4.test(name)) {
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
if (!name.includes(".")) {
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
return PRIVATE_SUFFIXES.some(
|
|
445
|
+
(suffix) => name === suffix || name.endsWith(`.${suffix}`)
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
var UNSPELLABLE_OPTION = /[()|?#{}]/;
|
|
449
|
+
function setPiece(options) {
|
|
450
|
+
if (options.some((option) => UNSPELLABLE_OPTION.test(option))) {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
return `(${options.join("|")})`;
|
|
454
|
+
}
|
|
455
|
+
var ONE = { kind: "one" };
|
|
456
|
+
var OPTIONAL = { kind: "optional" };
|
|
457
|
+
var STAR = { kind: "star" };
|
|
458
|
+
var HOLE_SEGMENT = /^\{[^{}]*?([?+*]?)\}$/;
|
|
459
|
+
var SET_PIECE = /\(([^()]*)\)/;
|
|
460
|
+
var ALTERNATIVE_CAP = 64;
|
|
461
|
+
function alternativesOf(path6) {
|
|
462
|
+
let alternatives = [path6];
|
|
463
|
+
for (; ; ) {
|
|
464
|
+
const next = [];
|
|
465
|
+
let expanded = false;
|
|
466
|
+
for (const alternative of alternatives) {
|
|
467
|
+
const set = SET_PIECE.exec(alternative);
|
|
468
|
+
if (set === null || set.index === void 0) {
|
|
469
|
+
next.push(alternative);
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
expanded = true;
|
|
473
|
+
const head = alternative.slice(0, set.index);
|
|
474
|
+
const tail = alternative.slice(set.index + set[0].length);
|
|
475
|
+
for (const option of (set[1] ?? "").split("|")) {
|
|
476
|
+
next.push(head + option + tail);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (!expanded) {
|
|
480
|
+
return alternatives;
|
|
481
|
+
}
|
|
482
|
+
if (next.length > ALTERNATIVE_CAP) {
|
|
483
|
+
return [path6.replace(new RegExp(SET_PIECE, "g"), "{value}")];
|
|
484
|
+
}
|
|
485
|
+
alternatives = next;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
var HOLE_RANGE_ITEMS = {
|
|
489
|
+
"": [ONE],
|
|
490
|
+
"?": [OPTIONAL],
|
|
491
|
+
"+": [ONE, STAR],
|
|
492
|
+
"*": [STAR]
|
|
493
|
+
};
|
|
494
|
+
function escapedForRegex(text) {
|
|
495
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
496
|
+
}
|
|
497
|
+
function shapedItem(segment) {
|
|
498
|
+
const source = segment.split(/(\{[^}]*\}|\*)/g).map((part) => {
|
|
499
|
+
if (part === "*") {
|
|
500
|
+
return ".*";
|
|
501
|
+
}
|
|
502
|
+
if (part.startsWith("{")) {
|
|
503
|
+
return /[?*]\}$/.test(part) ? "[^/]*" : "[^/]+";
|
|
504
|
+
}
|
|
505
|
+
return escapedForRegex(part);
|
|
506
|
+
}).join("");
|
|
507
|
+
const holes = segment.match(/\{[^}]*\}|\*/g) ?? [];
|
|
508
|
+
const text = segment.replace(/\{[^}]*\}|\*/g, "");
|
|
509
|
+
return {
|
|
510
|
+
kind: "shaped",
|
|
511
|
+
shape: segment.replace(/\{[^}]*\}/g, "{}"),
|
|
512
|
+
test: new RegExp(`^${source}$`),
|
|
513
|
+
bare: holes.every((hole) => hole === "*" || /[?*]\}$/.test(hole)) ? text : null,
|
|
514
|
+
textLength: text.length
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
function itemsOf(segment) {
|
|
518
|
+
if (segment === "*") {
|
|
519
|
+
return [STAR];
|
|
520
|
+
}
|
|
521
|
+
const hole = HOLE_SEGMENT.exec(segment);
|
|
522
|
+
if (hole !== null) {
|
|
523
|
+
return HOLE_RANGE_ITEMS[hole[1] ?? ""] ?? [ONE];
|
|
524
|
+
}
|
|
525
|
+
if (segment.includes("{") || segment.includes("*")) {
|
|
526
|
+
return [shapedItem(segment)];
|
|
527
|
+
}
|
|
528
|
+
return [{ kind: "text", text: segment }];
|
|
529
|
+
}
|
|
530
|
+
function segmentsOf(alternative) {
|
|
531
|
+
const trimmed = alternative.startsWith("/") ? alternative.slice(1) : alternative;
|
|
532
|
+
return trimmed === "" ? [] : trimmed.split("/");
|
|
533
|
+
}
|
|
534
|
+
function patternSegmentsOf(alternative) {
|
|
535
|
+
const segments = segmentsOf(alternative);
|
|
536
|
+
const first = segments[0];
|
|
537
|
+
const opensWithBaseUrl = !alternative.startsWith("/") && first !== void 0 && HOLE_SEGMENT.test(first);
|
|
538
|
+
return opensWithBaseUrl ? segments.slice(1) : segments;
|
|
539
|
+
}
|
|
540
|
+
function pathAfterBaseUrl(path6) {
|
|
541
|
+
const segments = segmentsOf(path6);
|
|
542
|
+
const first = segments[0];
|
|
543
|
+
if (path6.startsWith("/") || first === void 0 || segments.length < 2 || !HOLE_SEGMENT.test(first)) {
|
|
544
|
+
return path6;
|
|
545
|
+
}
|
|
546
|
+
return `/${segments.slice(1).join("/")}`;
|
|
547
|
+
}
|
|
548
|
+
var PATTERNS = /* @__PURE__ */ new Map();
|
|
549
|
+
var PATTERN_CACHE_LIMIT = 5e4;
|
|
550
|
+
function patternOf(path6) {
|
|
551
|
+
const known = PATTERNS.get(path6);
|
|
552
|
+
if (known !== void 0) {
|
|
553
|
+
return known;
|
|
554
|
+
}
|
|
555
|
+
const pattern = alternativesOf(path6).map(
|
|
556
|
+
(alternative) => patternSegmentsOf(alternative).flatMap(itemsOf)
|
|
557
|
+
);
|
|
558
|
+
if (PATTERNS.size >= PATTERN_CACHE_LIMIT) {
|
|
559
|
+
PATTERNS.clear();
|
|
560
|
+
}
|
|
561
|
+
PATTERNS.set(path6, pattern);
|
|
562
|
+
return pattern;
|
|
563
|
+
}
|
|
564
|
+
function literalItemsOf(path6) {
|
|
565
|
+
return segmentsOf(path6).map((text) => ({ kind: "text", text }));
|
|
566
|
+
}
|
|
567
|
+
function absorbsAnything(item) {
|
|
568
|
+
return item.kind === "one" || item.kind === "optional" || item.kind === "star";
|
|
569
|
+
}
|
|
570
|
+
function meetOnOneSegment(a, b) {
|
|
571
|
+
if (absorbsAnything(a) || absorbsAnything(b)) {
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
if (a.kind === "text" && b.kind === "text") {
|
|
575
|
+
return a.text === b.text;
|
|
576
|
+
}
|
|
577
|
+
if (a.kind === "text" && b.kind === "shaped") {
|
|
578
|
+
return b.test.test(a.text);
|
|
579
|
+
}
|
|
580
|
+
if (a.kind === "shaped" && b.kind === "text") {
|
|
581
|
+
return a.test.test(b.text);
|
|
582
|
+
}
|
|
583
|
+
return a.kind === "shaped" && b.kind === "shaped" && a.shape === b.shape;
|
|
584
|
+
}
|
|
585
|
+
function skippable(item) {
|
|
586
|
+
return item.kind === "optional" || item.kind === "star";
|
|
587
|
+
}
|
|
588
|
+
function itemsMeet(a, b) {
|
|
589
|
+
const width = b.length + 1;
|
|
590
|
+
const seen = new Uint8Array((a.length + 1) * width);
|
|
591
|
+
const queue = [0];
|
|
592
|
+
seen[0] = 1;
|
|
593
|
+
const visit = (i, j) => {
|
|
594
|
+
const at = i * width + j;
|
|
595
|
+
if (seen[at] === 0) {
|
|
596
|
+
seen[at] = 1;
|
|
597
|
+
queue.push(at);
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
while (queue.length > 0) {
|
|
601
|
+
const at = queue.pop();
|
|
602
|
+
const i = Math.floor(at / width);
|
|
603
|
+
const j = at % width;
|
|
604
|
+
if (i === a.length && j === b.length) {
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
const left = a[i];
|
|
608
|
+
const right = b[j];
|
|
609
|
+
if (left !== void 0 && skippable(left)) {
|
|
610
|
+
visit(i + 1, j);
|
|
611
|
+
}
|
|
612
|
+
if (right !== void 0 && skippable(right)) {
|
|
613
|
+
visit(i, j + 1);
|
|
614
|
+
}
|
|
615
|
+
if (left !== void 0 && right !== void 0) {
|
|
616
|
+
if (meetOnOneSegment(left, right)) {
|
|
617
|
+
visit(
|
|
618
|
+
left.kind === "star" ? i : i + 1,
|
|
619
|
+
right.kind === "star" ? j : j + 1
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
function pathsMeet(a, b) {
|
|
627
|
+
const left = patternOf(a);
|
|
628
|
+
const right = patternOf(b);
|
|
629
|
+
return left.some((one) => right.some((other) => itemsMeet(one, other)));
|
|
630
|
+
}
|
|
631
|
+
function patternAdmits(declared, request) {
|
|
632
|
+
const literal = literalItemsOf(request);
|
|
633
|
+
return patternOf(declared).some((items) => itemsMeet(items, literal));
|
|
634
|
+
}
|
|
635
|
+
function pathSpansShapes(path6) {
|
|
636
|
+
return /\{[^{}]*[?+*]\}|\(|(?:^|\/)\*(?:\/|$)/.test(path6);
|
|
637
|
+
}
|
|
638
|
+
function countOf(items, kinds) {
|
|
639
|
+
return items.filter((item) => kinds.includes(item.kind)).length;
|
|
640
|
+
}
|
|
641
|
+
function readingSpecificity(items) {
|
|
642
|
+
return [
|
|
643
|
+
countOf(items, ["text"]),
|
|
644
|
+
countOf(items, ["shaped"]),
|
|
645
|
+
-countOf(items, ["optional", "star"])
|
|
646
|
+
];
|
|
647
|
+
}
|
|
648
|
+
function segmentFit(route, request) {
|
|
649
|
+
if (!meetOnOneSegment(route, request)) {
|
|
650
|
+
return "apart";
|
|
651
|
+
}
|
|
652
|
+
if (request.kind === "text") {
|
|
653
|
+
if (route.kind === "text") {
|
|
654
|
+
return "stated";
|
|
655
|
+
}
|
|
656
|
+
return route.kind === "shaped" ? "shaped" : "free";
|
|
657
|
+
}
|
|
658
|
+
if (absorbsAnything(route)) {
|
|
659
|
+
return "free";
|
|
660
|
+
}
|
|
661
|
+
if (request.kind !== "shaped") {
|
|
662
|
+
return "guess";
|
|
663
|
+
}
|
|
664
|
+
if (route.kind === "shaped") {
|
|
665
|
+
return "free";
|
|
666
|
+
}
|
|
667
|
+
return route.kind === "text" && request.bare === route.text ? "stated" : "guess";
|
|
668
|
+
}
|
|
669
|
+
function shapedText(item) {
|
|
670
|
+
return item.kind === "shaped" ? item.textLength : 0;
|
|
671
|
+
}
|
|
672
|
+
function betterFit(a, b) {
|
|
673
|
+
return a === void 0 || compareRanks(b, a) > 0;
|
|
674
|
+
}
|
|
675
|
+
function bestFit(route, request) {
|
|
676
|
+
const width = request.length + 1;
|
|
677
|
+
const best = new Array((route.length + 1) * width);
|
|
678
|
+
best[0] = [0, 0];
|
|
679
|
+
const offer = (i, j, fit) => {
|
|
680
|
+
const at = i * width + j;
|
|
681
|
+
if (betterFit(best[at], fit)) {
|
|
682
|
+
best[at] = fit;
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
for (let reached = 0; reached <= route.length + request.length; reached += 1) {
|
|
686
|
+
for (let i = Math.max(0, reached - request.length); i <= Math.min(route.length, reached); i += 1) {
|
|
687
|
+
const j = reached - i;
|
|
688
|
+
const fit = best[i * width + j];
|
|
689
|
+
if (fit === void 0) {
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
const left = route[i];
|
|
693
|
+
const right = request[j];
|
|
694
|
+
if (left !== void 0 && skippable(left)) {
|
|
695
|
+
offer(i + 1, j, fit);
|
|
696
|
+
}
|
|
697
|
+
if (right !== void 0 && skippable(right)) {
|
|
698
|
+
offer(i, j + 1, fit);
|
|
699
|
+
}
|
|
700
|
+
if (left === void 0 || right === void 0) {
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
if (left.kind === "star" && right.kind === "star") {
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
const kind = segmentFit(left, right);
|
|
707
|
+
if (kind === "apart" || kind === "guess") {
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
offer(
|
|
711
|
+
left.kind === "star" ? i : i + 1,
|
|
712
|
+
right.kind === "star" ? j : j + 1,
|
|
713
|
+
[
|
|
714
|
+
fit[0] + (kind === "stated" ? 1 : 0),
|
|
715
|
+
fit[1] + (kind === "shaped" ? shapedText(left) : 0)
|
|
716
|
+
]
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return best[route.length * width + request.length] ?? null;
|
|
721
|
+
}
|
|
722
|
+
function requestRank(route, request) {
|
|
723
|
+
let found = null;
|
|
724
|
+
for (const routeItems of patternOf(route)) {
|
|
725
|
+
for (const requestItems of patternOf(request)) {
|
|
726
|
+
const fit = bestFit(routeItems, requestItems);
|
|
727
|
+
const rank = fit === null ? null : [...fit, ...readingSpecificity(routeItems)];
|
|
728
|
+
if (rank !== null && (found === null || compareRanks(rank, found) > 0)) {
|
|
729
|
+
found = rank;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return found;
|
|
734
|
+
}
|
|
735
|
+
function isCatchAll(route) {
|
|
736
|
+
return patternOf(route).some(
|
|
737
|
+
(items) => items.length > 0 && items.every(absorbsAnything) && items.some((item) => item.kind === "star")
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
function compareRanks(a, b) {
|
|
741
|
+
const length = Math.max(a.length, b.length);
|
|
742
|
+
for (let index = 0; index < length; index += 1) {
|
|
743
|
+
const difference = (a[index] ?? 0) - (b[index] ?? 0);
|
|
744
|
+
if (difference !== 0) {
|
|
745
|
+
return difference;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return 0;
|
|
749
|
+
}
|
|
750
|
+
var RestSemanticsSchema = z7.object({
|
|
751
|
+
name: z7.literal("rest"),
|
|
752
|
+
/**
|
|
753
|
+
* Uppercase HTTP method ("GET", "POST", …), `"*"` for a handler that
|
|
754
|
+
* responds to every method, or null when this source does not say
|
|
755
|
+
* which method.
|
|
756
|
+
*/
|
|
757
|
+
method: z7.string().min(1).nullable(),
|
|
758
|
+
/** Normalized route path ("/users/{id}"), or null when this source does not give one. */
|
|
759
|
+
path: z7.string().min(1).nullable(),
|
|
760
|
+
/**
|
|
761
|
+
* Status codes the producing source declared explicitly, such as
|
|
762
|
+
* OpenAPI responses or CloudFormation MethodResponses. They are kept
|
|
763
|
+
* here so the pairing layer can see them without unwrapping any
|
|
764
|
+
* metadata. Absent or empty for an inferred source.
|
|
765
|
+
*/
|
|
766
|
+
declaredResponses: z7.array(z7.number()).optional(),
|
|
767
|
+
/**
|
|
768
|
+
* The host a client wrote in an absolute URL, with its port
|
|
769
|
+
* (`api.example.com:8443`). A piece suss could not read is written
|
|
770
|
+
* `{name}`. Absent for a relative URL and for a route.
|
|
771
|
+
*/
|
|
772
|
+
host: z7.string().min(1).optional(),
|
|
773
|
+
/**
|
|
774
|
+
* True for a route the framework also serves with `.{format}` after
|
|
775
|
+
* its path, the way Rails serves `/search.json` from `get "search"`.
|
|
776
|
+
*/
|
|
777
|
+
optionalFormat: z7.boolean().optional()
|
|
778
|
+
});
|
|
779
|
+
var OPENING_HOLE = /^\{([^{}]+)\}(.*)$/;
|
|
780
|
+
function baseUrlReference(semantics) {
|
|
781
|
+
const label = OPENING_HOLE.exec(semantics.path ?? "")?.[1];
|
|
782
|
+
return label === void 0 ? null : referenceFromName(patternHole(label));
|
|
783
|
+
}
|
|
784
|
+
function normalizePath(path6) {
|
|
785
|
+
let normalized = withoutParameterConstraints(path6).replace(
|
|
786
|
+
/:([a-zA-Z_]\w*)([?+*]?)/g,
|
|
787
|
+
"{$1$2}"
|
|
788
|
+
);
|
|
789
|
+
if (normalized.length > 1 && normalized.endsWith("/")) {
|
|
790
|
+
normalized = normalized.slice(0, -1);
|
|
791
|
+
}
|
|
792
|
+
normalized = normalized.replace(
|
|
793
|
+
/\{[^}]+\}|[^{]+/g,
|
|
794
|
+
(segment) => segment.startsWith("{") ? segment : segment.toLowerCase()
|
|
795
|
+
);
|
|
796
|
+
return normalized;
|
|
797
|
+
}
|
|
798
|
+
function withoutParameterConstraints(path6) {
|
|
799
|
+
let out = "";
|
|
800
|
+
let i = 0;
|
|
801
|
+
while (i < path6.length) {
|
|
802
|
+
const name = /^:[a-zA-Z_]\w*\(/.exec(path6.slice(i));
|
|
803
|
+
if (name === null) {
|
|
804
|
+
out += path6[i];
|
|
805
|
+
i += 1;
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
const open = i + name[0].length - 1;
|
|
809
|
+
const close = closingParenthesis(path6, open);
|
|
810
|
+
if (close === -1) {
|
|
811
|
+
return out + path6.slice(i);
|
|
812
|
+
}
|
|
813
|
+
out += path6.slice(i, open);
|
|
814
|
+
i = close + 1;
|
|
815
|
+
}
|
|
816
|
+
return out;
|
|
817
|
+
}
|
|
818
|
+
function closingParenthesis(text, open) {
|
|
819
|
+
let depth = 0;
|
|
820
|
+
for (let i = open; i < text.length; i += 1) {
|
|
821
|
+
const char = text[i];
|
|
822
|
+
if (char === "\\") {
|
|
823
|
+
i += 1;
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
if (char === "(") {
|
|
827
|
+
depth += 1;
|
|
828
|
+
}
|
|
829
|
+
if (char === ")") {
|
|
830
|
+
depth -= 1;
|
|
831
|
+
if (depth === 0) {
|
|
832
|
+
return i;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return -1;
|
|
837
|
+
}
|
|
838
|
+
function pathShape(path6) {
|
|
839
|
+
return normalizePath(path6).replace(/\{[^{}]*?([?+*]?)\}/g, "{$1}");
|
|
840
|
+
}
|
|
841
|
+
function methodsAgree(a, b) {
|
|
842
|
+
if (a === null || b === null) {
|
|
843
|
+
return false;
|
|
844
|
+
}
|
|
845
|
+
return a === b || a === "*" || b === "*";
|
|
846
|
+
}
|
|
847
|
+
function routePathAdmits(declaredPath, requestPath) {
|
|
848
|
+
return patternAdmits(normalizePath(declaredPath), normalizePath(requestPath));
|
|
849
|
+
}
|
|
850
|
+
function readHost(semantics) {
|
|
851
|
+
const host = semantics.host;
|
|
852
|
+
return host === void 0 || /[{(]/.test(host) ? void 0 : host;
|
|
853
|
+
}
|
|
854
|
+
function hostsAgree(a, b) {
|
|
855
|
+
return readHost(a) === readHost(b);
|
|
856
|
+
}
|
|
857
|
+
function hostUnread(semantics) {
|
|
858
|
+
if (semantics.host !== void 0) {
|
|
859
|
+
return readHost(semantics) === void 0;
|
|
860
|
+
}
|
|
861
|
+
return baseUrlReference(semantics) !== null;
|
|
862
|
+
}
|
|
863
|
+
function servedPath(semantics, path6) {
|
|
864
|
+
const normalized = normalizePath(path6);
|
|
865
|
+
return semantics.optionalFormat === true ? `${normalized}(|.{format})` : normalized;
|
|
866
|
+
}
|
|
867
|
+
function withoutHost(semantics) {
|
|
868
|
+
const { host: _host, ...rest } = semantics;
|
|
869
|
+
return rest;
|
|
870
|
+
}
|
|
871
|
+
function groundedBaseUrl(semantics, deployment) {
|
|
872
|
+
const reference = baseUrlReference(semantics);
|
|
873
|
+
if (reference === null) {
|
|
874
|
+
return null;
|
|
875
|
+
}
|
|
876
|
+
const base = deployment.setTo(reference);
|
|
877
|
+
if (base === null) {
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
const rest = OPENING_HOLE.exec(semantics.path ?? "")?.[2] ?? "";
|
|
881
|
+
const grounded = pathAfterOrigin(`${base}${rest}`);
|
|
882
|
+
return { ...semantics, path: grounded === "" ? "/" : grounded };
|
|
883
|
+
}
|
|
884
|
+
var restSemantics = defineBoundarySemantics({
|
|
885
|
+
name: "rest",
|
|
886
|
+
schema: RestSemanticsSchema,
|
|
887
|
+
semconv: {
|
|
888
|
+
// A span states the one method a request used, so a route that
|
|
889
|
+
// responds to every method has nothing to compare.
|
|
890
|
+
method: { name: "http.request.method", placeholderValues: ["*"] },
|
|
891
|
+
path: { name: "http.route" }
|
|
892
|
+
// declaredResponses is a list, and http.response.status_code is
|
|
893
|
+
// the one status a request got back.
|
|
894
|
+
},
|
|
895
|
+
behavior: {
|
|
896
|
+
/** A request goes out, a status and a body come back. */
|
|
897
|
+
exchangesHttpResponses: true,
|
|
898
|
+
leavesTheProcess: true,
|
|
899
|
+
reportsUnpairedItself: false,
|
|
900
|
+
/** `"METHOD /normalized/path"`, or null when either half is missing. */
|
|
901
|
+
identityKey(semantics) {
|
|
902
|
+
if (semantics.method === null || semantics.path === null) {
|
|
903
|
+
return null;
|
|
904
|
+
}
|
|
905
|
+
return `${semantics.method.toUpperCase()} ${normalizePath(semantics.path)}`;
|
|
906
|
+
},
|
|
907
|
+
/**
|
|
908
|
+
* The bucket has only the path, so a `"*"` route lands with the
|
|
909
|
+
* consumers that each give a method, and `methodsAgree` compares the
|
|
910
|
+
* methods inside the bucket. The identity key is still what a reader
|
|
911
|
+
* sees and what a suppression targets ("GET /users", "* /users").
|
|
912
|
+
*
|
|
913
|
+
* Parameter names are dropped too. A hand-written spec writes
|
|
914
|
+
* `{userId}` where the Express route that serves it writes `:id`,
|
|
915
|
+
* and both match the same requests. Pairing on the name would leave
|
|
916
|
+
* two boundaries that never meet.
|
|
917
|
+
*/
|
|
918
|
+
pairingKey(semantics) {
|
|
919
|
+
if (semantics.method === null || semantics.path === null) {
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
const shape = pathShape(pathAfterBaseUrl(normalizePath(semantics.path)));
|
|
923
|
+
const host = readHost(semantics);
|
|
924
|
+
return host === void 0 ? `rest ${shape}` : `rest //${host}${shape}`;
|
|
925
|
+
},
|
|
926
|
+
/**
|
|
927
|
+
* A spec writes `/teams/{team_id}` where the route that serves it
|
|
928
|
+
* writes `/teams/:id`. The method stays in, so a `"*"` handler is
|
|
929
|
+
* never compared with the document for one method.
|
|
930
|
+
*/
|
|
931
|
+
operationKey(semantics) {
|
|
932
|
+
if (semantics.method === null || semantics.path === null) {
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
935
|
+
return `${semantics.method.toUpperCase()} ${pathShape(semantics.path)}`;
|
|
936
|
+
},
|
|
937
|
+
sidesAgree(a, b) {
|
|
938
|
+
return methodsAgree(a.method, b.method) && hostsAgree(a, b);
|
|
939
|
+
},
|
|
940
|
+
spansBuckets(semantics) {
|
|
941
|
+
return semantics.path !== null && (semantics.optionalFormat === true || pathSpansShapes(normalizePath(semantics.path)));
|
|
942
|
+
},
|
|
943
|
+
bucketsMeet(a, b) {
|
|
944
|
+
return a.path !== null && b.path !== null && hostsAgree(a, b) && pathsMeet(servedPath(a, a.path), servedPath(b, b.path));
|
|
945
|
+
},
|
|
946
|
+
/**
|
|
947
|
+
* A catch-all route never takes a call whose host suss could not
|
|
948
|
+
* read, since nothing else ties that call to this app.
|
|
949
|
+
*/
|
|
950
|
+
pairRank(provider, consumer) {
|
|
951
|
+
if (provider.path === null || consumer.path === null) {
|
|
952
|
+
return [];
|
|
953
|
+
}
|
|
954
|
+
const route = normalizePath(provider.path);
|
|
955
|
+
if (hostUnread(consumer) && isCatchAll(route)) {
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
return requestRank(
|
|
959
|
+
servedPath(provider, provider.path),
|
|
960
|
+
normalizePath(consumer.path)
|
|
961
|
+
);
|
|
962
|
+
},
|
|
963
|
+
/**
|
|
964
|
+
* The identity key, with a missing half still readable: `ANY` when
|
|
965
|
+
* the source gave no method, and `?` when it gave no path. Null when
|
|
966
|
+
* both are missing, since then there is nothing to show.
|
|
967
|
+
*/
|
|
968
|
+
displayLabel(semantics) {
|
|
969
|
+
if (semantics.method === null && semantics.path === null) {
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
972
|
+
const method = semantics.method === null ? "ANY" : semantics.method.toUpperCase();
|
|
973
|
+
const path6 = semantics.path === null ? "?" : normalizePath(semantics.path);
|
|
974
|
+
return `${method} ${path6}`;
|
|
975
|
+
},
|
|
976
|
+
/**
|
|
977
|
+
* With both halves present, the method goes through `methodsAgree`
|
|
978
|
+
* (so a `"*"` route matches every method) and the path through
|
|
979
|
+
* `routePathAdmits`. A route missing either half might still handle
|
|
980
|
+
* the request, and this declaration cannot settle that, so the
|
|
981
|
+
* result is `"unknown"`.
|
|
982
|
+
*/
|
|
983
|
+
servesRequest(semantics, method, path6) {
|
|
984
|
+
if (semantics.method === null || semantics.path === null) {
|
|
985
|
+
return "unknown";
|
|
986
|
+
}
|
|
987
|
+
if (!methodsAgree(semantics.method, method.toUpperCase())) {
|
|
988
|
+
return "nomatch";
|
|
989
|
+
}
|
|
990
|
+
return routePathAdmits(semantics.path, path6) ? "match" : "nomatch";
|
|
991
|
+
},
|
|
992
|
+
/**
|
|
993
|
+
* A route the registration pattern admits is inside it. A route
|
|
994
|
+
* without a path counts as outside, because nothing shows that the
|
|
995
|
+
* pattern covers it.
|
|
996
|
+
*/
|
|
997
|
+
withinScope(semantics, scope) {
|
|
998
|
+
return semantics.path !== null && routePathAdmits(scope, semantics.path);
|
|
999
|
+
},
|
|
1000
|
+
/**
|
|
1001
|
+
* A call whose base URL the deployment fills in, resolved to the
|
|
1002
|
+
* path it reaches, and a call to a host that can be the project's
|
|
1003
|
+
* own, with the host left out so it meets the route.
|
|
1004
|
+
*
|
|
1005
|
+
* The source cannot settle a base URL on its own, so the adapter
|
|
1006
|
+
* leaves the hole in. `API_BASE` could be `http://backend.internal`,
|
|
1007
|
+
* and then the path is `/orders`. It could equally be `/api/v2`, and
|
|
1008
|
+
* then the path is `/api/v2/orders`. Filling in the deployed value
|
|
1009
|
+
* and reading the path back out gets the right one either way, and
|
|
1010
|
+
* a base the deployment sets counts as the app's own.
|
|
1011
|
+
*/
|
|
1012
|
+
/** A listed host matches with or without the port the call gives. */
|
|
1013
|
+
withoutOwnHost(semantics, ownHosts) {
|
|
1014
|
+
const host = semantics.host?.toLowerCase();
|
|
1015
|
+
if (host === void 0) {
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
return ownHosts.has(host) || ownHosts.has(host.replace(/:\d+$/, "")) ? withoutHost(semantics) : null;
|
|
1019
|
+
},
|
|
1020
|
+
groundName(semantics, deployment) {
|
|
1021
|
+
const based = groundedBaseUrl(semantics, deployment);
|
|
1022
|
+
const grounded = based ?? semantics;
|
|
1023
|
+
const host = readHost(grounded);
|
|
1024
|
+
return host !== void 0 && hostCanBeOwn(host) ? withoutHost(grounded) : based;
|
|
1025
|
+
},
|
|
1026
|
+
nameReference: baseUrlReference,
|
|
1027
|
+
ruleBoundary: {
|
|
1028
|
+
// "METHOD /path": one token, a space, then an absolute path.
|
|
1029
|
+
claims(raw) {
|
|
1030
|
+
return /^\S+ +\//.test(raw.trim());
|
|
1031
|
+
},
|
|
1032
|
+
normalize(raw) {
|
|
1033
|
+
const trimmed = raw.trim();
|
|
1034
|
+
const spaceIdx = trimmed.indexOf(" ");
|
|
1035
|
+
const method = trimmed.slice(0, spaceIdx).toUpperCase();
|
|
1036
|
+
return `${method} ${normalizePath(trimmed.slice(spaceIdx + 1).trim())}`;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
var DeployableUnitSchema = z8.object({
|
|
1042
|
+
deploymentTarget: z8.enum([
|
|
1043
|
+
"lambda",
|
|
1044
|
+
"ecs-task",
|
|
1045
|
+
"container",
|
|
1046
|
+
"k8s-deployment",
|
|
1047
|
+
"worker"
|
|
1048
|
+
]),
|
|
1049
|
+
// An empty name would agree with every other empty name, so a unit
|
|
1050
|
+
// with no name has to leave the field off instead.
|
|
1051
|
+
instanceName: z8.string().min(1)
|
|
1052
|
+
});
|
|
1053
|
+
var RuntimeConfigSemanticsSchema = DeployableUnitSchema.partial({
|
|
1054
|
+
deploymentTarget: true,
|
|
1055
|
+
instanceName: true
|
|
1056
|
+
}).extend({
|
|
1057
|
+
name: z9.literal("runtime-config")
|
|
1058
|
+
});
|
|
1059
|
+
var runtimeConfigSemantics = defineBoundarySemantics({
|
|
1060
|
+
name: "runtime-config",
|
|
1061
|
+
schema: RuntimeConfigSemanticsSchema,
|
|
1062
|
+
// service.name and cloud.resource_id identify the same deployable, but
|
|
1063
|
+
// instanceName is the template's logical id, a different string from
|
|
1064
|
+
// both.
|
|
1065
|
+
semconv: {},
|
|
1066
|
+
behavior: {
|
|
1067
|
+
/** A process reads its config at startup, and nothing replies. */
|
|
1068
|
+
exchangesHttpResponses: false,
|
|
1069
|
+
// Whoever deploys the process puts the value there, so the value
|
|
1070
|
+
// comes from outside the code.
|
|
1071
|
+
leavesTheProcess: true,
|
|
1072
|
+
reportsUnpairedItself: false,
|
|
1073
|
+
identityKey: () => null,
|
|
1074
|
+
// A reader sees which deployable's environment this is. Code that
|
|
1075
|
+
// reads a variable does not say which deployable runs it, so a read
|
|
1076
|
+
// has no name.
|
|
1077
|
+
displayLabel(semantics) {
|
|
1078
|
+
const instance = semantics.instanceName;
|
|
1079
|
+
return instance === void 0 ? null : `runtime-config:${instance}`;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
1083
|
+
var StorageSemanticsSchema = z10.object({
|
|
1084
|
+
name: z10.literal("storage"),
|
|
1085
|
+
/**
|
|
1086
|
+
* The storage system: an OpenTelemetry value such as `postgresql` or
|
|
1087
|
+
* `aws.dynamodb`, or suss's own word for one the conventions do not
|
|
1088
|
+
* cover, such as `s3`. Null when the reader could not work out which
|
|
1089
|
+
* system the source uses. Two products' containers can share a name,
|
|
1090
|
+
* and this field keeps them apart. The pairing pass decides how to
|
|
1091
|
+
* treat a null.
|
|
1092
|
+
*/
|
|
1093
|
+
storageSystem: z10.string().nullable(),
|
|
1094
|
+
/**
|
|
1095
|
+
* The ORM, schema, or deployment scope. A single-database setup uses
|
|
1096
|
+
* `"default"`, and a monorepo with several schemas gives each one its
|
|
1097
|
+
* own value.
|
|
1098
|
+
*/
|
|
1099
|
+
scope: z10.string(),
|
|
1100
|
+
/**
|
|
1101
|
+
* The table, bucket, collection or index, as the source declares it.
|
|
1102
|
+
* Null when the reader could not work out the container, and a null
|
|
1103
|
+
* container pairs with nothing, so it cannot match by accident of
|
|
1104
|
+
* shared source text. The string uses the boundary-name syntax (a
|
|
1105
|
+
* literal, a pattern with `{}` holes, or a reference), and only
|
|
1106
|
+
* `parseBoundaryName` should read it.
|
|
1107
|
+
*/
|
|
1108
|
+
container: z10.string().nullable(),
|
|
1109
|
+
/**
|
|
1110
|
+
* A secondary way into the container, such as a DynamoDB global
|
|
1111
|
+
* secondary index or an Elasticsearch alias, each with its own key
|
|
1112
|
+
* fields. Null means the container's primary key. A query through an
|
|
1113
|
+
* index and a query through the table are different accesses, so
|
|
1114
|
+
* they pair separately.
|
|
1115
|
+
*/
|
|
1116
|
+
accessPath: z10.string().nullable()
|
|
1117
|
+
});
|
|
1118
|
+
function containerReference(semantics) {
|
|
1119
|
+
return semantics.container === null ? null : referenceFromName(semantics.container);
|
|
1120
|
+
}
|
|
1121
|
+
var storageSemantics = defineBoundarySemantics({
|
|
1122
|
+
name: "storage",
|
|
1123
|
+
schema: StorageSemanticsSchema,
|
|
1124
|
+
semconv: {
|
|
1125
|
+
storageSystem: { name: "db.system.name" },
|
|
1126
|
+
// suss writes "default" when the source did not say which database.
|
|
1127
|
+
scope: { name: "db.namespace", placeholderValues: ["default"] },
|
|
1128
|
+
container: { name: "db.collection.name" }
|
|
1129
|
+
// The conventions have no attribute for a secondary index, so
|
|
1130
|
+
// accessPath is left out.
|
|
1131
|
+
},
|
|
1132
|
+
behavior: {
|
|
1133
|
+
/** A query returns rows or items, with no status. */
|
|
1134
|
+
exchangesHttpResponses: false,
|
|
1135
|
+
leavesTheProcess: true,
|
|
1136
|
+
reportsUnpairedItself: false,
|
|
1137
|
+
identityKey: () => null,
|
|
1138
|
+
displayLabel: storageLabel,
|
|
1139
|
+
/**
|
|
1140
|
+
* A container the source gives as a variable is whatever the
|
|
1141
|
+
* deployment sets that variable to. `{SUBSCRIBERS_TABLE}` in the
|
|
1142
|
+
* code and `prod-subscribers-v1` in the manifest are one table.
|
|
1143
|
+
*/
|
|
1144
|
+
groundName(semantics, deployment) {
|
|
1145
|
+
const reference = containerReference(semantics);
|
|
1146
|
+
if (reference === null) {
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
const name = deployment.setTo(reference);
|
|
1150
|
+
return name === null ? null : { ...semantics, container: name };
|
|
1151
|
+
},
|
|
1152
|
+
nameReference: containerReference
|
|
1153
|
+
}
|
|
1154
|
+
});
|
|
1155
|
+
function storageLabel(semantics) {
|
|
1156
|
+
return `${storageSystemLabel(semantics)}:${storageContainerLabel(semantics)}`;
|
|
1157
|
+
}
|
|
1158
|
+
function storageSystemLabel(semantics) {
|
|
1159
|
+
return semantics.storageSystem ?? UNKNOWN_ENGINE;
|
|
1160
|
+
}
|
|
1161
|
+
var UNKNOWN_ENGINE = "<unknown engine>";
|
|
1162
|
+
function storageContainerLabel(semantics) {
|
|
1163
|
+
const container = semantics.container ?? "<unnamed container>";
|
|
1164
|
+
const addressed = semantics.accessPath === null ? container : `${container}#${semantics.accessPath}`;
|
|
1165
|
+
return semantics.scope === "default" ? addressed : `${semantics.scope}/${addressed}`;
|
|
1166
|
+
}
|
|
1167
|
+
var UnitInvocationSemanticsSchema = DeployableUnitSchema.extend({
|
|
1168
|
+
name: z11.literal("unit-invocation"),
|
|
1169
|
+
instanceName: DeployableUnitSchema.shape.instanceName.nullable()
|
|
1170
|
+
});
|
|
1171
|
+
function calleeReference(semantics) {
|
|
1172
|
+
return semantics.instanceName === null ? null : referenceFromName(semantics.instanceName);
|
|
1173
|
+
}
|
|
1174
|
+
var unitInvocationSemantics = defineBoundarySemantics({
|
|
1175
|
+
name: "unit-invocation",
|
|
1176
|
+
schema: UnitInvocationSemanticsSchema,
|
|
1177
|
+
// faas.name is the deployed function's own name and instanceName is
|
|
1178
|
+
// the deployment template's logical id, which is a different string.
|
|
1179
|
+
// Runtime config leaves the pair off for the same reason.
|
|
1180
|
+
semconv: {},
|
|
1181
|
+
behavior: {
|
|
1182
|
+
/**
|
|
1183
|
+
* A synchronous invoke gives back whatever the handler returned,
|
|
1184
|
+
* which is a value rather than a status and a body. A handler's
|
|
1185
|
+
* return only becomes an HTTP response under a proxy integration,
|
|
1186
|
+
* and that route gets a REST binding instead of this one.
|
|
1187
|
+
*/
|
|
1188
|
+
exchangesHttpResponses: false,
|
|
1189
|
+
leavesTheProcess: true,
|
|
1190
|
+
/**
|
|
1191
|
+
* A unit nothing in the run invokes is the ordinary case, since
|
|
1192
|
+
* most services are reached from a stack suss never read. So an
|
|
1193
|
+
* uninvoked unit stays in the generic unmatched list rather than
|
|
1194
|
+
* being reported as a problem here.
|
|
1195
|
+
*/
|
|
1196
|
+
reportsUnpairedItself: false,
|
|
1197
|
+
/**
|
|
1198
|
+
* A callee the source gives only as a variable has no name until the
|
|
1199
|
+
* deployment fills it in. It gets no key, since a key built from the
|
|
1200
|
+
* variable would pair only with itself.
|
|
1201
|
+
*/
|
|
1202
|
+
identityKey(semantics) {
|
|
1203
|
+
if (semantics.instanceName === null || namesNothing(semantics.instanceName)) {
|
|
1204
|
+
return null;
|
|
1205
|
+
}
|
|
1206
|
+
return unitIdentityKey(
|
|
1207
|
+
semantics.deploymentTarget,
|
|
1208
|
+
semantics.instanceName
|
|
1209
|
+
);
|
|
1210
|
+
},
|
|
1211
|
+
displayLabel(semantics) {
|
|
1212
|
+
if (semantics.instanceName === null) {
|
|
1213
|
+
return `unit:${semantics.deploymentTarget} (named at runtime)`;
|
|
1214
|
+
}
|
|
1215
|
+
return unitIdentityKey(
|
|
1216
|
+
semantics.deploymentTarget,
|
|
1217
|
+
semantics.instanceName
|
|
1218
|
+
);
|
|
1219
|
+
},
|
|
1220
|
+
/**
|
|
1221
|
+
* A call that reads its callee's name from the environment reaches
|
|
1222
|
+
* whichever resource the template wires that variable to. The name
|
|
1223
|
+
* is grounded to the logical id and not the deployed name, because
|
|
1224
|
+
* the invoked unit's own summary is keyed by the logical id.
|
|
1225
|
+
*/
|
|
1226
|
+
groundName(semantics, deployment) {
|
|
1227
|
+
const reference = calleeReference(semantics);
|
|
1228
|
+
if (reference === null) {
|
|
1229
|
+
return null;
|
|
1230
|
+
}
|
|
1231
|
+
const logicalId = deployment.pointsAt(reference);
|
|
1232
|
+
return logicalId === null ? null : { ...semantics, instanceName: logicalId };
|
|
1233
|
+
},
|
|
1234
|
+
nameReference: calleeReference
|
|
1235
|
+
}
|
|
1236
|
+
});
|
|
1237
|
+
var SemanticsSchema = z12.discriminatedUnion("name", [
|
|
1238
|
+
restSemantics.schema,
|
|
1239
|
+
functionCallSemantics.schema,
|
|
1240
|
+
graphqlResolverSemantics.schema,
|
|
1241
|
+
graphqlOperationSemantics.schema,
|
|
1242
|
+
runtimeConfigSemantics.schema,
|
|
1243
|
+
storageSemantics.schema,
|
|
1244
|
+
messageBusSemantics.schema,
|
|
1245
|
+
metricSemantics.schema,
|
|
1246
|
+
unitInvocationSemantics.schema,
|
|
1247
|
+
ioSemantics.schema
|
|
1248
|
+
]);
|
|
1249
|
+
var DEFINITIONS = [
|
|
1250
|
+
restSemantics,
|
|
1251
|
+
functionCallSemantics,
|
|
1252
|
+
graphqlResolverSemantics,
|
|
1253
|
+
graphqlOperationSemantics,
|
|
1254
|
+
runtimeConfigSemantics,
|
|
1255
|
+
storageSemantics,
|
|
1256
|
+
messageBusSemantics,
|
|
1257
|
+
metricSemantics,
|
|
1258
|
+
unitInvocationSemantics,
|
|
1259
|
+
ioSemantics
|
|
1260
|
+
];
|
|
1261
|
+
var BY_NAME = new Map(
|
|
1262
|
+
DEFINITIONS.map((d) => [d.name, d])
|
|
1263
|
+
);
|
|
1264
|
+
var ConfidenceSourceSchema = z13.enum([
|
|
1265
|
+
"inferred_static",
|
|
1266
|
+
"inferred_ai",
|
|
1267
|
+
"declared",
|
|
1268
|
+
"derived"
|
|
1269
|
+
]);
|
|
1270
|
+
var ConfidenceLevelSchema = z13.enum(["high", "medium", "low"]);
|
|
1271
|
+
var CorroborationSchema = z13.object({
|
|
1272
|
+
outcome: z13.enum(["observed", "refuted", "untested"]),
|
|
1273
|
+
/** Executions that produced a verdict for this claim. */
|
|
1274
|
+
runs: z13.number(),
|
|
1275
|
+
/** Present when refuted: the input and observation that disagreed. */
|
|
1276
|
+
counterexample: z13.unknown().optional(),
|
|
1277
|
+
/** Present when untested: why no verdict was reachable. */
|
|
1278
|
+
reason: z13.string().optional()
|
|
1279
|
+
});
|
|
1280
|
+
var ConfidenceInfoSchema = z13.object({
|
|
1281
|
+
source: ConfidenceSourceSchema,
|
|
1282
|
+
level: ConfidenceLevelSchema,
|
|
1283
|
+
corroboration: CorroborationSchema.optional()
|
|
1284
|
+
});
|
|
1285
|
+
var SourceLocationSchema = z13.object({
|
|
1286
|
+
file: z13.string(),
|
|
1287
|
+
/** Line numbers, for a person reading the summary or an editor link. */
|
|
1288
|
+
range: z13.object({ start: z13.number(), end: z13.number() }),
|
|
1289
|
+
/**
|
|
1290
|
+
* Character offsets of the unit in its file. Identity and joins use
|
|
1291
|
+
* these, because two functions can share a line but never an offset
|
|
1292
|
+
* range. `range` gives the same place in lines. Absent on a summary
|
|
1293
|
+
* with no source position, such as one read from a contract.
|
|
1294
|
+
*/
|
|
1295
|
+
span: z13.object({ start: z13.number(), end: z13.number() }).optional(),
|
|
1296
|
+
exportName: z13.string().nullable(),
|
|
1297
|
+
/**
|
|
1298
|
+
* The project the extract ran on, by the name the project gives
|
|
1299
|
+
* itself.
|
|
1300
|
+
*
|
|
1301
|
+
* Paths are relative to wherever the extract ran, so two services in
|
|
1302
|
+
* one repository can both report `src/handlers.ts`, and merging their
|
|
1303
|
+
* summaries would mix them up. This field tells them apart, and a
|
|
1304
|
+
* reader groups by it.
|
|
1305
|
+
*/
|
|
1306
|
+
workspace: z13.string().optional(),
|
|
1307
|
+
/**
|
|
1308
|
+
* The module of the application the unit belongs to, by the name the
|
|
1309
|
+
* project's `suss.json` gives it. Set only when the project lists its
|
|
1310
|
+
* modules, and then on every unit whose file is under a module's root.
|
|
1311
|
+
*/
|
|
1312
|
+
module: z13.string().optional()
|
|
1313
|
+
});
|
|
1314
|
+
var BoundaryBindingSchema = z13.object({
|
|
1315
|
+
transport: z13.string(),
|
|
1316
|
+
semantics: SemanticsSchema,
|
|
1317
|
+
recognition: z13.string()
|
|
1318
|
+
});
|
|
1319
|
+
var TypeShapeSchema = z13.lazy(
|
|
1320
|
+
() => z13.discriminatedUnion("type", [
|
|
1321
|
+
z13.object({
|
|
1322
|
+
type: z13.literal("record"),
|
|
1323
|
+
properties: z13.record(z13.string(), TypeShapeSchema),
|
|
1324
|
+
spreads: z13.array(z13.object({ sourceText: z13.string() })).optional()
|
|
1325
|
+
}),
|
|
1326
|
+
z13.object({ type: z13.literal("dictionary"), values: TypeShapeSchema }),
|
|
1327
|
+
z13.object({ type: z13.literal("array"), items: TypeShapeSchema }),
|
|
1328
|
+
z13.object({
|
|
1329
|
+
type: z13.literal("literal"),
|
|
1330
|
+
value: z13.union([z13.string(), z13.number(), z13.boolean()]),
|
|
1331
|
+
raw: z13.string().optional()
|
|
1332
|
+
}),
|
|
1333
|
+
z13.object({ type: z13.literal("text") }),
|
|
1334
|
+
z13.object({ type: z13.literal("integer") }),
|
|
1335
|
+
z13.object({ type: z13.literal("number") }),
|
|
1336
|
+
z13.object({ type: z13.literal("boolean") }),
|
|
1337
|
+
z13.object({ type: z13.literal("null") }),
|
|
1338
|
+
z13.object({ type: z13.literal("undefined") }),
|
|
1339
|
+
z13.object({
|
|
1340
|
+
type: z13.literal("union"),
|
|
1341
|
+
variants: z13.array(TypeShapeSchema)
|
|
1342
|
+
}),
|
|
1343
|
+
z13.object({
|
|
1344
|
+
type: z13.literal("ref"),
|
|
1345
|
+
name: z13.string(),
|
|
1346
|
+
from: z13.string().optional(),
|
|
1347
|
+
def: z13.string().optional()
|
|
1348
|
+
}),
|
|
1349
|
+
z13.object({ type: z13.literal("unknown") })
|
|
1350
|
+
])
|
|
1351
|
+
);
|
|
1352
|
+
|
|
1353
|
+
// ../ir-core/dist/index.js
|
|
1354
|
+
import { z as z14 } from "zod";
|
|
1355
|
+
import { z as z22 } from "zod";
|
|
1356
|
+
var RelationSchema = z14.enum([
|
|
1357
|
+
"provides",
|
|
1358
|
+
"reads",
|
|
1359
|
+
"writes",
|
|
1360
|
+
"invokes"
|
|
1361
|
+
]);
|
|
1362
|
+
var EffectRelationSchema = RelationSchema.exclude(["provides"]);
|
|
1363
|
+
var SuppressionSideSchema = z22.object({
|
|
1364
|
+
summary: z22.string().optional(),
|
|
1365
|
+
transitionId: z22.string().optional()
|
|
1366
|
+
}).optional();
|
|
1367
|
+
var SuppressionRuleSchema = z22.object({
|
|
1368
|
+
/**
|
|
1369
|
+
* The finding kind to match, behavioral or intent. The schema accepts
|
|
1370
|
+
* any string because ir-core cannot import either IR's kind enum. The
|
|
1371
|
+
* CLI's loader checks it against both.
|
|
1372
|
+
*/
|
|
1373
|
+
kind: z22.string().optional(),
|
|
1374
|
+
/**
|
|
1375
|
+
* The boundary's human-readable key, such as "GET /pet/{petId}" or
|
|
1376
|
+
* "fn:@acme/api::getUser". A REST key goes through the same path
|
|
1377
|
+
* normalizer the checkers use, so `:id` and `{id}` compare equal.
|
|
1378
|
+
*/
|
|
1379
|
+
boundary: z22.string().optional(),
|
|
1380
|
+
/**
|
|
1381
|
+
* Consumer-side discriminators, the narrowest useful match. They
|
|
1382
|
+
* apply only to behavioral findings. A rule that sets `consumer`
|
|
1383
|
+
* never matches an intent finding, because an intent finding has no
|
|
1384
|
+
* consumer side.
|
|
1385
|
+
*/
|
|
1386
|
+
consumer: SuppressionSideSchema,
|
|
1387
|
+
/**
|
|
1388
|
+
* Provider-side discriminators, matching the same fields as
|
|
1389
|
+
* `consumer`. A finding about a status the provider produces has its
|
|
1390
|
+
* transition id on this side, and that id is the only value narrow
|
|
1391
|
+
* enough to pick out that one finding. As with `consumer`, a rule
|
|
1392
|
+
* that sets `provider` never matches an intent finding.
|
|
1393
|
+
*/
|
|
1394
|
+
provider: SuppressionSideSchema,
|
|
1395
|
+
/**
|
|
1396
|
+
* The title of one PRD scenario. A PRD finding's boundary is the
|
|
1397
|
+
* whole PRD, so without this a rule accepts every scenario in it,
|
|
1398
|
+
* including one added later. A rule that sets `scenario` matches
|
|
1399
|
+
* only intent findings about a scenario with that title.
|
|
1400
|
+
*/
|
|
1401
|
+
scenario: z22.string().optional(),
|
|
1402
|
+
/**
|
|
1403
|
+
* "narrow", the default, requires `kind` plus one of `boundary`,
|
|
1404
|
+
* `scenario`, `consumer.transitionId` or `provider.transitionId`, so
|
|
1405
|
+
* the rule targets a specific class of finding. "broad" allows a
|
|
1406
|
+
* match on kind alone or boundary alone, and such a rule also
|
|
1407
|
+
* silences future regressions in that category.
|
|
1408
|
+
*/
|
|
1409
|
+
scope: z22.enum(["narrow", "broad"]).default("narrow"),
|
|
1410
|
+
/** Required human-written justification. */
|
|
1411
|
+
reason: z22.string().min(1),
|
|
1412
|
+
/** What to do when a finding matches. */
|
|
1413
|
+
effect: z22.enum(["mark", "downgrade", "hide"]).default("mark")
|
|
1414
|
+
}).strict();
|
|
1415
|
+
var SuppressionFileSchema = z22.object({
|
|
1416
|
+
version: z22.literal(1),
|
|
1417
|
+
rules: z22.array(SuppressionRuleSchema)
|
|
1418
|
+
}).strict();
|
|
1419
|
+
|
|
1420
|
+
// ../framework/rails/dist/index.js
|
|
1421
|
+
var ADDED_PATHS = /config\.(?:eager_load_paths|autoload_paths|autoload_once_paths)\s*(?:<<|\+=|\.push\(|\.unshift\(|\.concat\()\s*([^\n]*)/g;
|
|
1422
|
+
var ROOT_JOIN = /Rails\.root\.join\(\s*([^)]*)\)/g;
|
|
1423
|
+
var INTERPOLATED_ROOT = /#\{(?:Rails\.root|config\.root|root)\}\/([\w./-]+)/g;
|
|
1424
|
+
var AUTOLOAD_LIB = /config\.autoload_lib(?:_once)?\b/;
|
|
1425
|
+
var EVERY_LITERAL = /(["'])([^"']*)\1/g;
|
|
1426
|
+
function applicationFile(configDirectory) {
|
|
1427
|
+
const file = applicationPath(configDirectory);
|
|
1428
|
+
return fs.existsSync(file) ? [file] : [];
|
|
1429
|
+
}
|
|
1430
|
+
function applicationPath(configDirectory) {
|
|
1431
|
+
return path.join(configDirectory, "config", "application.rb");
|
|
1432
|
+
}
|
|
1433
|
+
function readAutoloadRoots(configDirectory) {
|
|
1434
|
+
const file = applicationPath(configDirectory);
|
|
1435
|
+
let source;
|
|
1436
|
+
try {
|
|
1437
|
+
source = fs.readFileSync(file, "utf8");
|
|
1438
|
+
} catch {
|
|
1439
|
+
return [];
|
|
1440
|
+
}
|
|
1441
|
+
const relative = [];
|
|
1442
|
+
for (const line of source.split("\n")) {
|
|
1443
|
+
if (line.trimStart().startsWith("#")) {
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1446
|
+
if (AUTOLOAD_LIB.test(line)) {
|
|
1447
|
+
relative.push("lib");
|
|
1448
|
+
}
|
|
1449
|
+
for (const added of line.matchAll(ADDED_PATHS)) {
|
|
1450
|
+
relative.push(...directoriesIn(added[1] ?? ""));
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
return [...new Set(relative)].map((directory) => path.resolve(configDirectory, directory)).filter((directory) => isDirectory(directory));
|
|
1454
|
+
}
|
|
1455
|
+
function directoriesIn(fragment) {
|
|
1456
|
+
const joined = [...fragment.matchAll(ROOT_JOIN)].map(
|
|
1457
|
+
(join) => [...(join[1] ?? "").matchAll(EVERY_LITERAL)].map((literal) => literal[2] ?? "").join("/")
|
|
1458
|
+
);
|
|
1459
|
+
const interpolated = [...fragment.matchAll(INTERPOLATED_ROOT)].map(
|
|
1460
|
+
(match) => match[1] ?? ""
|
|
1461
|
+
);
|
|
1462
|
+
return [...joined, ...interpolated].filter((one) => one !== "");
|
|
1463
|
+
}
|
|
1464
|
+
function isDirectory(directory) {
|
|
1465
|
+
try {
|
|
1466
|
+
return fs.statSync(directory).isDirectory();
|
|
1467
|
+
} catch {
|
|
1468
|
+
return false;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
27
1471
|
var ENGINE_BASE_NAMES = /* @__PURE__ */ new Set(["Rails::Engine", "::Rails::Engine"]);
|
|
28
1472
|
var CONSTANT_TYPES = /* @__PURE__ */ new Set(["constant", "scope_resolution"]);
|
|
29
1473
|
function readEngines(roots, acronyms = []) {
|
|
@@ -31,7 +1475,7 @@ function readEngines(roots, acronyms = []) {
|
|
|
31
1475
|
for (const root of roots) {
|
|
32
1476
|
const routesFile = engineRoutesFile(root);
|
|
33
1477
|
for (const file of engineFilesUnder(root)) {
|
|
34
|
-
const source =
|
|
1478
|
+
const source = fs2.readFileSync(file, "utf8");
|
|
35
1479
|
const rootNode = parseRubySync(source).rootNode;
|
|
36
1480
|
for (const found of engineClassesIn(rootNode, acronyms)) {
|
|
37
1481
|
engines.push({ ...found, routesFile });
|
|
@@ -48,48 +1492,45 @@ function engineSourceFiles(root) {
|
|
|
48
1492
|
];
|
|
49
1493
|
}
|
|
50
1494
|
function engineRoutesFile(root) {
|
|
51
|
-
const routesFile =
|
|
52
|
-
return
|
|
1495
|
+
const routesFile = path2.join(root, "config", "routes.rb");
|
|
1496
|
+
return fs2.existsSync(routesFile) ? routesFile : null;
|
|
53
1497
|
}
|
|
54
1498
|
function engineFilesUnder(root) {
|
|
55
|
-
const directory =
|
|
56
|
-
if (!
|
|
1499
|
+
const directory = path2.join(root, "lib");
|
|
1500
|
+
if (!fs2.existsSync(directory)) {
|
|
57
1501
|
return [];
|
|
58
1502
|
}
|
|
59
1503
|
return rubyFilesUnder(directory).filter(
|
|
60
|
-
(file) =>
|
|
1504
|
+
(file) => fs2.readFileSync(file, "utf8").includes("Rails::Engine")
|
|
61
1505
|
);
|
|
62
1506
|
}
|
|
63
1507
|
function expandPathPattern(pattern) {
|
|
64
|
-
const segments = pattern.split(
|
|
65
|
-
const start =
|
|
1508
|
+
const segments = pattern.split(path2.sep).filter((s) => s !== "");
|
|
1509
|
+
const start = path2.isAbsolute(pattern) ? path2.sep : ".";
|
|
66
1510
|
let candidates = [start];
|
|
67
1511
|
for (const segment of segments) {
|
|
68
1512
|
if (!segment.includes("*")) {
|
|
69
|
-
candidates = candidates.map((base) =>
|
|
1513
|
+
candidates = candidates.map((base) => path2.join(base, segment));
|
|
70
1514
|
continue;
|
|
71
1515
|
}
|
|
72
1516
|
const matcher = new RegExp(
|
|
73
1517
|
`^${segment.split("*").map(escapeRegExp).join(".*")}$`
|
|
74
1518
|
);
|
|
75
1519
|
candidates = candidates.flatMap(
|
|
76
|
-
(base) =>
|
|
1520
|
+
(base) => fs2.existsSync(base) ? fs2.readdirSync(base).filter((name) => matcher.test(name)).sort().map((name) => path2.join(base, name)) : []
|
|
77
1521
|
);
|
|
78
1522
|
}
|
|
79
|
-
return candidates.filter((candidate) =>
|
|
1523
|
+
return candidates.filter((candidate) => fs2.existsSync(candidate));
|
|
80
1524
|
}
|
|
81
1525
|
function escapeRegExp(text) {
|
|
82
1526
|
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
83
1527
|
}
|
|
84
1528
|
function rubyFilesUnder(directory) {
|
|
85
|
-
return
|
|
1529
|
+
return fs2.readdirSync(directory, { withFileTypes: true, recursive: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".rb")).map((entry) => path2.join(entry.parentPath, entry.name)).sort();
|
|
86
1530
|
}
|
|
87
1531
|
function engineClassesIn(node, acronyms, enclosing = []) {
|
|
88
1532
|
const found = [];
|
|
89
|
-
for (const child of node
|
|
90
|
-
if (child === null) {
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
1533
|
+
for (const child of children(node)) {
|
|
93
1534
|
if (child.type === "class" && extendsEngine(child)) {
|
|
94
1535
|
const name = definedName(child);
|
|
95
1536
|
if (name !== void 0) {
|
|
@@ -110,10 +1551,10 @@ function definedName(definition) {
|
|
|
110
1551
|
}
|
|
111
1552
|
function extendsEngine(classNode) {
|
|
112
1553
|
const superclass = field(classNode, "superclass");
|
|
113
|
-
const base = superclass
|
|
114
|
-
(child) =>
|
|
1554
|
+
const base = children(superclass).find(
|
|
1555
|
+
(child) => CONSTANT_TYPES.has(child.type)
|
|
115
1556
|
);
|
|
116
|
-
return base !== void 0 &&
|
|
1557
|
+
return base !== void 0 && ENGINE_BASE_NAMES.has(base.text);
|
|
117
1558
|
}
|
|
118
1559
|
function isolatedNamespaceOf(classNode, acronyms) {
|
|
119
1560
|
const body = field(classNode, "body");
|
|
@@ -137,7 +1578,7 @@ var IRREGULAR_CALL = /\.irregular\s*\(?\s*(["'])([^"']*)\1\s*,\s*(["'])([^"']*)\
|
|
|
137
1578
|
var UNCOUNTABLE_CALL = /\.uncountable\s*\(?\s*([^\n]*)/g;
|
|
138
1579
|
var SINGULAR_CALL = /\.singular\s*\(?\s*(?:(["'])([^"']*)\1|\/([^/]*)\/([a-z]*))\s*,\s*(["'])([^"']*)\5/g;
|
|
139
1580
|
var WORD_LIST = /%[wi][([{]([^)\]}]*)[)\]}]/;
|
|
140
|
-
var
|
|
1581
|
+
var EVERY_LITERAL2 = /(["'])([^"']*)\1/g;
|
|
141
1582
|
var ANY_CALL = /\.(acronym|irregular|uncountable|singular)[\s("'/]/;
|
|
142
1583
|
function group(match, at) {
|
|
143
1584
|
return match[at] ?? "";
|
|
@@ -148,7 +1589,7 @@ function readInflections(configDirectory) {
|
|
|
148
1589
|
const uncountable = /* @__PURE__ */ new Set();
|
|
149
1590
|
const singular = [];
|
|
150
1591
|
for (const file of inflectionFiles(configDirectory)) {
|
|
151
|
-
const source = uncommented(
|
|
1592
|
+
const source = uncommented(fs3.readFileSync(file, "utf8"));
|
|
152
1593
|
for (const match of source.matchAll(ACRONYM_CALL)) {
|
|
153
1594
|
acronyms.add(group(match, 2));
|
|
154
1595
|
}
|
|
@@ -182,18 +1623,130 @@ function wordsIn(argument) {
|
|
|
182
1623
|
if (list !== null) {
|
|
183
1624
|
return group(list, 1).split(/\s+/).filter((word) => word.length > 0);
|
|
184
1625
|
}
|
|
185
|
-
return [...argument.matchAll(
|
|
1626
|
+
return [...argument.matchAll(EVERY_LITERAL2)].map((match) => group(match, 2));
|
|
186
1627
|
}
|
|
187
1628
|
function inflectionFiles(configDirectory) {
|
|
188
|
-
const directory =
|
|
189
|
-
if (!
|
|
1629
|
+
const directory = path3.join(configDirectory, "config", "initializers");
|
|
1630
|
+
if (!fs3.existsSync(directory)) {
|
|
190
1631
|
return [];
|
|
191
1632
|
}
|
|
192
|
-
return
|
|
1633
|
+
return fs3.readdirSync(directory, { withFileTypes: true, recursive: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".rb")).map((entry) => path3.join(entry.parentPath, entry.name)).filter((file) => ANY_CALL.test(uncommented(fs3.readFileSync(file, "utf8")))).sort();
|
|
193
1634
|
}
|
|
194
1635
|
function uncommented(source) {
|
|
195
1636
|
return source.split("\n").filter((line) => !line.trimStart().startsWith("#")).join("\n");
|
|
196
1637
|
}
|
|
1638
|
+
var EXPANSION_CAP = 64;
|
|
1639
|
+
function closingParenthesis2(text, open) {
|
|
1640
|
+
let depth = 0;
|
|
1641
|
+
for (let index = open; index < text.length; index += 1) {
|
|
1642
|
+
const char = text[index];
|
|
1643
|
+
if (char === "(") {
|
|
1644
|
+
depth += 1;
|
|
1645
|
+
}
|
|
1646
|
+
if (char === ")") {
|
|
1647
|
+
depth -= 1;
|
|
1648
|
+
if (depth === 0) {
|
|
1649
|
+
return index;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
return -1;
|
|
1654
|
+
}
|
|
1655
|
+
function expansions(text) {
|
|
1656
|
+
let found = [""];
|
|
1657
|
+
let index = 0;
|
|
1658
|
+
while (index < text.length) {
|
|
1659
|
+
const open = text.indexOf("(", index);
|
|
1660
|
+
const close = text.indexOf(")", index);
|
|
1661
|
+
if (close !== -1 && (open === -1 || close < open)) {
|
|
1662
|
+
return null;
|
|
1663
|
+
}
|
|
1664
|
+
if (open === -1) {
|
|
1665
|
+
const rest = text.slice(index);
|
|
1666
|
+
found = found.map((prefix) => prefix + rest);
|
|
1667
|
+
break;
|
|
1668
|
+
}
|
|
1669
|
+
const end = closingParenthesis2(text, open);
|
|
1670
|
+
const inner = end === -1 ? null : expansions(text.slice(open + 1, end));
|
|
1671
|
+
if (inner === null) {
|
|
1672
|
+
return null;
|
|
1673
|
+
}
|
|
1674
|
+
const before = text.slice(index, open);
|
|
1675
|
+
const options = ["", ...inner];
|
|
1676
|
+
found = found.flatMap(
|
|
1677
|
+
(prefix) => options.map((option) => prefix + before + option)
|
|
1678
|
+
);
|
|
1679
|
+
if (found.length > EXPANSION_CAP) {
|
|
1680
|
+
found = found.slice(0, 1);
|
|
1681
|
+
}
|
|
1682
|
+
index = end + 1;
|
|
1683
|
+
}
|
|
1684
|
+
return found;
|
|
1685
|
+
}
|
|
1686
|
+
function tidied(path6) {
|
|
1687
|
+
const squeezed = `/${path6}`.replace(/\/{2,}/g, "/");
|
|
1688
|
+
return squeezed.length > 1 ? squeezed.replace(/\/$/, "") : squeezed;
|
|
1689
|
+
}
|
|
1690
|
+
function segmentsOf2(path6) {
|
|
1691
|
+
return path6 === "/" ? [] : path6.slice(1).split("/");
|
|
1692
|
+
}
|
|
1693
|
+
function sharedCount(lists, limit, fromEnd) {
|
|
1694
|
+
const first = lists[0] ?? [];
|
|
1695
|
+
const at = (list, offset) => fromEnd ? list[list.length - 1 - offset] : list[offset];
|
|
1696
|
+
let count = 0;
|
|
1697
|
+
while (count < limit && lists.every((list) => at(list, count) === at(first, count))) {
|
|
1698
|
+
count += 1;
|
|
1699
|
+
}
|
|
1700
|
+
return count;
|
|
1701
|
+
}
|
|
1702
|
+
var ENDS_IN_PARAMETER = /:[A-Za-z_]\w*$/;
|
|
1703
|
+
function headBeforeSet(lists, shortest) {
|
|
1704
|
+
const first = lists[0] ?? [];
|
|
1705
|
+
let count = sharedCount(lists, shortest, false);
|
|
1706
|
+
while (count > 0 && ENDS_IN_PARAMETER.test(first[count - 1] ?? "")) {
|
|
1707
|
+
count -= 1;
|
|
1708
|
+
}
|
|
1709
|
+
return count;
|
|
1710
|
+
}
|
|
1711
|
+
function asOnePath(paths) {
|
|
1712
|
+
const lists = paths.map(segmentsOf2);
|
|
1713
|
+
const shortest = Math.min(...lists.map((list) => list.length));
|
|
1714
|
+
const headCount = headBeforeSet(lists, shortest);
|
|
1715
|
+
const tailCount = sharedCount(lists, shortest - headCount, true);
|
|
1716
|
+
const first = lists[0] ?? [];
|
|
1717
|
+
const head = first.slice(0, headCount).join("/");
|
|
1718
|
+
const tail = first.slice(first.length - tailCount).join("/");
|
|
1719
|
+
const middles = lists.map(
|
|
1720
|
+
(list) => list.slice(headCount, list.length - tailCount).join("/")
|
|
1721
|
+
);
|
|
1722
|
+
if (head !== "") {
|
|
1723
|
+
const set2 = setPiece(middles.map((middle) => middle ? `/${middle}` : ""));
|
|
1724
|
+
return set2 === null ? null : `/${head}${set2}${tail ? `/${tail}` : ""}`;
|
|
1725
|
+
}
|
|
1726
|
+
const set = setPiece(
|
|
1727
|
+
middles.map((middle) => middle && tail ? `${middle}/` : middle)
|
|
1728
|
+
);
|
|
1729
|
+
return set === null ? null : `/${set}${tail}`;
|
|
1730
|
+
}
|
|
1731
|
+
function withGlobsAsHoles(path6) {
|
|
1732
|
+
return path6.replace(/(^|[/(])\*([A-Za-z_]\w*)/g, "$1:$2+");
|
|
1733
|
+
}
|
|
1734
|
+
function pathWithOptionalGroups(written) {
|
|
1735
|
+
const path6 = withGlobsAsHoles(written);
|
|
1736
|
+
if (!path6.includes("(")) {
|
|
1737
|
+
return path6;
|
|
1738
|
+
}
|
|
1739
|
+
const expanded = expansions(path6);
|
|
1740
|
+
if (expanded === null) {
|
|
1741
|
+
return path6;
|
|
1742
|
+
}
|
|
1743
|
+
const paths = [...new Set(expanded.map(tidied))];
|
|
1744
|
+
const withoutGroups = paths[0] ?? path6;
|
|
1745
|
+
if (paths.length === 1) {
|
|
1746
|
+
return withoutGroups;
|
|
1747
|
+
}
|
|
1748
|
+
return asOnePath(paths) ?? withoutGroups;
|
|
1749
|
+
}
|
|
197
1750
|
function readSimpleArgs(call, defaults = {}) {
|
|
198
1751
|
const argumentList = field2(call, "arguments");
|
|
199
1752
|
const positional = [];
|
|
@@ -261,23 +1814,33 @@ function readSymbolList(node) {
|
|
|
261
1814
|
const single = symbolValue(node);
|
|
262
1815
|
return single === null ? null : [single];
|
|
263
1816
|
}
|
|
1817
|
+
var updateAt = (path6) => [
|
|
1818
|
+
{ method: "PATCH", path: path6 },
|
|
1819
|
+
{ method: "PUT", path: path6 }
|
|
1820
|
+
];
|
|
264
1821
|
var RESTFUL_ROUTES_PLURAL = {
|
|
265
|
-
index: (base) =>
|
|
266
|
-
create: (base) =>
|
|
267
|
-
new: (base) =>
|
|
268
|
-
show: (base) =>
|
|
269
|
-
edit: (base) =>
|
|
270
|
-
update: (base) => (
|
|
271
|
-
destroy: (base) =>
|
|
1822
|
+
index: (base) => [{ method: "GET", path: base }],
|
|
1823
|
+
create: (base) => [{ method: "POST", path: base }],
|
|
1824
|
+
new: (base) => [{ method: "GET", path: `${base}/new` }],
|
|
1825
|
+
show: (base) => [{ method: "GET", path: `${base}/:id` }],
|
|
1826
|
+
edit: (base) => [{ method: "GET", path: `${base}/:id/edit` }],
|
|
1827
|
+
update: (base) => updateAt(`${base}/:id`),
|
|
1828
|
+
destroy: (base) => [{ method: "DELETE", path: `${base}/:id` }]
|
|
272
1829
|
};
|
|
273
1830
|
var RESTFUL_ROUTES_SINGULAR = {
|
|
274
|
-
create: (base) =>
|
|
275
|
-
new: (base) =>
|
|
276
|
-
show: (base) =>
|
|
277
|
-
edit: (base) =>
|
|
278
|
-
update: (base) => (
|
|
279
|
-
destroy: (base) =>
|
|
1831
|
+
create: (base) => [{ method: "POST", path: base }],
|
|
1832
|
+
new: (base) => [{ method: "GET", path: `${base}/new` }],
|
|
1833
|
+
show: (base) => [{ method: "GET", path: base }],
|
|
1834
|
+
edit: (base) => [{ method: "GET", path: `${base}/edit` }],
|
|
1835
|
+
update: (base) => updateAt(base),
|
|
1836
|
+
destroy: (base) => [{ method: "DELETE", path: base }]
|
|
280
1837
|
};
|
|
1838
|
+
function restfulRoutes(action, base) {
|
|
1839
|
+
if (!Object.hasOwn(RESTFUL_ROUTES_PLURAL, action)) {
|
|
1840
|
+
return [];
|
|
1841
|
+
}
|
|
1842
|
+
return RESTFUL_ROUTES_PLURAL[action]?.(base) ?? [];
|
|
1843
|
+
}
|
|
281
1844
|
function pluralize(word) {
|
|
282
1845
|
if (/[^aeiou]y$/.test(word)) {
|
|
283
1846
|
return `${word.slice(0, -1)}ies`;
|
|
@@ -317,12 +1880,19 @@ var RouteAccumulator = class {
|
|
|
317
1880
|
mounting = /* @__PURE__ */ new Set();
|
|
318
1881
|
/** The file whose statements are being walked, as a gap message prints it. */
|
|
319
1882
|
file = "";
|
|
320
|
-
/**
|
|
1883
|
+
/** An action routed from several places serves every one of them, so each distinct route is kept. */
|
|
321
1884
|
add(controllerKey, action, route) {
|
|
322
1885
|
const key = `${controllerKey}#${action}`;
|
|
323
|
-
|
|
324
|
-
|
|
1886
|
+
const routes = this.byKey.get(key) ?? [];
|
|
1887
|
+
const path6 = pathWithOptionalGroups(route.path);
|
|
1888
|
+
if (!routes.some((one) => one.method === route.method && one.path === path6)) {
|
|
1889
|
+
routes.push({
|
|
1890
|
+
method: route.method,
|
|
1891
|
+
path: path6,
|
|
1892
|
+
...route.formatless === true ? { formatless: true } : {}
|
|
1893
|
+
});
|
|
325
1894
|
}
|
|
1895
|
+
this.byKey.set(key, routes);
|
|
326
1896
|
}
|
|
327
1897
|
recordUnread(callName) {
|
|
328
1898
|
addUnderFile(this.unread, this.file, callName);
|
|
@@ -333,8 +1903,8 @@ var RouteAccumulator = class {
|
|
|
333
1903
|
recordMissingDrawn(name) {
|
|
334
1904
|
this.missingDrawn.push({ file: this.file, name });
|
|
335
1905
|
}
|
|
336
|
-
|
|
337
|
-
return this.byKey.get(`${controllerKey}#${action}`) ??
|
|
1906
|
+
routesFor(controllerKey, action) {
|
|
1907
|
+
return this.byKey.get(`${controllerKey}#${action}`) ?? [];
|
|
338
1908
|
}
|
|
339
1909
|
/** Runs `walk` with gaps attributed to `file`, then goes back to the file being walked before. */
|
|
340
1910
|
inFile(file, walk) {
|
|
@@ -359,7 +1929,7 @@ var RouteAccumulator = class {
|
|
|
359
1929
|
}
|
|
360
1930
|
for (const { file, name } of this.missingDrawn) {
|
|
361
1931
|
gaps.push(
|
|
362
|
-
`${file} draws ${name}, but there is no ${name}.rb under ${
|
|
1932
|
+
`${file} draws ${name}, but there is no ${name}.rb under ${path4.basename(this.drawDirectory)}/ beside it to read; whatever that file routes is missing from what suss reports`
|
|
363
1933
|
);
|
|
364
1934
|
}
|
|
365
1935
|
return gaps;
|
|
@@ -396,14 +1966,16 @@ function handleResourceCall(call, ctx, out, plural) {
|
|
|
396
1966
|
const only = readSymbolList(args.keyword.only);
|
|
397
1967
|
const except = readSymbolList(args.keyword.except);
|
|
398
1968
|
const table = plural ? RESTFUL_ROUTES_PLURAL : RESTFUL_ROUTES_SINGULAR;
|
|
399
|
-
for (const [action,
|
|
1969
|
+
for (const [action, routesAt] of Object.entries(table)) {
|
|
400
1970
|
if (only !== null && !only.includes(action)) {
|
|
401
1971
|
continue;
|
|
402
1972
|
}
|
|
403
1973
|
if (except?.includes(action)) {
|
|
404
1974
|
continue;
|
|
405
1975
|
}
|
|
406
|
-
|
|
1976
|
+
for (const route of routesAt(base)) {
|
|
1977
|
+
out.add(controllerKey, action, route);
|
|
1978
|
+
}
|
|
407
1979
|
}
|
|
408
1980
|
const nestedBase = plural ? `${base}/:${singularize(symbol)}_id` : base;
|
|
409
1981
|
const nested = {
|
|
@@ -447,13 +2019,15 @@ function handleVerb(call, ctx, out, method) {
|
|
|
447
2019
|
const on = wordValue(args.keyword.on, ctx);
|
|
448
2020
|
const base = ctx.resource === void 0 ? ctx.pathPrefix : baseForOn(ctx.resource, on);
|
|
449
2021
|
const target = readRouteTarget(args, ctx);
|
|
2022
|
+
const format = formatOf(args);
|
|
450
2023
|
if (target !== null) {
|
|
451
2024
|
const literalPath = args.positional[0] ? textValue(args.positional[0], ctx) : null;
|
|
452
|
-
const
|
|
453
|
-
if (
|
|
2025
|
+
const path6 = target.path ?? literalPath;
|
|
2026
|
+
if (path6 !== null) {
|
|
454
2027
|
out.add(joinKey(ctx.modulePrefix, target.controllerKey), target.action, {
|
|
455
2028
|
method,
|
|
456
|
-
path: joinPath(base,
|
|
2029
|
+
path: joinPath(base, path6),
|
|
2030
|
+
...format
|
|
457
2031
|
});
|
|
458
2032
|
}
|
|
459
2033
|
return;
|
|
@@ -468,7 +2042,14 @@ function handleVerb(call, ctx, out, method) {
|
|
|
468
2042
|
if (controllerKey === void 0) {
|
|
469
2043
|
return;
|
|
470
2044
|
}
|
|
471
|
-
out.add(controllerKey, action, {
|
|
2045
|
+
out.add(controllerKey, action, {
|
|
2046
|
+
method,
|
|
2047
|
+
path: joinPath(base, segment),
|
|
2048
|
+
...format
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
function formatOf(args) {
|
|
2052
|
+
return args.keyword.format?.type === "false" ? { formatless: true } : {};
|
|
472
2053
|
}
|
|
473
2054
|
function handleRoot(call, ctx, out) {
|
|
474
2055
|
const args = readSimpleArgs(call, ctx.defaults);
|
|
@@ -598,9 +2179,9 @@ function handleMatch(call, ctx, out) {
|
|
|
598
2179
|
}
|
|
599
2180
|
}
|
|
600
2181
|
function drawDirectoryOf(routesFile) {
|
|
601
|
-
return
|
|
602
|
-
|
|
603
|
-
|
|
2182
|
+
return path4.join(
|
|
2183
|
+
path4.dirname(routesFile),
|
|
2184
|
+
path4.basename(routesFile, path4.extname(routesFile))
|
|
604
2185
|
);
|
|
605
2186
|
}
|
|
606
2187
|
function handleDraw(call, ctx, out) {
|
|
@@ -609,16 +2190,16 @@ function handleDraw(call, ctx, out) {
|
|
|
609
2190
|
if (name === null) {
|
|
610
2191
|
return;
|
|
611
2192
|
}
|
|
612
|
-
const file =
|
|
2193
|
+
const file = path4.join(out.drawDirectory, `${name}.rb`);
|
|
613
2194
|
if (out.drawn.has(file)) {
|
|
614
2195
|
return;
|
|
615
2196
|
}
|
|
616
2197
|
out.drawn.add(file);
|
|
617
|
-
if (!
|
|
2198
|
+
if (!fs4.existsSync(file)) {
|
|
618
2199
|
out.recordMissingDrawn(name);
|
|
619
2200
|
return;
|
|
620
2201
|
}
|
|
621
|
-
const tree = parseRubySync2(
|
|
2202
|
+
const tree = parseRubySync2(fs4.readFileSync(file, "utf8"));
|
|
622
2203
|
walkBody(tree.rootNode, ctx, out);
|
|
623
2204
|
}
|
|
624
2205
|
var HANDLERS = {
|
|
@@ -726,10 +2307,8 @@ function routeDeclarations(root, displayPath) {
|
|
|
726
2307
|
found.push(declaration);
|
|
727
2308
|
return;
|
|
728
2309
|
}
|
|
729
|
-
for (const child of node
|
|
730
|
-
|
|
731
|
-
visit(child);
|
|
732
|
-
}
|
|
2310
|
+
for (const child of children2(node)) {
|
|
2311
|
+
visit(child);
|
|
733
2312
|
}
|
|
734
2313
|
};
|
|
735
2314
|
visit(root);
|
|
@@ -756,16 +2335,16 @@ function isApplication(owner) {
|
|
|
756
2335
|
return owner === "Rails.application" || /(^|::)Application$/.test(owner);
|
|
757
2336
|
}
|
|
758
2337
|
function parseDeclarations(source) {
|
|
759
|
-
if (!
|
|
2338
|
+
if (!fs4.existsSync(source.file)) {
|
|
760
2339
|
return [];
|
|
761
2340
|
}
|
|
762
|
-
const tree = parseRubySync2(
|
|
2341
|
+
const tree = parseRubySync2(fs4.readFileSync(source.file, "utf8"));
|
|
763
2342
|
return routeDeclarations(tree.rootNode, source.displayPath);
|
|
764
2343
|
}
|
|
765
2344
|
function readRoutes(input) {
|
|
766
|
-
if (!
|
|
2345
|
+
if (!fs4.existsSync(input.routesFile.file)) {
|
|
767
2346
|
return {
|
|
768
|
-
|
|
2347
|
+
routesFor: () => [],
|
|
769
2348
|
fileFound: false,
|
|
770
2349
|
gaps: [
|
|
771
2350
|
`${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`
|
|
@@ -806,7 +2385,7 @@ function readRoutes(input) {
|
|
|
806
2385
|
});
|
|
807
2386
|
}
|
|
808
2387
|
return {
|
|
809
|
-
|
|
2388
|
+
routesFor: (controllerKey, action) => out.routesFor(controllerKey, action),
|
|
810
2389
|
fileFound: true,
|
|
811
2390
|
gaps: out.gaps()
|
|
812
2391
|
};
|
|
@@ -879,65 +2458,127 @@ var RACK_STATUS_CODE_NAMES = {
|
|
|
879
2458
|
not_extended: 510,
|
|
880
2459
|
network_authentication_required: 511
|
|
881
2460
|
};
|
|
882
|
-
var
|
|
2461
|
+
var STANDARD_ERROR = ["StandardError", "Exception"];
|
|
2462
|
+
var ACTION_CONTROLLER_ERROR = [
|
|
2463
|
+
"ActionController::ActionControllerError",
|
|
2464
|
+
...STANDARD_ERROR
|
|
2465
|
+
];
|
|
2466
|
+
var ACTIVE_RECORD_ERROR = [
|
|
2467
|
+
"ActiveRecord::ActiveRecordError",
|
|
2468
|
+
...STANDARD_ERROR
|
|
2469
|
+
];
|
|
2470
|
+
var INDEX_ERROR = ["IndexError", ...STANDARD_ERROR];
|
|
2471
|
+
var LIBRARY_EXCEPTIONS = {
|
|
2472
|
+
"ActionController::ActionControllerError": { ancestors: STANDARD_ERROR },
|
|
2473
|
+
"ActionController::BadRequest": {
|
|
2474
|
+
status: 400,
|
|
2475
|
+
ancestors: ACTION_CONTROLLER_ERROR
|
|
2476
|
+
},
|
|
2477
|
+
"ActionController::ParameterMissing": {
|
|
2478
|
+
status: 400,
|
|
2479
|
+
ancestors: ["KeyError", ...INDEX_ERROR]
|
|
2480
|
+
},
|
|
2481
|
+
"ActionController::UnpermittedParameters": { ancestors: INDEX_ERROR },
|
|
2482
|
+
"ActionController::RoutingError": {
|
|
2483
|
+
status: 404,
|
|
2484
|
+
ancestors: ACTION_CONTROLLER_ERROR
|
|
2485
|
+
},
|
|
2486
|
+
"AbstractController::ActionNotFound": {
|
|
2487
|
+
status: 404,
|
|
2488
|
+
ancestors: STANDARD_ERROR
|
|
2489
|
+
},
|
|
2490
|
+
"ActionController::MethodNotAllowed": {
|
|
2491
|
+
status: 405,
|
|
2492
|
+
ancestors: ACTION_CONTROLLER_ERROR
|
|
2493
|
+
},
|
|
2494
|
+
"ActionController::NotImplemented": {
|
|
2495
|
+
status: 501,
|
|
2496
|
+
ancestors: [
|
|
2497
|
+
"ActionController::MethodNotAllowed",
|
|
2498
|
+
...ACTION_CONTROLLER_ERROR
|
|
2499
|
+
]
|
|
2500
|
+
},
|
|
2501
|
+
"ActionController::UnknownFormat": {
|
|
2502
|
+
status: 406,
|
|
2503
|
+
ancestors: ACTION_CONTROLLER_ERROR
|
|
2504
|
+
},
|
|
2505
|
+
"ActionController::InvalidAuthenticityToken": {
|
|
2506
|
+
status: 422,
|
|
2507
|
+
ancestors: ACTION_CONTROLLER_ERROR
|
|
2508
|
+
},
|
|
2509
|
+
"ActiveRecord::ActiveRecordError": { ancestors: STANDARD_ERROR },
|
|
2510
|
+
"ActiveRecord::RecordNotFound": {
|
|
2511
|
+
status: 404,
|
|
2512
|
+
ancestors: ACTIVE_RECORD_ERROR
|
|
2513
|
+
},
|
|
2514
|
+
"ActiveRecord::RecordInvalid": {
|
|
2515
|
+
status: 422,
|
|
2516
|
+
ancestors: ACTIVE_RECORD_ERROR
|
|
2517
|
+
},
|
|
2518
|
+
"ActiveRecord::RecordNotSaved": {
|
|
2519
|
+
status: 422,
|
|
2520
|
+
ancestors: ACTIVE_RECORD_ERROR
|
|
2521
|
+
},
|
|
2522
|
+
"ActiveRecord::StaleObjectError": {
|
|
2523
|
+
status: 409,
|
|
2524
|
+
ancestors: ACTIVE_RECORD_ERROR
|
|
2525
|
+
}
|
|
2526
|
+
};
|
|
2527
|
+
var optionsSchema = z15.object({
|
|
883
2528
|
/**
|
|
884
2529
|
* The directory a bare controller name is looked up under, `app` in a
|
|
885
2530
|
* scaffolded Rails app.
|
|
886
2531
|
*/
|
|
887
|
-
root:
|
|
2532
|
+
root: z15.string().optional(),
|
|
888
2533
|
/**
|
|
889
2534
|
* The CLI fills this in with the config file's directory, or with the
|
|
890
2535
|
* directory the run reads when there is no file. A relative `root` or
|
|
891
2536
|
* `routesFile` resolves against it. A config file does not set it.
|
|
892
2537
|
*/
|
|
893
|
-
configDirectory:
|
|
2538
|
+
configDirectory: z15.string().optional(),
|
|
894
2539
|
/**
|
|
895
2540
|
* Base classes beyond `ApplicationController` whose subclasses are
|
|
896
2541
|
* controllers. A config file may not set this; a dependency stub
|
|
897
2542
|
* fills it.
|
|
898
2543
|
*/
|
|
899
|
-
baseClassNames:
|
|
2544
|
+
baseClassNames: z15.array(z15.string()).optional(),
|
|
900
2545
|
/**
|
|
901
2546
|
* Controller methods a gem such as Devise adds, beyond the ones Rails
|
|
902
2547
|
* defines. They are left off an action's effects the same way Rails'
|
|
903
2548
|
* own are.
|
|
904
2549
|
*/
|
|
905
|
-
inheritedMethodNames:
|
|
2550
|
+
inheritedMethodNames: z15.array(z15.string()).optional(),
|
|
906
2551
|
/**
|
|
907
2552
|
* The routes file, `config/routes.rb` in a scaffolded app. A relative
|
|
908
2553
|
* path resolves against `configDirectory` when there is one.
|
|
909
2554
|
*/
|
|
910
|
-
routesFile:
|
|
2555
|
+
routesFile: z15.string().optional(),
|
|
911
2556
|
/**
|
|
912
2557
|
* Directories that each contain an engine, with its `Rails::Engine`
|
|
913
2558
|
* class under `lib/` and its routes in `config/routes.rb`. A `*` in a
|
|
914
2559
|
* segment matches any one directory, so `plugins/*` covers every
|
|
915
2560
|
* plugin. Relative paths resolve against `configDirectory`.
|
|
916
2561
|
*/
|
|
917
|
-
engineRoots:
|
|
2562
|
+
engineRoots: z15.array(z15.string()).optional(),
|
|
918
2563
|
/**
|
|
919
2564
|
* Other files that add routes through a `Rails.application.routes.draw`,
|
|
920
2565
|
* `.append` or `.prepend` block, most often a plugin's `plugin.rb`.
|
|
921
2566
|
* They take `*` the same way `engineRoots` does, and relative paths
|
|
922
2567
|
* resolve against `configDirectory`.
|
|
923
2568
|
*/
|
|
924
|
-
routesFiles:
|
|
2569
|
+
routesFiles: z15.array(z15.string()).optional()
|
|
925
2570
|
}).strict();
|
|
926
2571
|
var RAILS_ROOT_CLASS_NAMES = [
|
|
927
2572
|
"ActionController::Base",
|
|
928
2573
|
"ActionController::API"
|
|
929
2574
|
];
|
|
930
|
-
var RESTFUL_ACTIONS = {
|
|
931
|
-
index: { method: "GET", pathTemplate: "/:resource" },
|
|
932
|
-
show: { method: "GET", pathTemplate: "/:resource/:id" },
|
|
933
|
-
new: { method: "GET", pathTemplate: "/:resource/new" },
|
|
934
|
-
create: { method: "POST", pathTemplate: "/:resource" },
|
|
935
|
-
edit: { method: "GET", pathTemplate: "/:resource/:id/edit" },
|
|
936
|
-
update: { method: "PATCH", pathTemplate: "/:resource/:id" },
|
|
937
|
-
destroy: { method: "DELETE", pathTemplate: "/:resource/:id" }
|
|
938
|
-
};
|
|
939
2575
|
var RESPONSE_STATUS_CALLS = [
|
|
940
|
-
{
|
|
2576
|
+
{
|
|
2577
|
+
name: "render",
|
|
2578
|
+
statusKeyword: "status",
|
|
2579
|
+
bodyKeyword: "json",
|
|
2580
|
+
bodySerializers: ["to_json", "as_json"]
|
|
2581
|
+
},
|
|
941
2582
|
{ name: "head", statusArgument: 0, statusKeyword: "status" },
|
|
942
2583
|
{ name: "redirect_to", statusKeyword: "status", defaultStatusCode: 302 },
|
|
943
2584
|
{ name: "redirect_back", statusKeyword: "status", defaultStatusCode: 302 }
|
|
@@ -993,22 +2634,15 @@ function controllerKeyFromQualified(qualifiedName, acronyms) {
|
|
|
993
2634
|
const withoutSuffix = qualifiedName.endsWith("Controller") ? qualifiedName.slice(0, -"Controller".length) : qualifiedName;
|
|
994
2635
|
return underscoreConstantPath2(withoutSuffix, acronyms);
|
|
995
2636
|
}
|
|
996
|
-
function
|
|
997
|
-
const template = RESTFUL_ACTIONS[actionName];
|
|
998
|
-
if (template === void 0) {
|
|
999
|
-
return null;
|
|
1000
|
-
}
|
|
2637
|
+
function conventionalRoutes(controllerKey, actionName) {
|
|
1001
2638
|
const resource = controllerKey.split("/").pop() ?? controllerKey;
|
|
1002
|
-
return {
|
|
1003
|
-
method: template.method,
|
|
1004
|
-
path: template.pathTemplate.replace(":resource", resource)
|
|
1005
|
-
};
|
|
2639
|
+
return restfulRoutes(actionName, `/${resource}`);
|
|
1006
2640
|
}
|
|
1007
2641
|
function resolveAgainst(configDirectory, value) {
|
|
1008
|
-
if (configDirectory === void 0 ||
|
|
2642
|
+
if (configDirectory === void 0 || path5.isAbsolute(value)) {
|
|
1009
2643
|
return value;
|
|
1010
2644
|
}
|
|
1011
|
-
return
|
|
2645
|
+
return path5.resolve(configDirectory, value);
|
|
1012
2646
|
}
|
|
1013
2647
|
function railsFramework(options = {}) {
|
|
1014
2648
|
const root = resolveAgainst(options.configDirectory, options.root ?? "app");
|
|
@@ -1016,7 +2650,7 @@ function railsFramework(options = {}) {
|
|
|
1016
2650
|
options.configDirectory,
|
|
1017
2651
|
options.routesFile ?? "config/routes.rb"
|
|
1018
2652
|
);
|
|
1019
|
-
const displayPathOf = (file) => options.configDirectory === void 0 ? file :
|
|
2653
|
+
const displayPathOf = (file) => options.configDirectory === void 0 ? file : path5.relative(options.configDirectory, file);
|
|
1020
2654
|
const engineRoots = () => expandPatterns(options.configDirectory, options.engineRoots);
|
|
1021
2655
|
const extraRoutesFiles = () => expandPatterns(options.configDirectory, options.routesFiles);
|
|
1022
2656
|
const inflections = options.configDirectory === void 0 ? EMPTY_INFLECTIONS : readInflections(options.configDirectory);
|
|
@@ -1047,24 +2681,28 @@ function railsFramework(options = {}) {
|
|
|
1047
2681
|
...options.baseClassNames ?? []
|
|
1048
2682
|
],
|
|
1049
2683
|
root,
|
|
2684
|
+
autoloadRoots: options.configDirectory === void 0 ? [] : readAutoloadRoots(options.configDirectory),
|
|
1050
2685
|
pathConvention: "railsUnderscore",
|
|
1051
2686
|
acronyms,
|
|
1052
2687
|
ancestryRootClassNames: [...RAILS_ROOT_CLASS_NAMES],
|
|
1053
2688
|
defaultStatusCode: 200,
|
|
1054
2689
|
responseStatusCalls: RESPONSE_STATUS_CALLS,
|
|
1055
2690
|
statusCodeNames: RACK_STATUS_CODE_NAMES,
|
|
2691
|
+
formatSwitch: { name: "respond_to", matchesEvery: "any" },
|
|
2692
|
+
libraryExceptions: LIBRARY_EXCEPTIONS,
|
|
1056
2693
|
inheritedMethodNames: [
|
|
1057
2694
|
...RAILS_CONTROLLER_METHODS,
|
|
1058
2695
|
...options.inheritedMethodNames ?? []
|
|
1059
2696
|
],
|
|
1060
2697
|
routesFile,
|
|
1061
2698
|
filters: CONTROLLER_FILTERS,
|
|
1062
|
-
|
|
2699
|
+
routesFor: (controllerQualifiedName, actionName) => {
|
|
1063
2700
|
const key = controllerKeyFromQualified(controllerQualifiedName, acronyms);
|
|
1064
2701
|
const found = routeTable();
|
|
1065
|
-
return found.fileFound ? found.
|
|
2702
|
+
return found.fileFound ? found.routesFor(key, actionName) : conventionalRoutes(key, actionName);
|
|
1066
2703
|
},
|
|
1067
|
-
routingGaps: () => routeTable().gaps
|
|
2704
|
+
routingGaps: () => routeTable().gaps,
|
|
2705
|
+
optionalFormat: true
|
|
1068
2706
|
};
|
|
1069
2707
|
return {
|
|
1070
2708
|
name: "rails",
|
|
@@ -1089,7 +2727,10 @@ function railsFramework(options = {}) {
|
|
|
1089
2727
|
...drawableRoutesFiles(routesFile),
|
|
1090
2728
|
...engineRoots().flatMap(engineSourceFiles),
|
|
1091
2729
|
...extraRoutesFiles(),
|
|
1092
|
-
...options.configDirectory === void 0 ? [] :
|
|
2730
|
+
...options.configDirectory === void 0 ? [] : [
|
|
2731
|
+
...inflectionFiles(options.configDirectory),
|
|
2732
|
+
...applicationFile(options.configDirectory)
|
|
2733
|
+
]
|
|
1093
2734
|
]
|
|
1094
2735
|
};
|
|
1095
2736
|
}
|
|
@@ -1100,10 +2741,10 @@ function expandPatterns(configDirectory, patterns) {
|
|
|
1100
2741
|
}
|
|
1101
2742
|
function drawableRoutesFiles(routesFile) {
|
|
1102
2743
|
const directory = drawDirectoryOf(routesFile);
|
|
1103
|
-
if (!
|
|
2744
|
+
if (!fs5.existsSync(directory)) {
|
|
1104
2745
|
return [];
|
|
1105
2746
|
}
|
|
1106
|
-
return
|
|
2747
|
+
return fs5.readdirSync(directory).filter((name) => name.endsWith(".rb")).sort().map((name) => path5.join(directory, name));
|
|
1107
2748
|
}
|
|
1108
2749
|
var declares = {
|
|
1109
2750
|
kind: "framework",
|