@suss/packs 0.34.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/activerecord.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 +198 -69
- package/dist/rails.js +1752 -107
- 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,1448 @@ 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
|
+
/** The field on that side, for a finding about one field and no transition. */
|
|
1367
|
+
field: z22.string().optional()
|
|
1368
|
+
}).optional();
|
|
1369
|
+
var SuppressionRuleSchema = z22.object({
|
|
1370
|
+
/**
|
|
1371
|
+
* The finding kind to match, behavioral or intent. The schema accepts
|
|
1372
|
+
* any string because ir-core cannot import either IR's kind enum. The
|
|
1373
|
+
* CLI's loader checks it against both.
|
|
1374
|
+
*/
|
|
1375
|
+
kind: z22.string().optional(),
|
|
1376
|
+
/**
|
|
1377
|
+
* The boundary's human-readable key, such as "GET /pet/{petId}" or
|
|
1378
|
+
* "fn:@acme/api::getUser". A REST key goes through the same path
|
|
1379
|
+
* normalizer the checkers use, so `:id` and `{id}` compare equal.
|
|
1380
|
+
*/
|
|
1381
|
+
boundary: z22.string().optional(),
|
|
1382
|
+
/**
|
|
1383
|
+
* Consumer-side discriminators, the narrowest useful match. They
|
|
1384
|
+
* apply only to behavioral findings. A rule that sets `consumer`
|
|
1385
|
+
* never matches an intent finding, because an intent finding has no
|
|
1386
|
+
* consumer side.
|
|
1387
|
+
*/
|
|
1388
|
+
consumer: SuppressionSideSchema,
|
|
1389
|
+
/**
|
|
1390
|
+
* Provider-side discriminators, matching the same fields as
|
|
1391
|
+
* `consumer`. A finding about a status the provider produces has its
|
|
1392
|
+
* transition id on this side, and that id is the only value narrow
|
|
1393
|
+
* enough to pick out that one finding. As with `consumer`, a rule
|
|
1394
|
+
* that sets `provider` never matches an intent finding.
|
|
1395
|
+
*/
|
|
1396
|
+
provider: SuppressionSideSchema,
|
|
1397
|
+
/**
|
|
1398
|
+
* The title of one PRD scenario. A PRD finding's boundary is the
|
|
1399
|
+
* whole PRD, so without this a rule accepts every scenario in it,
|
|
1400
|
+
* including one added later. A rule that sets `scenario` matches
|
|
1401
|
+
* only intent findings about a scenario with that title.
|
|
1402
|
+
*/
|
|
1403
|
+
scenario: z22.string().optional(),
|
|
1404
|
+
/**
|
|
1405
|
+
* "narrow", the default, requires `kind` plus one of `boundary`,
|
|
1406
|
+
* `scenario`, `consumer.transitionId` or `provider.transitionId`, so
|
|
1407
|
+
* the rule targets a specific class of finding. "broad" allows a
|
|
1408
|
+
* match on kind alone or boundary alone, and such a rule also
|
|
1409
|
+
* silences future regressions in that category.
|
|
1410
|
+
*/
|
|
1411
|
+
scope: z22.enum(["narrow", "broad"]).default("narrow"),
|
|
1412
|
+
/** Required human-written justification. */
|
|
1413
|
+
reason: z22.string().min(1),
|
|
1414
|
+
/** What to do when a finding matches. */
|
|
1415
|
+
effect: z22.enum(["mark", "downgrade", "hide"]).default("mark")
|
|
1416
|
+
}).strict();
|
|
1417
|
+
var SuppressionFileSchema = z22.object({
|
|
1418
|
+
version: z22.literal(1),
|
|
1419
|
+
rules: z22.array(SuppressionRuleSchema)
|
|
1420
|
+
}).strict();
|
|
1421
|
+
|
|
1422
|
+
// ../framework/rails/dist/index.js
|
|
1423
|
+
var ADDED_PATHS = /config\.(?:eager_load_paths|autoload_paths|autoload_once_paths)\s*(?:<<|\+=|\.push\(|\.unshift\(|\.concat\()\s*([^\n]*)/g;
|
|
1424
|
+
var ROOT_JOIN = /Rails\.root\.join\(\s*([^)]*)\)/g;
|
|
1425
|
+
var INTERPOLATED_ROOT = /#\{(?:Rails\.root|config\.root|root)\}\/([\w./-]+)/g;
|
|
1426
|
+
var AUTOLOAD_LIB = /config\.autoload_lib(?:_once)?\b/;
|
|
1427
|
+
var EVERY_LITERAL = /(["'])([^"']*)\1/g;
|
|
1428
|
+
function applicationFile(configDirectory) {
|
|
1429
|
+
const file = applicationPath(configDirectory);
|
|
1430
|
+
return fs.existsSync(file) ? [file] : [];
|
|
1431
|
+
}
|
|
1432
|
+
function applicationPath(configDirectory) {
|
|
1433
|
+
return path.join(configDirectory, "config", "application.rb");
|
|
1434
|
+
}
|
|
1435
|
+
function readAutoloadRoots(configDirectory) {
|
|
1436
|
+
const file = applicationPath(configDirectory);
|
|
1437
|
+
let source;
|
|
1438
|
+
try {
|
|
1439
|
+
source = fs.readFileSync(file, "utf8");
|
|
1440
|
+
} catch {
|
|
1441
|
+
return [];
|
|
1442
|
+
}
|
|
1443
|
+
const relative = [];
|
|
1444
|
+
for (const line of source.split("\n")) {
|
|
1445
|
+
if (line.trimStart().startsWith("#")) {
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
1448
|
+
if (AUTOLOAD_LIB.test(line)) {
|
|
1449
|
+
relative.push("lib");
|
|
1450
|
+
}
|
|
1451
|
+
for (const added of line.matchAll(ADDED_PATHS)) {
|
|
1452
|
+
relative.push(...directoriesIn(added[1] ?? ""));
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
return [...new Set(relative)].map((directory) => path.resolve(configDirectory, directory)).filter((directory) => isDirectory(directory));
|
|
1456
|
+
}
|
|
1457
|
+
function directoriesIn(fragment) {
|
|
1458
|
+
const joined = [...fragment.matchAll(ROOT_JOIN)].map(
|
|
1459
|
+
(join) => [...(join[1] ?? "").matchAll(EVERY_LITERAL)].map((literal) => literal[2] ?? "").join("/")
|
|
1460
|
+
);
|
|
1461
|
+
const interpolated = [...fragment.matchAll(INTERPOLATED_ROOT)].map(
|
|
1462
|
+
(match) => match[1] ?? ""
|
|
1463
|
+
);
|
|
1464
|
+
return [...joined, ...interpolated].filter((one) => one !== "");
|
|
1465
|
+
}
|
|
1466
|
+
function isDirectory(directory) {
|
|
1467
|
+
try {
|
|
1468
|
+
return fs.statSync(directory).isDirectory();
|
|
1469
|
+
} catch {
|
|
1470
|
+
return false;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
27
1473
|
var ENGINE_BASE_NAMES = /* @__PURE__ */ new Set(["Rails::Engine", "::Rails::Engine"]);
|
|
28
1474
|
var CONSTANT_TYPES = /* @__PURE__ */ new Set(["constant", "scope_resolution"]);
|
|
29
1475
|
function readEngines(roots, acronyms = []) {
|
|
@@ -31,7 +1477,7 @@ function readEngines(roots, acronyms = []) {
|
|
|
31
1477
|
for (const root of roots) {
|
|
32
1478
|
const routesFile = engineRoutesFile(root);
|
|
33
1479
|
for (const file of engineFilesUnder(root)) {
|
|
34
|
-
const source =
|
|
1480
|
+
const source = fs2.readFileSync(file, "utf8");
|
|
35
1481
|
const rootNode = parseRubySync(source).rootNode;
|
|
36
1482
|
for (const found of engineClassesIn(rootNode, acronyms)) {
|
|
37
1483
|
engines.push({ ...found, routesFile });
|
|
@@ -48,48 +1494,45 @@ function engineSourceFiles(root) {
|
|
|
48
1494
|
];
|
|
49
1495
|
}
|
|
50
1496
|
function engineRoutesFile(root) {
|
|
51
|
-
const routesFile =
|
|
52
|
-
return
|
|
1497
|
+
const routesFile = path2.join(root, "config", "routes.rb");
|
|
1498
|
+
return fs2.existsSync(routesFile) ? routesFile : null;
|
|
53
1499
|
}
|
|
54
1500
|
function engineFilesUnder(root) {
|
|
55
|
-
const directory =
|
|
56
|
-
if (!
|
|
1501
|
+
const directory = path2.join(root, "lib");
|
|
1502
|
+
if (!fs2.existsSync(directory)) {
|
|
57
1503
|
return [];
|
|
58
1504
|
}
|
|
59
1505
|
return rubyFilesUnder(directory).filter(
|
|
60
|
-
(file) =>
|
|
1506
|
+
(file) => fs2.readFileSync(file, "utf8").includes("Rails::Engine")
|
|
61
1507
|
);
|
|
62
1508
|
}
|
|
63
1509
|
function expandPathPattern(pattern) {
|
|
64
|
-
const segments = pattern.split(
|
|
65
|
-
const start =
|
|
1510
|
+
const segments = pattern.split(path2.sep).filter((s) => s !== "");
|
|
1511
|
+
const start = path2.isAbsolute(pattern) ? path2.sep : ".";
|
|
66
1512
|
let candidates = [start];
|
|
67
1513
|
for (const segment of segments) {
|
|
68
1514
|
if (!segment.includes("*")) {
|
|
69
|
-
candidates = candidates.map((base) =>
|
|
1515
|
+
candidates = candidates.map((base) => path2.join(base, segment));
|
|
70
1516
|
continue;
|
|
71
1517
|
}
|
|
72
1518
|
const matcher = new RegExp(
|
|
73
1519
|
`^${segment.split("*").map(escapeRegExp).join(".*")}$`
|
|
74
1520
|
);
|
|
75
1521
|
candidates = candidates.flatMap(
|
|
76
|
-
(base) =>
|
|
1522
|
+
(base) => fs2.existsSync(base) ? fs2.readdirSync(base).filter((name) => matcher.test(name)).sort().map((name) => path2.join(base, name)) : []
|
|
77
1523
|
);
|
|
78
1524
|
}
|
|
79
|
-
return candidates.filter((candidate) =>
|
|
1525
|
+
return candidates.filter((candidate) => fs2.existsSync(candidate));
|
|
80
1526
|
}
|
|
81
1527
|
function escapeRegExp(text) {
|
|
82
1528
|
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
83
1529
|
}
|
|
84
1530
|
function rubyFilesUnder(directory) {
|
|
85
|
-
return
|
|
1531
|
+
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
1532
|
}
|
|
87
1533
|
function engineClassesIn(node, acronyms, enclosing = []) {
|
|
88
1534
|
const found = [];
|
|
89
|
-
for (const child of node
|
|
90
|
-
if (child === null) {
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
1535
|
+
for (const child of children(node)) {
|
|
93
1536
|
if (child.type === "class" && extendsEngine(child)) {
|
|
94
1537
|
const name = definedName(child);
|
|
95
1538
|
if (name !== void 0) {
|
|
@@ -110,10 +1553,10 @@ function definedName(definition) {
|
|
|
110
1553
|
}
|
|
111
1554
|
function extendsEngine(classNode) {
|
|
112
1555
|
const superclass = field(classNode, "superclass");
|
|
113
|
-
const base = superclass
|
|
114
|
-
(child) =>
|
|
1556
|
+
const base = children(superclass).find(
|
|
1557
|
+
(child) => CONSTANT_TYPES.has(child.type)
|
|
115
1558
|
);
|
|
116
|
-
return base !== void 0 &&
|
|
1559
|
+
return base !== void 0 && ENGINE_BASE_NAMES.has(base.text);
|
|
117
1560
|
}
|
|
118
1561
|
function isolatedNamespaceOf(classNode, acronyms) {
|
|
119
1562
|
const body = field(classNode, "body");
|
|
@@ -137,7 +1580,7 @@ var IRREGULAR_CALL = /\.irregular\s*\(?\s*(["'])([^"']*)\1\s*,\s*(["'])([^"']*)\
|
|
|
137
1580
|
var UNCOUNTABLE_CALL = /\.uncountable\s*\(?\s*([^\n]*)/g;
|
|
138
1581
|
var SINGULAR_CALL = /\.singular\s*\(?\s*(?:(["'])([^"']*)\1|\/([^/]*)\/([a-z]*))\s*,\s*(["'])([^"']*)\5/g;
|
|
139
1582
|
var WORD_LIST = /%[wi][([{]([^)\]}]*)[)\]}]/;
|
|
140
|
-
var
|
|
1583
|
+
var EVERY_LITERAL2 = /(["'])([^"']*)\1/g;
|
|
141
1584
|
var ANY_CALL = /\.(acronym|irregular|uncountable|singular)[\s("'/]/;
|
|
142
1585
|
function group(match, at) {
|
|
143
1586
|
return match[at] ?? "";
|
|
@@ -148,7 +1591,7 @@ function readInflections(configDirectory) {
|
|
|
148
1591
|
const uncountable = /* @__PURE__ */ new Set();
|
|
149
1592
|
const singular = [];
|
|
150
1593
|
for (const file of inflectionFiles(configDirectory)) {
|
|
151
|
-
const source = uncommented(
|
|
1594
|
+
const source = uncommented(fs3.readFileSync(file, "utf8"));
|
|
152
1595
|
for (const match of source.matchAll(ACRONYM_CALL)) {
|
|
153
1596
|
acronyms.add(group(match, 2));
|
|
154
1597
|
}
|
|
@@ -182,18 +1625,130 @@ function wordsIn(argument) {
|
|
|
182
1625
|
if (list !== null) {
|
|
183
1626
|
return group(list, 1).split(/\s+/).filter((word) => word.length > 0);
|
|
184
1627
|
}
|
|
185
|
-
return [...argument.matchAll(
|
|
1628
|
+
return [...argument.matchAll(EVERY_LITERAL2)].map((match) => group(match, 2));
|
|
186
1629
|
}
|
|
187
1630
|
function inflectionFiles(configDirectory) {
|
|
188
|
-
const directory =
|
|
189
|
-
if (!
|
|
1631
|
+
const directory = path3.join(configDirectory, "config", "initializers");
|
|
1632
|
+
if (!fs3.existsSync(directory)) {
|
|
190
1633
|
return [];
|
|
191
1634
|
}
|
|
192
|
-
return
|
|
1635
|
+
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
1636
|
}
|
|
194
1637
|
function uncommented(source) {
|
|
195
1638
|
return source.split("\n").filter((line) => !line.trimStart().startsWith("#")).join("\n");
|
|
196
1639
|
}
|
|
1640
|
+
var EXPANSION_CAP = 64;
|
|
1641
|
+
function closingParenthesis2(text, open) {
|
|
1642
|
+
let depth = 0;
|
|
1643
|
+
for (let index = open; index < text.length; index += 1) {
|
|
1644
|
+
const char = text[index];
|
|
1645
|
+
if (char === "(") {
|
|
1646
|
+
depth += 1;
|
|
1647
|
+
}
|
|
1648
|
+
if (char === ")") {
|
|
1649
|
+
depth -= 1;
|
|
1650
|
+
if (depth === 0) {
|
|
1651
|
+
return index;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
return -1;
|
|
1656
|
+
}
|
|
1657
|
+
function expansions(text) {
|
|
1658
|
+
let found = [""];
|
|
1659
|
+
let index = 0;
|
|
1660
|
+
while (index < text.length) {
|
|
1661
|
+
const open = text.indexOf("(", index);
|
|
1662
|
+
const close = text.indexOf(")", index);
|
|
1663
|
+
if (close !== -1 && (open === -1 || close < open)) {
|
|
1664
|
+
return null;
|
|
1665
|
+
}
|
|
1666
|
+
if (open === -1) {
|
|
1667
|
+
const rest = text.slice(index);
|
|
1668
|
+
found = found.map((prefix) => prefix + rest);
|
|
1669
|
+
break;
|
|
1670
|
+
}
|
|
1671
|
+
const end = closingParenthesis2(text, open);
|
|
1672
|
+
const inner = end === -1 ? null : expansions(text.slice(open + 1, end));
|
|
1673
|
+
if (inner === null) {
|
|
1674
|
+
return null;
|
|
1675
|
+
}
|
|
1676
|
+
const before = text.slice(index, open);
|
|
1677
|
+
const options = ["", ...inner];
|
|
1678
|
+
found = found.flatMap(
|
|
1679
|
+
(prefix) => options.map((option) => prefix + before + option)
|
|
1680
|
+
);
|
|
1681
|
+
if (found.length > EXPANSION_CAP) {
|
|
1682
|
+
found = found.slice(0, 1);
|
|
1683
|
+
}
|
|
1684
|
+
index = end + 1;
|
|
1685
|
+
}
|
|
1686
|
+
return found;
|
|
1687
|
+
}
|
|
1688
|
+
function tidied(path6) {
|
|
1689
|
+
const squeezed = `/${path6}`.replace(/\/{2,}/g, "/");
|
|
1690
|
+
return squeezed.length > 1 ? squeezed.replace(/\/$/, "") : squeezed;
|
|
1691
|
+
}
|
|
1692
|
+
function segmentsOf2(path6) {
|
|
1693
|
+
return path6 === "/" ? [] : path6.slice(1).split("/");
|
|
1694
|
+
}
|
|
1695
|
+
function sharedCount(lists, limit, fromEnd) {
|
|
1696
|
+
const first = lists[0] ?? [];
|
|
1697
|
+
const at = (list, offset) => fromEnd ? list[list.length - 1 - offset] : list[offset];
|
|
1698
|
+
let count = 0;
|
|
1699
|
+
while (count < limit && lists.every((list) => at(list, count) === at(first, count))) {
|
|
1700
|
+
count += 1;
|
|
1701
|
+
}
|
|
1702
|
+
return count;
|
|
1703
|
+
}
|
|
1704
|
+
var ENDS_IN_PARAMETER = /:[A-Za-z_]\w*$/;
|
|
1705
|
+
function headBeforeSet(lists, shortest) {
|
|
1706
|
+
const first = lists[0] ?? [];
|
|
1707
|
+
let count = sharedCount(lists, shortest, false);
|
|
1708
|
+
while (count > 0 && ENDS_IN_PARAMETER.test(first[count - 1] ?? "")) {
|
|
1709
|
+
count -= 1;
|
|
1710
|
+
}
|
|
1711
|
+
return count;
|
|
1712
|
+
}
|
|
1713
|
+
function asOnePath(paths) {
|
|
1714
|
+
const lists = paths.map(segmentsOf2);
|
|
1715
|
+
const shortest = Math.min(...lists.map((list) => list.length));
|
|
1716
|
+
const headCount = headBeforeSet(lists, shortest);
|
|
1717
|
+
const tailCount = sharedCount(lists, shortest - headCount, true);
|
|
1718
|
+
const first = lists[0] ?? [];
|
|
1719
|
+
const head = first.slice(0, headCount).join("/");
|
|
1720
|
+
const tail = first.slice(first.length - tailCount).join("/");
|
|
1721
|
+
const middles = lists.map(
|
|
1722
|
+
(list) => list.slice(headCount, list.length - tailCount).join("/")
|
|
1723
|
+
);
|
|
1724
|
+
if (head !== "") {
|
|
1725
|
+
const set2 = setPiece(middles.map((middle) => middle ? `/${middle}` : ""));
|
|
1726
|
+
return set2 === null ? null : `/${head}${set2}${tail ? `/${tail}` : ""}`;
|
|
1727
|
+
}
|
|
1728
|
+
const set = setPiece(
|
|
1729
|
+
middles.map((middle) => middle && tail ? `${middle}/` : middle)
|
|
1730
|
+
);
|
|
1731
|
+
return set === null ? null : `/${set}${tail}`;
|
|
1732
|
+
}
|
|
1733
|
+
function withGlobsAsHoles(path6) {
|
|
1734
|
+
return path6.replace(/(^|[/(])\*([A-Za-z_]\w*)/g, "$1:$2+");
|
|
1735
|
+
}
|
|
1736
|
+
function pathWithOptionalGroups(written) {
|
|
1737
|
+
const path6 = withGlobsAsHoles(written);
|
|
1738
|
+
if (!path6.includes("(")) {
|
|
1739
|
+
return path6;
|
|
1740
|
+
}
|
|
1741
|
+
const expanded = expansions(path6);
|
|
1742
|
+
if (expanded === null) {
|
|
1743
|
+
return path6;
|
|
1744
|
+
}
|
|
1745
|
+
const paths = [...new Set(expanded.map(tidied))];
|
|
1746
|
+
const withoutGroups = paths[0] ?? path6;
|
|
1747
|
+
if (paths.length === 1) {
|
|
1748
|
+
return withoutGroups;
|
|
1749
|
+
}
|
|
1750
|
+
return asOnePath(paths) ?? withoutGroups;
|
|
1751
|
+
}
|
|
197
1752
|
function readSimpleArgs(call, defaults = {}) {
|
|
198
1753
|
const argumentList = field2(call, "arguments");
|
|
199
1754
|
const positional = [];
|
|
@@ -261,23 +1816,33 @@ function readSymbolList(node) {
|
|
|
261
1816
|
const single = symbolValue(node);
|
|
262
1817
|
return single === null ? null : [single];
|
|
263
1818
|
}
|
|
1819
|
+
var updateAt = (path6) => [
|
|
1820
|
+
{ method: "PATCH", path: path6 },
|
|
1821
|
+
{ method: "PUT", path: path6 }
|
|
1822
|
+
];
|
|
264
1823
|
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) =>
|
|
1824
|
+
index: (base) => [{ method: "GET", path: base }],
|
|
1825
|
+
create: (base) => [{ method: "POST", path: base }],
|
|
1826
|
+
new: (base) => [{ method: "GET", path: `${base}/new` }],
|
|
1827
|
+
show: (base) => [{ method: "GET", path: `${base}/:id` }],
|
|
1828
|
+
edit: (base) => [{ method: "GET", path: `${base}/:id/edit` }],
|
|
1829
|
+
update: (base) => updateAt(`${base}/:id`),
|
|
1830
|
+
destroy: (base) => [{ method: "DELETE", path: `${base}/:id` }]
|
|
272
1831
|
};
|
|
273
1832
|
var RESTFUL_ROUTES_SINGULAR = {
|
|
274
|
-
create: (base) =>
|
|
275
|
-
new: (base) =>
|
|
276
|
-
show: (base) =>
|
|
277
|
-
edit: (base) =>
|
|
278
|
-
update: (base) => (
|
|
279
|
-
destroy: (base) =>
|
|
1833
|
+
create: (base) => [{ method: "POST", path: base }],
|
|
1834
|
+
new: (base) => [{ method: "GET", path: `${base}/new` }],
|
|
1835
|
+
show: (base) => [{ method: "GET", path: base }],
|
|
1836
|
+
edit: (base) => [{ method: "GET", path: `${base}/edit` }],
|
|
1837
|
+
update: (base) => updateAt(base),
|
|
1838
|
+
destroy: (base) => [{ method: "DELETE", path: base }]
|
|
280
1839
|
};
|
|
1840
|
+
function restfulRoutes(action, base) {
|
|
1841
|
+
if (!Object.hasOwn(RESTFUL_ROUTES_PLURAL, action)) {
|
|
1842
|
+
return [];
|
|
1843
|
+
}
|
|
1844
|
+
return RESTFUL_ROUTES_PLURAL[action]?.(base) ?? [];
|
|
1845
|
+
}
|
|
281
1846
|
function pluralize(word) {
|
|
282
1847
|
if (/[^aeiou]y$/.test(word)) {
|
|
283
1848
|
return `${word.slice(0, -1)}ies`;
|
|
@@ -317,12 +1882,19 @@ var RouteAccumulator = class {
|
|
|
317
1882
|
mounting = /* @__PURE__ */ new Set();
|
|
318
1883
|
/** The file whose statements are being walked, as a gap message prints it. */
|
|
319
1884
|
file = "";
|
|
320
|
-
/**
|
|
1885
|
+
/** An action routed from several places serves every one of them, so each distinct route is kept. */
|
|
321
1886
|
add(controllerKey, action, route) {
|
|
322
1887
|
const key = `${controllerKey}#${action}`;
|
|
323
|
-
|
|
324
|
-
|
|
1888
|
+
const routes = this.byKey.get(key) ?? [];
|
|
1889
|
+
const path6 = pathWithOptionalGroups(route.path);
|
|
1890
|
+
if (!routes.some((one) => one.method === route.method && one.path === path6)) {
|
|
1891
|
+
routes.push({
|
|
1892
|
+
method: route.method,
|
|
1893
|
+
path: path6,
|
|
1894
|
+
...route.formatless === true ? { formatless: true } : {}
|
|
1895
|
+
});
|
|
325
1896
|
}
|
|
1897
|
+
this.byKey.set(key, routes);
|
|
326
1898
|
}
|
|
327
1899
|
recordUnread(callName) {
|
|
328
1900
|
addUnderFile(this.unread, this.file, callName);
|
|
@@ -333,8 +1905,8 @@ var RouteAccumulator = class {
|
|
|
333
1905
|
recordMissingDrawn(name) {
|
|
334
1906
|
this.missingDrawn.push({ file: this.file, name });
|
|
335
1907
|
}
|
|
336
|
-
|
|
337
|
-
return this.byKey.get(`${controllerKey}#${action}`) ??
|
|
1908
|
+
routesFor(controllerKey, action) {
|
|
1909
|
+
return this.byKey.get(`${controllerKey}#${action}`) ?? [];
|
|
338
1910
|
}
|
|
339
1911
|
/** Runs `walk` with gaps attributed to `file`, then goes back to the file being walked before. */
|
|
340
1912
|
inFile(file, walk) {
|
|
@@ -359,7 +1931,7 @@ var RouteAccumulator = class {
|
|
|
359
1931
|
}
|
|
360
1932
|
for (const { file, name } of this.missingDrawn) {
|
|
361
1933
|
gaps.push(
|
|
362
|
-
`${file} draws ${name}, but there is no ${name}.rb under ${
|
|
1934
|
+
`${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
1935
|
);
|
|
364
1936
|
}
|
|
365
1937
|
return gaps;
|
|
@@ -396,14 +1968,16 @@ function handleResourceCall(call, ctx, out, plural) {
|
|
|
396
1968
|
const only = readSymbolList(args.keyword.only);
|
|
397
1969
|
const except = readSymbolList(args.keyword.except);
|
|
398
1970
|
const table = plural ? RESTFUL_ROUTES_PLURAL : RESTFUL_ROUTES_SINGULAR;
|
|
399
|
-
for (const [action,
|
|
1971
|
+
for (const [action, routesAt] of Object.entries(table)) {
|
|
400
1972
|
if (only !== null && !only.includes(action)) {
|
|
401
1973
|
continue;
|
|
402
1974
|
}
|
|
403
1975
|
if (except?.includes(action)) {
|
|
404
1976
|
continue;
|
|
405
1977
|
}
|
|
406
|
-
|
|
1978
|
+
for (const route of routesAt(base)) {
|
|
1979
|
+
out.add(controllerKey, action, route);
|
|
1980
|
+
}
|
|
407
1981
|
}
|
|
408
1982
|
const nestedBase = plural ? `${base}/:${singularize(symbol)}_id` : base;
|
|
409
1983
|
const nested = {
|
|
@@ -447,13 +2021,15 @@ function handleVerb(call, ctx, out, method) {
|
|
|
447
2021
|
const on = wordValue(args.keyword.on, ctx);
|
|
448
2022
|
const base = ctx.resource === void 0 ? ctx.pathPrefix : baseForOn(ctx.resource, on);
|
|
449
2023
|
const target = readRouteTarget(args, ctx);
|
|
2024
|
+
const format = formatOf(args);
|
|
450
2025
|
if (target !== null) {
|
|
451
2026
|
const literalPath = args.positional[0] ? textValue(args.positional[0], ctx) : null;
|
|
452
|
-
const
|
|
453
|
-
if (
|
|
2027
|
+
const path6 = target.path ?? literalPath;
|
|
2028
|
+
if (path6 !== null) {
|
|
454
2029
|
out.add(joinKey(ctx.modulePrefix, target.controllerKey), target.action, {
|
|
455
2030
|
method,
|
|
456
|
-
path: joinPath(base,
|
|
2031
|
+
path: joinPath(base, path6),
|
|
2032
|
+
...format
|
|
457
2033
|
});
|
|
458
2034
|
}
|
|
459
2035
|
return;
|
|
@@ -468,7 +2044,14 @@ function handleVerb(call, ctx, out, method) {
|
|
|
468
2044
|
if (controllerKey === void 0) {
|
|
469
2045
|
return;
|
|
470
2046
|
}
|
|
471
|
-
out.add(controllerKey, action, {
|
|
2047
|
+
out.add(controllerKey, action, {
|
|
2048
|
+
method,
|
|
2049
|
+
path: joinPath(base, segment),
|
|
2050
|
+
...format
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
function formatOf(args) {
|
|
2054
|
+
return args.keyword.format?.type === "false" ? { formatless: true } : {};
|
|
472
2055
|
}
|
|
473
2056
|
function handleRoot(call, ctx, out) {
|
|
474
2057
|
const args = readSimpleArgs(call, ctx.defaults);
|
|
@@ -598,9 +2181,9 @@ function handleMatch(call, ctx, out) {
|
|
|
598
2181
|
}
|
|
599
2182
|
}
|
|
600
2183
|
function drawDirectoryOf(routesFile) {
|
|
601
|
-
return
|
|
602
|
-
|
|
603
|
-
|
|
2184
|
+
return path4.join(
|
|
2185
|
+
path4.dirname(routesFile),
|
|
2186
|
+
path4.basename(routesFile, path4.extname(routesFile))
|
|
604
2187
|
);
|
|
605
2188
|
}
|
|
606
2189
|
function handleDraw(call, ctx, out) {
|
|
@@ -609,16 +2192,16 @@ function handleDraw(call, ctx, out) {
|
|
|
609
2192
|
if (name === null) {
|
|
610
2193
|
return;
|
|
611
2194
|
}
|
|
612
|
-
const file =
|
|
2195
|
+
const file = path4.join(out.drawDirectory, `${name}.rb`);
|
|
613
2196
|
if (out.drawn.has(file)) {
|
|
614
2197
|
return;
|
|
615
2198
|
}
|
|
616
2199
|
out.drawn.add(file);
|
|
617
|
-
if (!
|
|
2200
|
+
if (!fs4.existsSync(file)) {
|
|
618
2201
|
out.recordMissingDrawn(name);
|
|
619
2202
|
return;
|
|
620
2203
|
}
|
|
621
|
-
const tree = parseRubySync2(
|
|
2204
|
+
const tree = parseRubySync2(fs4.readFileSync(file, "utf8"));
|
|
622
2205
|
walkBody(tree.rootNode, ctx, out);
|
|
623
2206
|
}
|
|
624
2207
|
var HANDLERS = {
|
|
@@ -726,10 +2309,8 @@ function routeDeclarations(root, displayPath) {
|
|
|
726
2309
|
found.push(declaration);
|
|
727
2310
|
return;
|
|
728
2311
|
}
|
|
729
|
-
for (const child of node
|
|
730
|
-
|
|
731
|
-
visit(child);
|
|
732
|
-
}
|
|
2312
|
+
for (const child of children2(node)) {
|
|
2313
|
+
visit(child);
|
|
733
2314
|
}
|
|
734
2315
|
};
|
|
735
2316
|
visit(root);
|
|
@@ -756,16 +2337,16 @@ function isApplication(owner) {
|
|
|
756
2337
|
return owner === "Rails.application" || /(^|::)Application$/.test(owner);
|
|
757
2338
|
}
|
|
758
2339
|
function parseDeclarations(source) {
|
|
759
|
-
if (!
|
|
2340
|
+
if (!fs4.existsSync(source.file)) {
|
|
760
2341
|
return [];
|
|
761
2342
|
}
|
|
762
|
-
const tree = parseRubySync2(
|
|
2343
|
+
const tree = parseRubySync2(fs4.readFileSync(source.file, "utf8"));
|
|
763
2344
|
return routeDeclarations(tree.rootNode, source.displayPath);
|
|
764
2345
|
}
|
|
765
2346
|
function readRoutes(input) {
|
|
766
|
-
if (!
|
|
2347
|
+
if (!fs4.existsSync(input.routesFile.file)) {
|
|
767
2348
|
return {
|
|
768
|
-
|
|
2349
|
+
routesFor: () => [],
|
|
769
2350
|
fileFound: false,
|
|
770
2351
|
gaps: [
|
|
771
2352
|
`${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 +2387,7 @@ function readRoutes(input) {
|
|
|
806
2387
|
});
|
|
807
2388
|
}
|
|
808
2389
|
return {
|
|
809
|
-
|
|
2390
|
+
routesFor: (controllerKey, action) => out.routesFor(controllerKey, action),
|
|
810
2391
|
fileFound: true,
|
|
811
2392
|
gaps: out.gaps()
|
|
812
2393
|
};
|
|
@@ -876,68 +2457,131 @@ var RACK_STATUS_CODE_NAMES = {
|
|
|
876
2457
|
variant_also_negotiates: 506,
|
|
877
2458
|
insufficient_storage: 507,
|
|
878
2459
|
loop_detected: 508,
|
|
2460
|
+
bandwidth_limit_exceeded: 509,
|
|
879
2461
|
not_extended: 510,
|
|
880
2462
|
network_authentication_required: 511
|
|
881
2463
|
};
|
|
882
|
-
var
|
|
2464
|
+
var STANDARD_ERROR = ["StandardError", "Exception"];
|
|
2465
|
+
var ACTION_CONTROLLER_ERROR = [
|
|
2466
|
+
"ActionController::ActionControllerError",
|
|
2467
|
+
...STANDARD_ERROR
|
|
2468
|
+
];
|
|
2469
|
+
var ACTIVE_RECORD_ERROR = [
|
|
2470
|
+
"ActiveRecord::ActiveRecordError",
|
|
2471
|
+
...STANDARD_ERROR
|
|
2472
|
+
];
|
|
2473
|
+
var INDEX_ERROR = ["IndexError", ...STANDARD_ERROR];
|
|
2474
|
+
var LIBRARY_EXCEPTIONS = {
|
|
2475
|
+
"ActionController::ActionControllerError": { ancestors: STANDARD_ERROR },
|
|
2476
|
+
"ActionController::BadRequest": {
|
|
2477
|
+
status: 400,
|
|
2478
|
+
ancestors: ACTION_CONTROLLER_ERROR
|
|
2479
|
+
},
|
|
2480
|
+
"ActionController::ParameterMissing": {
|
|
2481
|
+
status: 400,
|
|
2482
|
+
ancestors: ["KeyError", ...INDEX_ERROR]
|
|
2483
|
+
},
|
|
2484
|
+
"ActionController::UnpermittedParameters": { ancestors: INDEX_ERROR },
|
|
2485
|
+
"ActionController::RoutingError": {
|
|
2486
|
+
status: 404,
|
|
2487
|
+
ancestors: ACTION_CONTROLLER_ERROR
|
|
2488
|
+
},
|
|
2489
|
+
"AbstractController::ActionNotFound": {
|
|
2490
|
+
status: 404,
|
|
2491
|
+
ancestors: STANDARD_ERROR
|
|
2492
|
+
},
|
|
2493
|
+
"ActionController::MethodNotAllowed": {
|
|
2494
|
+
status: 405,
|
|
2495
|
+
ancestors: ACTION_CONTROLLER_ERROR
|
|
2496
|
+
},
|
|
2497
|
+
"ActionController::NotImplemented": {
|
|
2498
|
+
status: 501,
|
|
2499
|
+
ancestors: [
|
|
2500
|
+
"ActionController::MethodNotAllowed",
|
|
2501
|
+
...ACTION_CONTROLLER_ERROR
|
|
2502
|
+
]
|
|
2503
|
+
},
|
|
2504
|
+
"ActionController::UnknownFormat": {
|
|
2505
|
+
status: 406,
|
|
2506
|
+
ancestors: ACTION_CONTROLLER_ERROR
|
|
2507
|
+
},
|
|
2508
|
+
"ActionController::InvalidAuthenticityToken": {
|
|
2509
|
+
status: 422,
|
|
2510
|
+
ancestors: ACTION_CONTROLLER_ERROR
|
|
2511
|
+
},
|
|
2512
|
+
"ActiveRecord::ActiveRecordError": { ancestors: STANDARD_ERROR },
|
|
2513
|
+
"ActiveRecord::RecordNotFound": {
|
|
2514
|
+
status: 404,
|
|
2515
|
+
ancestors: ACTIVE_RECORD_ERROR
|
|
2516
|
+
},
|
|
2517
|
+
"ActiveRecord::RecordInvalid": {
|
|
2518
|
+
status: 422,
|
|
2519
|
+
ancestors: ACTIVE_RECORD_ERROR
|
|
2520
|
+
},
|
|
2521
|
+
"ActiveRecord::RecordNotSaved": {
|
|
2522
|
+
status: 422,
|
|
2523
|
+
ancestors: ACTIVE_RECORD_ERROR
|
|
2524
|
+
},
|
|
2525
|
+
"ActiveRecord::StaleObjectError": {
|
|
2526
|
+
status: 409,
|
|
2527
|
+
ancestors: ACTIVE_RECORD_ERROR
|
|
2528
|
+
}
|
|
2529
|
+
};
|
|
2530
|
+
var optionsSchema = z15.object({
|
|
883
2531
|
/**
|
|
884
2532
|
* The directory a bare controller name is looked up under, `app` in a
|
|
885
2533
|
* scaffolded Rails app.
|
|
886
2534
|
*/
|
|
887
|
-
root:
|
|
2535
|
+
root: z15.string().optional(),
|
|
888
2536
|
/**
|
|
889
2537
|
* The CLI fills this in with the config file's directory, or with the
|
|
890
2538
|
* directory the run reads when there is no file. A relative `root` or
|
|
891
2539
|
* `routesFile` resolves against it. A config file does not set it.
|
|
892
2540
|
*/
|
|
893
|
-
configDirectory:
|
|
2541
|
+
configDirectory: z15.string().optional(),
|
|
894
2542
|
/**
|
|
895
2543
|
* Base classes beyond `ApplicationController` whose subclasses are
|
|
896
2544
|
* controllers. A config file may not set this; a dependency stub
|
|
897
2545
|
* fills it.
|
|
898
2546
|
*/
|
|
899
|
-
baseClassNames:
|
|
2547
|
+
baseClassNames: z15.array(z15.string()).optional(),
|
|
900
2548
|
/**
|
|
901
2549
|
* Controller methods a gem such as Devise adds, beyond the ones Rails
|
|
902
2550
|
* defines. They are left off an action's effects the same way Rails'
|
|
903
2551
|
* own are.
|
|
904
2552
|
*/
|
|
905
|
-
inheritedMethodNames:
|
|
2553
|
+
inheritedMethodNames: z15.array(z15.string()).optional(),
|
|
906
2554
|
/**
|
|
907
2555
|
* The routes file, `config/routes.rb` in a scaffolded app. A relative
|
|
908
2556
|
* path resolves against `configDirectory` when there is one.
|
|
909
2557
|
*/
|
|
910
|
-
routesFile:
|
|
2558
|
+
routesFile: z15.string().optional(),
|
|
911
2559
|
/**
|
|
912
2560
|
* Directories that each contain an engine, with its `Rails::Engine`
|
|
913
2561
|
* class under `lib/` and its routes in `config/routes.rb`. A `*` in a
|
|
914
2562
|
* segment matches any one directory, so `plugins/*` covers every
|
|
915
2563
|
* plugin. Relative paths resolve against `configDirectory`.
|
|
916
2564
|
*/
|
|
917
|
-
engineRoots:
|
|
2565
|
+
engineRoots: z15.array(z15.string()).optional(),
|
|
918
2566
|
/**
|
|
919
2567
|
* Other files that add routes through a `Rails.application.routes.draw`,
|
|
920
2568
|
* `.append` or `.prepend` block, most often a plugin's `plugin.rb`.
|
|
921
2569
|
* They take `*` the same way `engineRoots` does, and relative paths
|
|
922
2570
|
* resolve against `configDirectory`.
|
|
923
2571
|
*/
|
|
924
|
-
routesFiles:
|
|
2572
|
+
routesFiles: z15.array(z15.string()).optional()
|
|
925
2573
|
}).strict();
|
|
926
2574
|
var RAILS_ROOT_CLASS_NAMES = [
|
|
927
2575
|
"ActionController::Base",
|
|
928
2576
|
"ActionController::API"
|
|
929
2577
|
];
|
|
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
2578
|
var RESPONSE_STATUS_CALLS = [
|
|
940
|
-
{
|
|
2579
|
+
{
|
|
2580
|
+
name: "render",
|
|
2581
|
+
statusKeyword: "status",
|
|
2582
|
+
bodyKeyword: "json",
|
|
2583
|
+
bodySerializers: ["to_json", "as_json"]
|
|
2584
|
+
},
|
|
941
2585
|
{ name: "head", statusArgument: 0, statusKeyword: "status" },
|
|
942
2586
|
{ name: "redirect_to", statusKeyword: "status", defaultStatusCode: 302 },
|
|
943
2587
|
{ name: "redirect_back", statusKeyword: "status", defaultStatusCode: 302 }
|
|
@@ -993,22 +2637,15 @@ function controllerKeyFromQualified(qualifiedName, acronyms) {
|
|
|
993
2637
|
const withoutSuffix = qualifiedName.endsWith("Controller") ? qualifiedName.slice(0, -"Controller".length) : qualifiedName;
|
|
994
2638
|
return underscoreConstantPath2(withoutSuffix, acronyms);
|
|
995
2639
|
}
|
|
996
|
-
function
|
|
997
|
-
const template = RESTFUL_ACTIONS[actionName];
|
|
998
|
-
if (template === void 0) {
|
|
999
|
-
return null;
|
|
1000
|
-
}
|
|
2640
|
+
function conventionalRoutes(controllerKey, actionName) {
|
|
1001
2641
|
const resource = controllerKey.split("/").pop() ?? controllerKey;
|
|
1002
|
-
return {
|
|
1003
|
-
method: template.method,
|
|
1004
|
-
path: template.pathTemplate.replace(":resource", resource)
|
|
1005
|
-
};
|
|
2642
|
+
return restfulRoutes(actionName, `/${resource}`);
|
|
1006
2643
|
}
|
|
1007
2644
|
function resolveAgainst(configDirectory, value) {
|
|
1008
|
-
if (configDirectory === void 0 ||
|
|
2645
|
+
if (configDirectory === void 0 || path5.isAbsolute(value)) {
|
|
1009
2646
|
return value;
|
|
1010
2647
|
}
|
|
1011
|
-
return
|
|
2648
|
+
return path5.resolve(configDirectory, value);
|
|
1012
2649
|
}
|
|
1013
2650
|
function railsFramework(options = {}) {
|
|
1014
2651
|
const root = resolveAgainst(options.configDirectory, options.root ?? "app");
|
|
@@ -1016,7 +2653,8 @@ function railsFramework(options = {}) {
|
|
|
1016
2653
|
options.configDirectory,
|
|
1017
2654
|
options.routesFile ?? "config/routes.rb"
|
|
1018
2655
|
);
|
|
1019
|
-
const
|
|
2656
|
+
const railsRoot = path5.dirname(root);
|
|
2657
|
+
const displayPathOf = (file) => path5.isAbsolute(file) ? path5.relative(railsRoot, file) : file;
|
|
1020
2658
|
const engineRoots = () => expandPatterns(options.configDirectory, options.engineRoots);
|
|
1021
2659
|
const extraRoutesFiles = () => expandPatterns(options.configDirectory, options.routesFiles);
|
|
1022
2660
|
const inflections = options.configDirectory === void 0 ? EMPTY_INFLECTIONS : readInflections(options.configDirectory);
|
|
@@ -1024,7 +2662,7 @@ function railsFramework(options = {}) {
|
|
|
1024
2662
|
const routesInput = () => ({
|
|
1025
2663
|
routesFile: {
|
|
1026
2664
|
file: routesFile,
|
|
1027
|
-
displayPath:
|
|
2665
|
+
displayPath: displayPathOf(routesFile)
|
|
1028
2666
|
},
|
|
1029
2667
|
engines: readEngines(engineRoots(), acronyms).map((engine) => ({
|
|
1030
2668
|
...engine,
|
|
@@ -1047,24 +2685,28 @@ function railsFramework(options = {}) {
|
|
|
1047
2685
|
...options.baseClassNames ?? []
|
|
1048
2686
|
],
|
|
1049
2687
|
root,
|
|
2688
|
+
autoloadRoots: options.configDirectory === void 0 ? [] : readAutoloadRoots(options.configDirectory),
|
|
1050
2689
|
pathConvention: "railsUnderscore",
|
|
1051
2690
|
acronyms,
|
|
1052
2691
|
ancestryRootClassNames: [...RAILS_ROOT_CLASS_NAMES],
|
|
1053
2692
|
defaultStatusCode: 200,
|
|
1054
2693
|
responseStatusCalls: RESPONSE_STATUS_CALLS,
|
|
1055
2694
|
statusCodeNames: RACK_STATUS_CODE_NAMES,
|
|
2695
|
+
formatSwitch: { name: "respond_to", matchesEvery: "any" },
|
|
2696
|
+
libraryExceptions: LIBRARY_EXCEPTIONS,
|
|
1056
2697
|
inheritedMethodNames: [
|
|
1057
2698
|
...RAILS_CONTROLLER_METHODS,
|
|
1058
2699
|
...options.inheritedMethodNames ?? []
|
|
1059
2700
|
],
|
|
1060
2701
|
routesFile,
|
|
1061
2702
|
filters: CONTROLLER_FILTERS,
|
|
1062
|
-
|
|
2703
|
+
routesFor: (controllerQualifiedName, actionName) => {
|
|
1063
2704
|
const key = controllerKeyFromQualified(controllerQualifiedName, acronyms);
|
|
1064
2705
|
const found = routeTable();
|
|
1065
|
-
return found.fileFound ? found.
|
|
2706
|
+
return found.fileFound ? found.routesFor(key, actionName) : conventionalRoutes(key, actionName);
|
|
1066
2707
|
},
|
|
1067
|
-
routingGaps: () => routeTable().gaps
|
|
2708
|
+
routingGaps: () => routeTable().gaps,
|
|
2709
|
+
optionalFormat: true
|
|
1068
2710
|
};
|
|
1069
2711
|
return {
|
|
1070
2712
|
name: "rails",
|
|
@@ -1089,7 +2731,10 @@ function railsFramework(options = {}) {
|
|
|
1089
2731
|
...drawableRoutesFiles(routesFile),
|
|
1090
2732
|
...engineRoots().flatMap(engineSourceFiles),
|
|
1091
2733
|
...extraRoutesFiles(),
|
|
1092
|
-
...options.configDirectory === void 0 ? [] :
|
|
2734
|
+
...options.configDirectory === void 0 ? [] : [
|
|
2735
|
+
...inflectionFiles(options.configDirectory),
|
|
2736
|
+
...applicationFile(options.configDirectory)
|
|
2737
|
+
]
|
|
1093
2738
|
]
|
|
1094
2739
|
};
|
|
1095
2740
|
}
|
|
@@ -1100,10 +2745,10 @@ function expandPatterns(configDirectory, patterns) {
|
|
|
1100
2745
|
}
|
|
1101
2746
|
function drawableRoutesFiles(routesFile) {
|
|
1102
2747
|
const directory = drawDirectoryOf(routesFile);
|
|
1103
|
-
if (!
|
|
2748
|
+
if (!fs5.existsSync(directory)) {
|
|
1104
2749
|
return [];
|
|
1105
2750
|
}
|
|
1106
|
-
return
|
|
2751
|
+
return fs5.readdirSync(directory).filter((name) => name.endsWith(".rb")).sort().map((name) => path5.join(directory, name));
|
|
1107
2752
|
}
|
|
1108
2753
|
var declares = {
|
|
1109
2754
|
kind: "framework",
|