@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.
- package/dist/activerecord.d.ts +1 -0
- package/dist/aws-dynamodb.js +67 -2
- package/dist/graphql-ruby.d.ts +1 -1
- package/dist/graphql-ruby.js +12 -12
- package/dist/rails.d.ts +1 -0
- package/dist/rails.js +425 -0
- package/package.json +15 -10
package/dist/activerecord.d.ts
CHANGED
|
@@ -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';
|
package/dist/aws-dynamodb.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
package/dist/graphql-ruby.d.ts
CHANGED
|
@@ -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';
|
package/dist/graphql-ruby.js
CHANGED
|
@@ -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
|
-
|
|
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
|
};
|
package/dist/rails.d.ts
ADDED
|
@@ -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.
|
|
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.
|
|
189
|
-
"@suss/adapter-ruby": "0.
|
|
190
|
-
"@suss/adapter-typescript": "0.
|
|
191
|
-
"@suss/behavioral-ir": "0.
|
|
192
|
-
"@suss/contract-graphql": "0.
|
|
193
|
-
"@suss/extractor": "0.
|
|
194
|
-
"@suss/manifest-aws": "0.
|
|
195
|
-
"@suss/recognize": "0.
|
|
196
|
-
"@suss/sql": "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": "*",
|