@suss/packs 0.21.0 → 0.22.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.
@@ -28,6 +28,7 @@ import '@suss/framework-nextjs';
28
28
  import '@suss/runtime-node';
29
29
  import '@suss/framework-package-exports';
30
30
  import '@suss/framework-prisma';
31
+ import '@suss/framework-rails';
31
32
  import '@suss/framework-react-query';
32
33
  import '@suss/framework-react-router';
33
34
  import '@suss/framework-react';
@@ -24,8 +24,15 @@ var ATTRIBUTES = ({ input, entry, kind }) => {
24
24
  if (projected !== null) {
25
25
  return projected;
26
26
  }
27
+ if (kind !== "write") {
28
+ return everything(kind);
29
+ }
27
30
  const written = namesIn(input.property("Item"));
28
- return kind === "write" && written.length > 0 ? written : everything(kind);
31
+ if (written.length > 0) {
32
+ return written;
33
+ }
34
+ const updated = updatedAttributes(input);
35
+ return updated.length > 0 ? updated : everything(kind);
29
36
  };
30
37
  var KEY_ATTRIBUTES = ({ input, entry }) => {
31
38
  if (entry !== null) {
@@ -101,6 +108,10 @@ function aliasesIn(input) {
101
108
  }
102
109
  return names;
103
110
  }
111
+ function updatedAttributes(input) {
112
+ const expression = input.property("UpdateExpression")?.text() ?? null;
113
+ return expression === null ? [] : updateExpressionAttributes(expression, aliasesIn(input));
114
+ }
104
115
  var REQUESTED = ["Item", "Key", "Keys"];
105
116
  function requestedAttributes(requests) {
106
117
  const found = /* @__PURE__ */ new Set();
@@ -129,7 +140,7 @@ var ATTRIBUTE_POSITIONS = [
129
140
  /\(\s*([#\w.]+)\s*,/g,
130
141
  /([#\w.]+)\s+[A-Za-z_]+\s+:/g
131
142
  ];
132
- function keyConditionAttributes(expression, names) {
143
+ function attributeCollector(names) {
133
144
  const found = [];
134
145
  const add = (token) => {
135
146
  if (token === void 0) {
@@ -140,6 +151,10 @@ function keyConditionAttributes(expression, names) {
140
151
  found.push(name);
141
152
  }
142
153
  };
154
+ return { found, add };
155
+ }
156
+ function keyConditionAttributes(expression, names) {
157
+ const { found, add } = attributeCollector(names);
143
158
  for (const pattern of ATTRIBUTE_POSITIONS) {
144
159
  for (const match of expression.matchAll(pattern)) {
145
160
  add(match[1]);
@@ -147,6 +162,56 @@ function keyConditionAttributes(expression, names) {
147
162
  }
148
163
  return found;
149
164
  }
165
+ function updateClauses(expression) {
166
+ const pattern = /\b(set|remove|add|delete)\b/gi;
167
+ const starts = [];
168
+ for (let match = pattern.exec(expression); match !== null; match = pattern.exec(expression)) {
169
+ starts.push({
170
+ keyword: match[0].toLowerCase(),
171
+ index: match.index,
172
+ bodyStart: match.index + match[0].length
173
+ });
174
+ }
175
+ return starts.map(({ keyword, bodyStart }, position) => ({
176
+ keyword,
177
+ body: expression.slice(
178
+ bodyStart,
179
+ starts[position + 1]?.index ?? expression.length
180
+ )
181
+ }));
182
+ }
183
+ function firstToken(item) {
184
+ return item.split(/\s+/)[0];
185
+ }
186
+ var CLAUSE_PATH = {
187
+ set: (item) => {
188
+ const at = item.indexOf("=");
189
+ return at === -1 ? null : item.slice(0, at);
190
+ },
191
+ remove: (item) => item,
192
+ add: firstToken,
193
+ delete: firstToken
194
+ };
195
+ function firstPathElement(path) {
196
+ return /^[#\w]+/.exec(path.trim())?.[0];
197
+ }
198
+ function updateExpressionAttributes(expression, names) {
199
+ const { found, add } = attributeCollector(names);
200
+ for (const clause of updateClauses(expression)) {
201
+ for (const raw of clause.body.split(",")) {
202
+ const item = raw.trim();
203
+ if (item === "") {
204
+ continue;
205
+ }
206
+ const path = CLAUSE_PATH[clause.keyword](item);
207
+ if (path === null) {
208
+ continue;
209
+ }
210
+ add(firstPathElement(path));
211
+ }
212
+ }
213
+ return found;
214
+ }
150
215
  var optionsSchema = z.object({
151
216
  /**
152
217
  * Further modules whose presence makes a file worth reading. A helper
@@ -1 +1 @@
1
- export { default, optionsSchema } from '@suss/framework-graphql-ruby';
1
+ export { GRAPHQL_RUBY_ROOT_CLASS_NAMES, default, optionsSchema } from '@suss/framework-graphql-ruby';
@@ -20,6 +20,16 @@ var optionsSchema = z.object({
20
20
  /** The schema-wide camelCase default. A `field` or `argument` call's own `camelize:` keyword overrides it for that one name. */
21
21
  camelize: z.boolean().optional()
22
22
  }).strict();
23
+ var GRAPHQL_RUBY_ROOT_CLASS_NAMES = [
24
+ "GraphQL::Schema::Object",
25
+ "GraphQL::Schema::Mutation",
26
+ "GraphQL::Schema::Resolver",
27
+ "GraphQL::Schema::Interface",
28
+ "GraphQL::Schema::InputObject",
29
+ "GraphQL::Schema::Enum",
30
+ "GraphQL::Schema::Union",
31
+ "GraphQL::Schema::Scalar"
32
+ ];
23
33
  var DEFAULT_BASE_CLASS_NAMES = [
24
34
  "Types::BaseObject",
25
35
  "Types::BaseInterface",
@@ -53,18 +63,7 @@ function graphqlRubyFramework(options) {
53
63
  argumentCallName: "argument",
54
64
  wiringKeywords: ["mutation", "resolver"],
55
65
  resolverMethodName: "resolve",
56
- // The classes graphql-ruby's own generated base classes extend or
57
- // mix in. A project's chain ends at one of them.
58
- ancestryRootClassNames: [
59
- "GraphQL::Schema::Object",
60
- "GraphQL::Schema::Mutation",
61
- "GraphQL::Schema::Resolver",
62
- "GraphQL::Schema::Interface",
63
- "GraphQL::Schema::InputObject",
64
- "GraphQL::Schema::Enum",
65
- "GraphQL::Schema::Union",
66
- "GraphQL::Schema::Scalar"
67
- ],
66
+ ancestryRootClassNames: [...GRAPHQL_RUBY_ROOT_CLASS_NAMES],
68
67
  requiredKeyword: "required",
69
68
  // graphql-ruby registers an argument as required unless the call
70
69
  // opts out with `required: false`.
@@ -99,6 +98,7 @@ function graphqlRubyFramework(options) {
99
98
  }
100
99
  var index_default = graphqlRubyFramework;
101
100
  export {
101
+ GRAPHQL_RUBY_ROOT_CLASS_NAMES,
102
102
  index_default as default,
103
103
  optionsSchema
104
104
  };
@@ -0,0 +1 @@
1
+ export { RAILS_ROOT_CLASS_NAMES, default, optionsSchema } from '@suss/framework-rails';
package/dist/rails.js ADDED
@@ -0,0 +1,425 @@
1
+ // ../framework/rails/dist/index.js
2
+ import path from "path";
3
+ import { z } from "zod";
4
+ import { underscoreConstantPath } from "@suss/adapter-ruby";
5
+ import fs from "fs";
6
+ import {
7
+ bodyStatements,
8
+ field,
9
+ hashKeySymbolName,
10
+ parseRubySync,
11
+ stringLiteralValue,
12
+ symbolValue
13
+ } from "@suss/adapter-ruby";
14
+ function readSimpleArgs(call) {
15
+ const argumentList = field(call, "arguments");
16
+ const positional = [];
17
+ const keyword = {};
18
+ let hashRocketPair = null;
19
+ if (argumentList === null) {
20
+ return { positional, keyword, hashRocketPair };
21
+ }
22
+ for (const child of bodyStatements(argumentList)) {
23
+ if (child.type !== "pair") {
24
+ positional.push(child);
25
+ continue;
26
+ }
27
+ const keyNode = field(child, "key");
28
+ const valueNode = field(child, "value");
29
+ if (keyNode === null || valueNode === null) {
30
+ continue;
31
+ }
32
+ const symbolKey = hashKeySymbolName(keyNode);
33
+ if (symbolKey !== null) {
34
+ keyword[symbolKey] = valueNode;
35
+ } else {
36
+ hashRocketPair = { key: keyNode, value: valueNode };
37
+ }
38
+ }
39
+ return { positional, keyword, hashRocketPair };
40
+ }
41
+ function wordValue(node) {
42
+ if (node === void 0) {
43
+ return null;
44
+ }
45
+ return symbolValue(node) ?? stringLiteralValue(node);
46
+ }
47
+ function splitControllerAction(text) {
48
+ const index = text.indexOf("#");
49
+ if (index === -1) {
50
+ return null;
51
+ }
52
+ return { controllerKey: text.slice(0, index), action: text.slice(index + 1) };
53
+ }
54
+ function readRouteTarget(args) {
55
+ const toText = args.keyword.to ? stringLiteralValue(args.keyword.to) : null;
56
+ if (toText !== null) {
57
+ const target = splitControllerAction(toText);
58
+ return target === null ? null : { ...target, path: null };
59
+ }
60
+ if (args.hashRocketPair !== null) {
61
+ const pathText = stringLiteralValue(args.hashRocketPair.key);
62
+ const actionText = stringLiteralValue(args.hashRocketPair.value);
63
+ const target = actionText !== null ? splitControllerAction(actionText) : null;
64
+ return target !== null && pathText !== null ? { ...target, path: pathText } : null;
65
+ }
66
+ return null;
67
+ }
68
+ function readSymbolList(node) {
69
+ if (node === void 0) {
70
+ return null;
71
+ }
72
+ if (node.type === "array") {
73
+ return bodyStatements(node).map((child) => symbolValue(child)).filter((name) => name !== null);
74
+ }
75
+ const single = symbolValue(node);
76
+ return single === null ? null : [single];
77
+ }
78
+ var RESTFUL_ROUTES_PLURAL = {
79
+ index: (base) => ({ method: "GET", path: base }),
80
+ create: (base) => ({ method: "POST", path: base }),
81
+ new: (base) => ({ method: "GET", path: `${base}/new` }),
82
+ show: (base) => ({ method: "GET", path: `${base}/:id` }),
83
+ edit: (base) => ({ method: "GET", path: `${base}/:id/edit` }),
84
+ update: (base) => ({ method: "PATCH", path: `${base}/:id` }),
85
+ destroy: (base) => ({ method: "DELETE", path: `${base}/:id` })
86
+ };
87
+ var RESTFUL_ROUTES_SINGULAR = {
88
+ create: (base) => ({ method: "POST", path: base }),
89
+ new: (base) => ({ method: "GET", path: `${base}/new` }),
90
+ show: (base) => ({ method: "GET", path: base }),
91
+ edit: (base) => ({ method: "GET", path: `${base}/edit` }),
92
+ update: (base) => ({ method: "PATCH", path: base }),
93
+ destroy: (base) => ({ method: "DELETE", path: base })
94
+ };
95
+ function pluralize(word) {
96
+ if (/[^aeiou]y$/.test(word)) {
97
+ return `${word.slice(0, -1)}ies`;
98
+ }
99
+ if (/(s|x|z|ch|sh)$/.test(word)) {
100
+ return `${word}es`;
101
+ }
102
+ return `${word}s`;
103
+ }
104
+ function singularize(word) {
105
+ if (/ies$/.test(word)) {
106
+ return `${word.slice(0, -3)}y`;
107
+ }
108
+ if (/(ses|xes|zes|ches|shes)$/.test(word)) {
109
+ return word.slice(0, -2);
110
+ }
111
+ return /s$/.test(word) ? word.slice(0, -1) : word;
112
+ }
113
+ var RouteAccumulator = class {
114
+ byKey = /* @__PURE__ */ new Map();
115
+ unread = /* @__PURE__ */ new Set();
116
+ add(controllerKey, action, route) {
117
+ this.byKey.set(`${controllerKey}#${action}`, route);
118
+ }
119
+ recordUnread(callName) {
120
+ this.unread.add(callName);
121
+ }
122
+ routeFor(controllerKey, action) {
123
+ return this.byKey.get(`${controllerKey}#${action}`) ?? null;
124
+ }
125
+ gapsAgainst(displayPath) {
126
+ if (this.unread.size === 0) {
127
+ return [];
128
+ }
129
+ const names = [...this.unread].sort().join(", ");
130
+ return [
131
+ `${displayPath} also declares ${names}, which this pack does not read; whatever those declarations route is missing from what suss reports`
132
+ ];
133
+ }
134
+ };
135
+ function joinKey(prefix, segment) {
136
+ return prefix === "" ? segment : `${prefix}/${segment}`;
137
+ }
138
+ function joinPath(prefix, segment) {
139
+ return `${prefix}/${segment}`;
140
+ }
141
+ function handleResourceCall(call, ctx, out, plural) {
142
+ const args = readSimpleArgs(call);
143
+ const symbol = args.positional[0] ? symbolValue(args.positional[0]) : null;
144
+ if (symbol === null) {
145
+ return;
146
+ }
147
+ const controllerSegment = plural ? symbol : pluralize(symbol);
148
+ const controllerKey = joinKey(ctx.modulePrefix, controllerSegment);
149
+ const prefixBase = ctx.resource !== void 0 ? ctx.resource.nestedBase : ctx.pathPrefix;
150
+ const base = joinPath(prefixBase, symbol);
151
+ const only = readSymbolList(args.keyword.only);
152
+ const except = readSymbolList(args.keyword.except);
153
+ const table = plural ? RESTFUL_ROUTES_PLURAL : RESTFUL_ROUTES_SINGULAR;
154
+ for (const [action, routeAt] of Object.entries(table)) {
155
+ if (only !== null && !only.includes(action)) {
156
+ continue;
157
+ }
158
+ if (except?.includes(action)) {
159
+ continue;
160
+ }
161
+ out.add(controllerKey, action, routeAt(base));
162
+ }
163
+ const block = field(call, "block");
164
+ const body = block !== null ? field(block, "body") : null;
165
+ if (body === null) {
166
+ return;
167
+ }
168
+ const nested = {
169
+ controllerKey,
170
+ collectionBase: base,
171
+ memberBase: plural ? `${base}/:id` : base,
172
+ nestedBase: `${base}/:${singularize(symbol)}_id`,
173
+ ambientBase: null
174
+ };
175
+ walkBody(body, { ...ctx, resource: nested }, out);
176
+ }
177
+ function handleOnBlock(call, ctx, out, isMember) {
178
+ if (ctx.resource === void 0) {
179
+ return;
180
+ }
181
+ const block = field(call, "block");
182
+ const body = block !== null ? field(block, "body") : null;
183
+ if (body === null) {
184
+ return;
185
+ }
186
+ const ambientBase = isMember ? ctx.resource.memberBase : ctx.resource.collectionBase;
187
+ walkBody(body, { ...ctx, resource: { ...ctx.resource, ambientBase } }, out);
188
+ }
189
+ function handleVerb(call, ctx, out, method) {
190
+ const args = readSimpleArgs(call);
191
+ const target = readRouteTarget(args);
192
+ if (target !== null) {
193
+ const literalPath = args.positional[0] ? stringLiteralValue(args.positional[0]) : null;
194
+ const path2 = target.path ?? literalPath;
195
+ if (path2 !== null) {
196
+ out.add(target.controllerKey, target.action, {
197
+ method,
198
+ path: `${ctx.pathPrefix}${path2}`
199
+ });
200
+ }
201
+ return;
202
+ }
203
+ if (ctx.resource === void 0) {
204
+ return;
205
+ }
206
+ const symbol = args.positional[0] ? symbolValue(args.positional[0]) : null;
207
+ if (symbol === null) {
208
+ return;
209
+ }
210
+ const on = wordValue(args.keyword.on);
211
+ const base = on === "collection" ? ctx.resource.collectionBase : on === "member" ? ctx.resource.memberBase : ctx.resource.ambientBase;
212
+ if (base === null) {
213
+ return;
214
+ }
215
+ out.add(ctx.resource.controllerKey, symbol, {
216
+ method,
217
+ path: `${base}/${symbol}`
218
+ });
219
+ }
220
+ function handleRoot(call, ctx, out) {
221
+ const args = readSimpleArgs(call);
222
+ const target = readRouteTarget(args) ?? (args.positional[0] ? splitControllerAction(stringLiteralValue(args.positional[0]) ?? "") : null);
223
+ if (target !== null) {
224
+ out.add(target.controllerKey, target.action, {
225
+ method: "GET",
226
+ path: ctx.pathPrefix === "" ? "/" : ctx.pathPrefix
227
+ });
228
+ }
229
+ }
230
+ function handleNamespace(call, ctx, out) {
231
+ const args = readSimpleArgs(call);
232
+ const symbol = args.positional[0] ? symbolValue(args.positional[0]) : null;
233
+ const block = field(call, "block");
234
+ const body = block !== null ? field(block, "body") : null;
235
+ if (symbol === null || body === null) {
236
+ return;
237
+ }
238
+ walkBody(
239
+ body,
240
+ {
241
+ pathPrefix: joinPath(ctx.pathPrefix, symbol),
242
+ modulePrefix: joinKey(ctx.modulePrefix, symbol)
243
+ },
244
+ out
245
+ );
246
+ }
247
+ function handleScope(call, ctx, out) {
248
+ const args = readSimpleArgs(call);
249
+ const moduleName = wordValue(args.keyword.module);
250
+ const pathSegment = wordValue(args.keyword.path);
251
+ const block = field(call, "block");
252
+ const body = block !== null ? field(block, "body") : null;
253
+ if (body === null) {
254
+ return;
255
+ }
256
+ walkBody(
257
+ body,
258
+ {
259
+ pathPrefix: pathSegment !== null ? joinPath(ctx.pathPrefix, pathSegment) : ctx.pathPrefix,
260
+ modulePrefix: moduleName !== null ? joinKey(ctx.modulePrefix, moduleName) : ctx.modulePrefix
261
+ },
262
+ out
263
+ );
264
+ }
265
+ var HANDLERS = {
266
+ resources: (call, ctx, out) => handleResourceCall(call, ctx, out, true),
267
+ resource: (call, ctx, out) => handleResourceCall(call, ctx, out, false),
268
+ member: (call, ctx, out) => handleOnBlock(call, ctx, out, true),
269
+ collection: (call, ctx, out) => handleOnBlock(call, ctx, out, false),
270
+ namespace: handleNamespace,
271
+ scope: handleScope,
272
+ root: handleRoot,
273
+ get: (call, ctx, out) => handleVerb(call, ctx, out, "GET"),
274
+ post: (call, ctx, out) => handleVerb(call, ctx, out, "POST"),
275
+ patch: (call, ctx, out) => handleVerb(call, ctx, out, "PATCH"),
276
+ put: (call, ctx, out) => handleVerb(call, ctx, out, "PUT"),
277
+ delete: (call, ctx, out) => handleVerb(call, ctx, out, "DELETE")
278
+ };
279
+ function walkBody(body, ctx, out) {
280
+ for (const statement of bodyStatements(body)) {
281
+ if (statement.type !== "call" || field(statement, "receiver") !== null) {
282
+ continue;
283
+ }
284
+ const name = field(statement, "method")?.text;
285
+ if (name === void 0) {
286
+ continue;
287
+ }
288
+ const handler = HANDLERS[name];
289
+ if (handler === void 0) {
290
+ out.recordUnread(name);
291
+ continue;
292
+ }
293
+ handler(statement, ctx, out);
294
+ }
295
+ }
296
+ function drawBlockBody(root) {
297
+ for (const statement of bodyStatements(root)) {
298
+ if (statement.type !== "call") {
299
+ continue;
300
+ }
301
+ const block = field(statement, "block");
302
+ const body = block !== null ? field(block, "body") : null;
303
+ if (body !== null) {
304
+ return body;
305
+ }
306
+ }
307
+ return null;
308
+ }
309
+ function readRoutesFile(absRoutesPath, displayPath) {
310
+ if (!fs.existsSync(absRoutesPath)) {
311
+ return {
312
+ routeFor: () => null,
313
+ fileFound: false,
314
+ gaps: [
315
+ `${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`
316
+ ]
317
+ };
318
+ }
319
+ const source = fs.readFileSync(absRoutesPath, "utf8");
320
+ const tree = parseRubySync(source);
321
+ const out = new RouteAccumulator();
322
+ const body = drawBlockBody(tree.rootNode);
323
+ if (body !== null) {
324
+ walkBody(body, { pathPrefix: "", modulePrefix: "" }, out);
325
+ }
326
+ return {
327
+ routeFor: (controllerKey, action) => out.routeFor(controllerKey, action),
328
+ fileFound: true,
329
+ gaps: out.gapsAgainst(displayPath)
330
+ };
331
+ }
332
+ var optionsSchema = z.object({
333
+ /** The app directory a bare controller name is looked up under. Every Rails app scaffolds this at `app`. */
334
+ root: z.string().optional(),
335
+ /**
336
+ * The directory of the config file these options came from. Whatever
337
+ * read that file supplies this; it is not written in the file itself.
338
+ */
339
+ configDirectory: z.string().optional(),
340
+ /** Base classes beyond `ApplicationController` that also mark a class as a controller. */
341
+ baseClassNames: z.array(z.string()).optional(),
342
+ /** Where this project's routes live, relative to `configDirectory` when there is one. Every Rails app scaffolds this at `config/routes.rb`. */
343
+ routesFile: z.string().optional()
344
+ }).strict();
345
+ var RAILS_ROOT_CLASS_NAMES = [
346
+ "ActionController::Base",
347
+ "ActionController::API"
348
+ ];
349
+ var RESTFUL_ACTIONS = {
350
+ index: { method: "GET", pathTemplate: "/:resource" },
351
+ show: { method: "GET", pathTemplate: "/:resource/:id" },
352
+ new: { method: "GET", pathTemplate: "/:resource/new" },
353
+ create: { method: "POST", pathTemplate: "/:resource" },
354
+ edit: { method: "GET", pathTemplate: "/:resource/:id/edit" },
355
+ update: { method: "PATCH", pathTemplate: "/:resource/:id" },
356
+ destroy: { method: "DELETE", pathTemplate: "/:resource/:id" }
357
+ };
358
+ function controllerKeyFromQualified(qualifiedName) {
359
+ const withoutSuffix = qualifiedName.endsWith("Controller") ? qualifiedName.slice(0, -"Controller".length) : qualifiedName;
360
+ return underscoreConstantPath(withoutSuffix);
361
+ }
362
+ function conventionalRoute(controllerKey, actionName) {
363
+ const template = RESTFUL_ACTIONS[actionName];
364
+ if (template === void 0) {
365
+ return null;
366
+ }
367
+ const resource = controllerKey.split("/").pop() ?? controllerKey;
368
+ return {
369
+ method: template.method,
370
+ path: template.pathTemplate.replace(":resource", resource)
371
+ };
372
+ }
373
+ function resolveAgainst(configDirectory, value) {
374
+ if (configDirectory === void 0 || path.isAbsolute(value)) {
375
+ return value;
376
+ }
377
+ return path.resolve(configDirectory, value);
378
+ }
379
+ function railsFramework(options = {}) {
380
+ const root = resolveAgainst(options.configDirectory, options.root ?? "app");
381
+ const routesFile = resolveAgainst(
382
+ options.configDirectory,
383
+ options.routesFile ?? "config/routes.rb"
384
+ );
385
+ let table;
386
+ const routeTable = () => {
387
+ table ??= readRoutesFile(
388
+ routesFile,
389
+ options.routesFile ?? "config/routes.rb"
390
+ );
391
+ return table;
392
+ };
393
+ const pattern = {
394
+ type: "controllerActions",
395
+ baseClassNames: [
396
+ "ApplicationController",
397
+ ...options.baseClassNames ?? []
398
+ ],
399
+ root,
400
+ pathConvention: "railsUnderscore",
401
+ ancestryRootClassNames: [...RAILS_ROOT_CLASS_NAMES],
402
+ defaultStatusCode: 200,
403
+ routesFile,
404
+ routeFor: (controllerQualifiedName, actionName) => {
405
+ const key = controllerKeyFromQualified(controllerQualifiedName);
406
+ const found = routeTable();
407
+ return found.fileFound ? found.routeFor(key, actionName) : conventionalRoute(key, actionName);
408
+ },
409
+ routingGaps: () => routeTable().gaps
410
+ };
411
+ return {
412
+ name: "rails",
413
+ protocol: "http",
414
+ discovery: [pattern],
415
+ // The routes file decides every action's method and path but is
416
+ // never walked, so the cache key has to read it here.
417
+ discoveryInputs: () => [routesFile]
418
+ };
419
+ }
420
+ var index_default = railsFramework;
421
+ export {
422
+ RAILS_ROOT_CLASS_NAMES,
423
+ index_default as default,
424
+ optionsSchema
425
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suss/packs",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "type": "module",
5
5
  "description": "Every pack suss ships, as one package with a subpath per pack.",
6
6
  "keywords": [
@@ -140,6 +140,10 @@
140
140
  "types": "./dist/prisma.d.ts",
141
141
  "import": "./dist/prisma.js"
142
142
  },
143
+ "./rails": {
144
+ "types": "./dist/rails.d.ts",
145
+ "import": "./dist/rails.js"
146
+ },
143
147
  "./react": {
144
148
  "types": "./dist/react.d.ts",
145
149
  "import": "./dist/react.js"
@@ -185,15 +189,15 @@
185
189
  "ts-morph": "^23.0.0"
186
190
  },
187
191
  "dependencies": {
188
- "@suss/adapter-python": "0.21.0",
189
- "@suss/adapter-ruby": "0.21.0",
190
- "@suss/adapter-typescript": "0.21.0",
191
- "@suss/behavioral-ir": "0.21.0",
192
- "@suss/contract-graphql": "0.21.0",
193
- "@suss/extractor": "0.21.0",
194
- "@suss/manifest-aws": "0.21.0",
195
- "@suss/recognize": "0.21.0",
196
- "@suss/sql": "0.21.0",
192
+ "@suss/adapter-python": "0.22.0",
193
+ "@suss/adapter-ruby": "0.22.0",
194
+ "@suss/adapter-typescript": "0.22.0",
195
+ "@suss/behavioral-ir": "0.22.0",
196
+ "@suss/contract-graphql": "0.22.0",
197
+ "@suss/extractor": "0.22.0",
198
+ "@suss/manifest-aws": "0.22.0",
199
+ "@suss/recognize": "0.22.0",
200
+ "@suss/sql": "0.22.0",
197
201
  "zod": "^4.0.0"
198
202
  },
199
203
  "devDependencies": {
@@ -226,6 +230,7 @@
226
230
  "@suss/framework-nextjs": "*",
227
231
  "@suss/framework-package-exports": "*",
228
232
  "@suss/framework-prisma": "*",
233
+ "@suss/framework-rails": "*",
229
234
  "@suss/framework-react": "*",
230
235
  "@suss/framework-react-query": "*",
231
236
  "@suss/framework-zustand": "*",