@better-fullstack/types 2.0.0 → 2.0.2
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/{defaults-DNmNiuxG.mjs → defaults-Cy0_ttEE.mjs} +31 -1
- package/dist/defaults-Cy0_ttEE.mjs.map +1 -0
- package/dist/{defaults-BSM_JIBn.d.mts → defaults-zyxI-U_5.d.mts} +2 -2
- package/dist/defaults-zyxI-U_5.d.mts.map +1 -0
- package/dist/defaults.d.mts +3 -3
- package/dist/defaults.mjs +1 -1
- package/dist/index.d.mts +6 -6
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +5 -5
- package/dist/json-schema.d.mts +2183 -193
- package/dist/json-schema.d.mts.map +1 -1
- package/dist/json-schema.mjs +1 -1
- package/dist/{schemas-B5EgRfVL.d.mts → schemas-BTO7jwoE.d.mts} +2048 -232
- package/dist/schemas-BTO7jwoE.d.mts.map +1 -0
- package/dist/{schemas-DnEGgQzA.mjs → schemas-D444b8s8.mjs} +321 -14
- package/dist/schemas-D444b8s8.mjs.map +1 -0
- package/dist/schemas.d.mts +2 -2
- package/dist/schemas.mjs +2 -2
- package/dist/{stack-graph-_551XvrL.d.mts → stack-graph-DH1m1ONQ.d.mts} +19 -4
- package/dist/stack-graph-DH1m1ONQ.d.mts.map +1 -0
- package/dist/stack-graph-DXh8F1TL.mjs +2114 -0
- package/dist/stack-graph-DXh8F1TL.mjs.map +1 -0
- package/dist/stack-graph.d.mts +4 -4
- package/dist/stack-graph.mjs +3 -3
- package/dist/{stack-translation-BcsaMRuV.mjs → stack-translation-Bb3LIDTp.mjs} +4094 -3025
- package/dist/stack-translation-Bb3LIDTp.mjs.map +1 -0
- package/dist/{stack-translation-CqslePvp.d.mts → stack-translation-LwAKaXg1.d.mts} +116 -34
- package/dist/stack-translation-LwAKaXg1.d.mts.map +1 -0
- package/dist/stack-translation.d.mts +4 -4
- package/dist/stack-translation.mjs +3 -3
- package/dist/types-C4r4ucMM.d.mts +153 -0
- package/dist/types-C4r4ucMM.d.mts.map +1 -0
- package/dist/types.d.mts +3 -3
- package/package.json +1 -1
- package/dist/defaults-BSM_JIBn.d.mts.map +0 -1
- package/dist/defaults-DNmNiuxG.mjs.map +0 -1
- package/dist/schemas-B5EgRfVL.d.mts.map +0 -1
- package/dist/schemas-DnEGgQzA.mjs.map +0 -1
- package/dist/stack-graph-C2tzuzRe.mjs +0 -585
- package/dist/stack-graph-C2tzuzRe.mjs.map +0 -1
- package/dist/stack-graph-_551XvrL.d.mts.map +0 -1
- package/dist/stack-translation-BcsaMRuV.mjs.map +0 -1
- package/dist/stack-translation-CqslePvp.d.mts.map +0 -1
- package/dist/types-DorAmN4i.d.mts +0 -123
- package/dist/types-DorAmN4i.d.mts.map +0 -1
|
@@ -0,0 +1,2114 @@
|
|
|
1
|
+
import { $n as MOBILE_STORAGE_VALUES, $r as RUST_CLI_VALUES, An as JAVA_AUTH_VALUES, B as DOTNET_OBSERVABILITY_VALUES, Cr as PYTHON_QUALITY_VALUES, Ct as ELIXIR_WEB_FRAMEWORK_VALUES, D as CMS_VALUES, Di as SEARCH_VALUES, Dr as PYTHON_VALIDATION_VALUES, En as I18N_VALUES, Er as PYTHON_TESTING_VALUES, F as DOTNET_API_VALUES, Fn as JAVA_TESTING_LIBRARIES_VALUES, G as DOTNET_WEB_FRAMEWORK_VALUES, H as DOTNET_REALTIME_VALUES, I as DOTNET_AUTH_VALUES, Ii as STATE_MANAGEMENT_VALUES, In as JAVA_WEB_FRAMEWORK_VALUES, Jr as REALTIME_VALUES, Jt as FILE_UPLOAD_VALUES, Kt as FEATURE_FLAGS_VALUES, L as DOTNET_CACHING_VALUES, Ln as JOB_QUEUE_VALUES, M as DATABASE_SETUP_VALUES, Mn as JAVA_LIBRARIES_VALUES, N as DATABASE_VALUES, Nn as JAVA_LOGGING_VALUES, Oi as SERVER_DEPLOY_VALUES, Or as PYTHON_WEB_FRAMEWORK_VALUES, Pn as JAVA_ORM_VALUES, Qr as RUST_CACHING_VALUES, R as DOTNET_DEPLOY_VALUES, Sr as PYTHON_ORM_VALUES, St as ELIXIR_VALIDATION_VALUES, Tr as PYTHON_TASK_QUEUE_VALUES, Tt as EXAMPLES_VALUES, U as DOTNET_TESTING_VALUES, V as DOTNET_ORM_VALUES, W as DOTNET_VALIDATION_VALUES, Xn as MOBILE_NAVIGATION_VALUES, Xr as RUST_API_VALUES, Xt as FRONTEND_VALUES, Yr as RUNTIME_VALUES, Yt as FORMS_VALUES, Zr as RUST_AUTH_VALUES, _r as PYTHON_AUTH_VALUES, _t as ELIXIR_OBSERVABILITY_VALUES, a as ANALYTICS_VALUES, ai as RUST_OBSERVABILITY_VALUES, an as GO_CLI_VALUES, br as PYTHON_GRAPHQL_VALUES, bt as ELIXIR_REALTIME_VALUES, c as API_VALUES, ci as RUST_TEMPLATING_VALUES, cn as GO_MESSAGE_QUEUE_VALUES, ct as ELIXIR_API_VALUES, dn as GO_REALTIME_VALUES, dr as ORM_VALUES, dt as ELIXIR_DEPLOY_VALUES, ei as RUST_ERROR_HANDLING_VALUES, er as MOBILE_TESTING_VALUES, fn as GO_TESTING_VALUES, ft as ELIXIR_EMAIL_VALUES, gr as PYTHON_API_VALUES, gt as ELIXIR_LIBRARIES_VALUES, hr as PYTHON_AI_VALUES, i as AI_VALUES, ii as RUST_MESSAGE_QUEUE_VALUES, in as GO_CACHING_VALUES, jn as JAVA_BUILD_TOOL_VALUES, k as CSS_FRAMEWORK_VALUES, kn as JAVA_API_VALUES, li as RUST_WEB_FRAMEWORK_VALUES, ln as GO_OBSERVABILITY_VALUES, lr as OBSERVABILITY_VALUES, lt as ELIXIR_AUTH_VALUES, mr as PAYMENTS_VALUES, mt as ELIXIR_JOBS_VALUES, na as UI_LIBRARY_VALUES, ni as RUST_LIBRARIES_VALUES, nn as GO_API_VALUES, o as ANIMATION_VALUES, oi as RUST_ORM_VALUES, on as GO_CONFIG_VALUES, pn as GO_WEB_FRAMEWORK_VALUES, pt as ELIXIR_HTTP_VALUES, qi as StackPartRoleSchema, qn as LOGGING_VALUES, qr as RATE_LIMIT_VALUES, qt as FILE_STORAGE_VALUES, ra as VALIDATION_VALUES, ri as RUST_LOGGING_VALUES, rn as GO_AUTH_VALUES, sa as WEB_DEPLOY_VALUES, si as RUST_REALTIME_VALUES, sn as GO_LOGGING_VALUES, st as EFFECT_VALUES, t as ADDONS_VALUES, ti as RUST_FRONTEND_VALUES, tr as MOBILE_UI_VALUES, u as AUTH_VALUES, un as GO_ORM_VALUES, ut as ELIXIR_CACHING_VALUES, v as BACKEND_VALUES, vr as PYTHON_CACHING_VALUES, vt as ELIXIR_ORM_VALUES, w as CACHING_VALUES, wr as PYTHON_REALTIME_VALUES, wt as EMAIL_VALUES, xr as PYTHON_OBSERVABILITY_VALUES, xt as ELIXIR_TESTING_VALUES, yr as PYTHON_CLI_VALUES, yt as ELIXIR_QUALITY_VALUES, z as DOTNET_JOB_QUEUE_VALUES } from "./schemas-D444b8s8.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/stack-compatibility-rules.ts
|
|
4
|
+
const WEB_FRAMEWORKS = [
|
|
5
|
+
"tanstack-router",
|
|
6
|
+
"react-router",
|
|
7
|
+
"react-vite",
|
|
8
|
+
"tanstack-start",
|
|
9
|
+
"next",
|
|
10
|
+
"vinext",
|
|
11
|
+
"nuxt",
|
|
12
|
+
"svelte",
|
|
13
|
+
"solid",
|
|
14
|
+
"solid-start",
|
|
15
|
+
"astro",
|
|
16
|
+
"qwik",
|
|
17
|
+
"angular",
|
|
18
|
+
"redwood",
|
|
19
|
+
"fresh",
|
|
20
|
+
"none"
|
|
21
|
+
];
|
|
22
|
+
const WEB_DEPLOY_COMPATIBLE_FRONTENDS = {
|
|
23
|
+
render: [
|
|
24
|
+
"tanstack-router",
|
|
25
|
+
"react-router",
|
|
26
|
+
"react-vite",
|
|
27
|
+
"tanstack-start",
|
|
28
|
+
"next",
|
|
29
|
+
"nuxt",
|
|
30
|
+
"svelte",
|
|
31
|
+
"solid"
|
|
32
|
+
],
|
|
33
|
+
netlify: [
|
|
34
|
+
"tanstack-router",
|
|
35
|
+
"react-router",
|
|
36
|
+
"react-vite",
|
|
37
|
+
"tanstack-start",
|
|
38
|
+
"next",
|
|
39
|
+
"nuxt",
|
|
40
|
+
"svelte",
|
|
41
|
+
"solid",
|
|
42
|
+
"solid-start"
|
|
43
|
+
]
|
|
44
|
+
};
|
|
45
|
+
const hasPWACompatibleFrontend = (webFrontend) => webFrontend.some((frontend) => [
|
|
46
|
+
"tanstack-router",
|
|
47
|
+
"react-router",
|
|
48
|
+
"react-vite",
|
|
49
|
+
"solid",
|
|
50
|
+
"next",
|
|
51
|
+
"vinext",
|
|
52
|
+
"astro"
|
|
53
|
+
].includes(frontend));
|
|
54
|
+
const hasTauriCompatibleFrontend = (webFrontend) => webFrontend.some((frontend) => [
|
|
55
|
+
"tanstack-router",
|
|
56
|
+
"react-router",
|
|
57
|
+
"react-vite",
|
|
58
|
+
"nuxt",
|
|
59
|
+
"svelte",
|
|
60
|
+
"solid",
|
|
61
|
+
"next",
|
|
62
|
+
"vinext",
|
|
63
|
+
"astro"
|
|
64
|
+
].includes(frontend));
|
|
65
|
+
const hasDockerComposeCompatibleFrontend = (webFrontend) => webFrontend.some((frontend) => [
|
|
66
|
+
"tanstack-router",
|
|
67
|
+
"react-router",
|
|
68
|
+
"react-vite",
|
|
69
|
+
"solid",
|
|
70
|
+
"next",
|
|
71
|
+
"vinext",
|
|
72
|
+
"astro"
|
|
73
|
+
].includes(frontend));
|
|
74
|
+
function getUnsupportedWebDeployFrontend(webDeploy, frontends = []) {
|
|
75
|
+
const supported = WEB_DEPLOY_COMPATIBLE_FRONTENDS[webDeploy];
|
|
76
|
+
if (!supported) return void 0;
|
|
77
|
+
return frontends.find((frontend) => {
|
|
78
|
+
if (frontend === "none" || !WEB_FRAMEWORKS.includes(frontend)) return false;
|
|
79
|
+
return !supported.includes(frontend);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const UI_LIBRARY_COMPATIBILITY = {
|
|
83
|
+
"shadcn-ui": {
|
|
84
|
+
frontends: [
|
|
85
|
+
"tanstack-router",
|
|
86
|
+
"react-router",
|
|
87
|
+
"react-vite",
|
|
88
|
+
"tanstack-start",
|
|
89
|
+
"next",
|
|
90
|
+
"vinext",
|
|
91
|
+
"astro"
|
|
92
|
+
],
|
|
93
|
+
cssFrameworks: ["tailwind"]
|
|
94
|
+
},
|
|
95
|
+
"shadcn-svelte": {
|
|
96
|
+
frontends: ["svelte", "astro"],
|
|
97
|
+
cssFrameworks: ["tailwind"]
|
|
98
|
+
},
|
|
99
|
+
daisyui: {
|
|
100
|
+
frontends: [
|
|
101
|
+
"tanstack-router",
|
|
102
|
+
"react-router",
|
|
103
|
+
"react-vite",
|
|
104
|
+
"tanstack-start",
|
|
105
|
+
"next",
|
|
106
|
+
"vinext",
|
|
107
|
+
"nuxt",
|
|
108
|
+
"svelte",
|
|
109
|
+
"solid",
|
|
110
|
+
"solid-start",
|
|
111
|
+
"astro",
|
|
112
|
+
"qwik",
|
|
113
|
+
"angular",
|
|
114
|
+
"redwood",
|
|
115
|
+
"fresh"
|
|
116
|
+
],
|
|
117
|
+
cssFrameworks: ["tailwind"]
|
|
118
|
+
},
|
|
119
|
+
"radix-ui": {
|
|
120
|
+
frontends: [
|
|
121
|
+
"tanstack-router",
|
|
122
|
+
"react-router",
|
|
123
|
+
"react-vite",
|
|
124
|
+
"tanstack-start",
|
|
125
|
+
"next",
|
|
126
|
+
"vinext",
|
|
127
|
+
"astro"
|
|
128
|
+
],
|
|
129
|
+
cssFrameworks: [
|
|
130
|
+
"tailwind",
|
|
131
|
+
"scss",
|
|
132
|
+
"less",
|
|
133
|
+
"postcss-only",
|
|
134
|
+
"none"
|
|
135
|
+
]
|
|
136
|
+
},
|
|
137
|
+
"headless-ui": {
|
|
138
|
+
frontends: [
|
|
139
|
+
"tanstack-router",
|
|
140
|
+
"react-router",
|
|
141
|
+
"react-vite",
|
|
142
|
+
"tanstack-start",
|
|
143
|
+
"next",
|
|
144
|
+
"vinext",
|
|
145
|
+
"nuxt",
|
|
146
|
+
"astro"
|
|
147
|
+
],
|
|
148
|
+
cssFrameworks: [
|
|
149
|
+
"tailwind",
|
|
150
|
+
"scss",
|
|
151
|
+
"less",
|
|
152
|
+
"postcss-only",
|
|
153
|
+
"none"
|
|
154
|
+
]
|
|
155
|
+
},
|
|
156
|
+
"park-ui": {
|
|
157
|
+
frontends: [
|
|
158
|
+
"tanstack-router",
|
|
159
|
+
"react-router",
|
|
160
|
+
"react-vite",
|
|
161
|
+
"tanstack-start",
|
|
162
|
+
"next",
|
|
163
|
+
"vinext",
|
|
164
|
+
"nuxt",
|
|
165
|
+
"solid",
|
|
166
|
+
"solid-start",
|
|
167
|
+
"astro"
|
|
168
|
+
],
|
|
169
|
+
cssFrameworks: [
|
|
170
|
+
"tailwind",
|
|
171
|
+
"scss",
|
|
172
|
+
"less",
|
|
173
|
+
"postcss-only"
|
|
174
|
+
]
|
|
175
|
+
},
|
|
176
|
+
"chakra-ui": {
|
|
177
|
+
frontends: [
|
|
178
|
+
"tanstack-router",
|
|
179
|
+
"react-router",
|
|
180
|
+
"react-vite",
|
|
181
|
+
"tanstack-start",
|
|
182
|
+
"next",
|
|
183
|
+
"vinext",
|
|
184
|
+
"astro"
|
|
185
|
+
],
|
|
186
|
+
cssFrameworks: [
|
|
187
|
+
"tailwind",
|
|
188
|
+
"scss",
|
|
189
|
+
"less",
|
|
190
|
+
"postcss-only",
|
|
191
|
+
"none"
|
|
192
|
+
]
|
|
193
|
+
},
|
|
194
|
+
nextui: {
|
|
195
|
+
frontends: [
|
|
196
|
+
"tanstack-router",
|
|
197
|
+
"react-router",
|
|
198
|
+
"react-vite",
|
|
199
|
+
"tanstack-start",
|
|
200
|
+
"next",
|
|
201
|
+
"vinext",
|
|
202
|
+
"astro"
|
|
203
|
+
],
|
|
204
|
+
cssFrameworks: ["tailwind"]
|
|
205
|
+
},
|
|
206
|
+
mantine: {
|
|
207
|
+
frontends: [
|
|
208
|
+
"tanstack-router",
|
|
209
|
+
"react-router",
|
|
210
|
+
"react-vite",
|
|
211
|
+
"tanstack-start",
|
|
212
|
+
"next",
|
|
213
|
+
"vinext",
|
|
214
|
+
"astro"
|
|
215
|
+
],
|
|
216
|
+
cssFrameworks: [
|
|
217
|
+
"tailwind",
|
|
218
|
+
"scss",
|
|
219
|
+
"less",
|
|
220
|
+
"postcss-only",
|
|
221
|
+
"none"
|
|
222
|
+
]
|
|
223
|
+
},
|
|
224
|
+
mui: {
|
|
225
|
+
frontends: [
|
|
226
|
+
"tanstack-router",
|
|
227
|
+
"react-router",
|
|
228
|
+
"react-vite",
|
|
229
|
+
"tanstack-start",
|
|
230
|
+
"next",
|
|
231
|
+
"astro"
|
|
232
|
+
],
|
|
233
|
+
cssFrameworks: [
|
|
234
|
+
"tailwind",
|
|
235
|
+
"scss",
|
|
236
|
+
"less",
|
|
237
|
+
"postcss-only",
|
|
238
|
+
"none"
|
|
239
|
+
]
|
|
240
|
+
},
|
|
241
|
+
antd: {
|
|
242
|
+
frontends: [
|
|
243
|
+
"tanstack-router",
|
|
244
|
+
"react-router",
|
|
245
|
+
"react-vite",
|
|
246
|
+
"tanstack-start",
|
|
247
|
+
"next",
|
|
248
|
+
"astro"
|
|
249
|
+
],
|
|
250
|
+
cssFrameworks: [
|
|
251
|
+
"tailwind",
|
|
252
|
+
"scss",
|
|
253
|
+
"less",
|
|
254
|
+
"postcss-only",
|
|
255
|
+
"none"
|
|
256
|
+
]
|
|
257
|
+
},
|
|
258
|
+
"base-ui": {
|
|
259
|
+
frontends: [
|
|
260
|
+
"tanstack-router",
|
|
261
|
+
"react-router",
|
|
262
|
+
"react-vite",
|
|
263
|
+
"tanstack-start",
|
|
264
|
+
"next",
|
|
265
|
+
"vinext",
|
|
266
|
+
"astro"
|
|
267
|
+
],
|
|
268
|
+
cssFrameworks: [
|
|
269
|
+
"tailwind",
|
|
270
|
+
"scss",
|
|
271
|
+
"less",
|
|
272
|
+
"postcss-only",
|
|
273
|
+
"none"
|
|
274
|
+
]
|
|
275
|
+
},
|
|
276
|
+
"ark-ui": {
|
|
277
|
+
frontends: [
|
|
278
|
+
"tanstack-router",
|
|
279
|
+
"react-router",
|
|
280
|
+
"tanstack-start",
|
|
281
|
+
"next",
|
|
282
|
+
"vinext",
|
|
283
|
+
"nuxt",
|
|
284
|
+
"svelte",
|
|
285
|
+
"solid",
|
|
286
|
+
"solid-start",
|
|
287
|
+
"astro"
|
|
288
|
+
],
|
|
289
|
+
cssFrameworks: [
|
|
290
|
+
"tailwind",
|
|
291
|
+
"scss",
|
|
292
|
+
"less",
|
|
293
|
+
"postcss-only",
|
|
294
|
+
"none"
|
|
295
|
+
]
|
|
296
|
+
},
|
|
297
|
+
"react-aria": {
|
|
298
|
+
frontends: [
|
|
299
|
+
"tanstack-router",
|
|
300
|
+
"react-router",
|
|
301
|
+
"react-vite",
|
|
302
|
+
"tanstack-start",
|
|
303
|
+
"next",
|
|
304
|
+
"vinext",
|
|
305
|
+
"astro"
|
|
306
|
+
],
|
|
307
|
+
cssFrameworks: [
|
|
308
|
+
"tailwind",
|
|
309
|
+
"scss",
|
|
310
|
+
"less",
|
|
311
|
+
"postcss-only",
|
|
312
|
+
"none"
|
|
313
|
+
]
|
|
314
|
+
},
|
|
315
|
+
none: {
|
|
316
|
+
frontends: WEB_FRAMEWORKS,
|
|
317
|
+
cssFrameworks: [
|
|
318
|
+
"tailwind",
|
|
319
|
+
"scss",
|
|
320
|
+
"less",
|
|
321
|
+
"postcss-only",
|
|
322
|
+
"none"
|
|
323
|
+
]
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
const BACKEND_UTILS_COMPATIBLE_BACKENDS = [
|
|
327
|
+
"hono",
|
|
328
|
+
"express",
|
|
329
|
+
"fastify",
|
|
330
|
+
"elysia",
|
|
331
|
+
"fets",
|
|
332
|
+
"nestjs"
|
|
333
|
+
];
|
|
334
|
+
function isBackendUtilsCompatibleBackend(backend) {
|
|
335
|
+
return backend !== void 0 && BACKEND_UTILS_COMPATIBLE_BACKENDS.includes(backend);
|
|
336
|
+
}
|
|
337
|
+
function isExampleAIAllowed(backend, frontends = []) {
|
|
338
|
+
const includesSolid = frontends.includes("solid");
|
|
339
|
+
const includesSolidStart = frontends.includes("solid-start");
|
|
340
|
+
if (includesSolid || includesSolidStart) return false;
|
|
341
|
+
if (backend === "convex") {
|
|
342
|
+
const includesNuxt = frontends.includes("nuxt");
|
|
343
|
+
const includesSvelte = frontends.includes("svelte");
|
|
344
|
+
if (includesNuxt || includesSvelte) return false;
|
|
345
|
+
}
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
function hasExampleChatSdkSelfFrontend(frontends = []) {
|
|
349
|
+
return frontends.some((frontend) => [
|
|
350
|
+
"next",
|
|
351
|
+
"tanstack-start",
|
|
352
|
+
"nuxt"
|
|
353
|
+
].includes(frontend));
|
|
354
|
+
}
|
|
355
|
+
function isExampleChatSdkAllowed(backend, frontends = [], runtime) {
|
|
356
|
+
if (frontends.includes("react-vite")) return false;
|
|
357
|
+
if (!backend || backend === "none" || backend === "convex") return false;
|
|
358
|
+
if (backend === "self") return hasExampleChatSdkSelfFrontend(frontends);
|
|
359
|
+
if (backend === "self-next" || backend === "self-tanstack-start" || backend === "self-nuxt") return true;
|
|
360
|
+
if (backend === "self-astro" || backend === "self-svelte" || backend === "self-solid-start") return false;
|
|
361
|
+
if (backend === "hono") return runtime === "node";
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
function requiresChatSdkVercelAIForExamples(backend, runtime, examples = []) {
|
|
365
|
+
return examples.includes("chat-sdk") && (backend === "self-nuxt" || backend === "hono" && runtime === "node");
|
|
366
|
+
}
|
|
367
|
+
function hasTanStackAICompatibleFrontend(frontends = []) {
|
|
368
|
+
return frontends.some((frontend) => [
|
|
369
|
+
"tanstack-router",
|
|
370
|
+
"react-router",
|
|
371
|
+
"react-vite",
|
|
372
|
+
"tanstack-start",
|
|
373
|
+
"next",
|
|
374
|
+
"redwood",
|
|
375
|
+
"solid",
|
|
376
|
+
"solid-start"
|
|
377
|
+
].includes(frontend));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
//#endregion
|
|
381
|
+
//#region src/stack-graph.ts
|
|
382
|
+
const PRIMARY_ROLES = new Set([
|
|
383
|
+
"frontend",
|
|
384
|
+
"backend",
|
|
385
|
+
"mobile",
|
|
386
|
+
"database"
|
|
387
|
+
]);
|
|
388
|
+
const NATIVE_FRONTENDS = new Set([
|
|
389
|
+
"native-bare",
|
|
390
|
+
"native-uniwind",
|
|
391
|
+
"native-unistyles"
|
|
392
|
+
]);
|
|
393
|
+
const WEB_FRONTENDS = FRONTEND_VALUES.filter((value) => value !== "none" && !NATIVE_FRONTENDS.has(value));
|
|
394
|
+
const DJANGO_API_TOOLS = new Set(["django-rest-framework", "django-ninja"]);
|
|
395
|
+
const JAVA_SPRING_CAPABILITY_TOOLS = new Set(["spring-data-jpa", "spring-security"]);
|
|
396
|
+
const TYPESCRIPT_TRPC_INCOMPATIBLE_FRONTENDS = new Set([
|
|
397
|
+
"nuxt",
|
|
398
|
+
"svelte",
|
|
399
|
+
"solid",
|
|
400
|
+
"solid-start"
|
|
401
|
+
]);
|
|
402
|
+
const BETTER_AUTH_UNSUPPORTED_ORM_TOOLS = new Set([
|
|
403
|
+
"typeorm",
|
|
404
|
+
"mikroorm",
|
|
405
|
+
"sequelize"
|
|
406
|
+
]);
|
|
407
|
+
const ELIXIR_ECTO_REQUIRED_TOOLS = new Set(["absinthe", "phx-gen-auth"]);
|
|
408
|
+
const ELIXIR_ECTO_SQL_REQUIRED_TOOLS = new Set(["oban"]);
|
|
409
|
+
const ELIXIR_PHOENIX_REQUIRED_ROLE_MESSAGES = {
|
|
410
|
+
auth: "Elixir auth scaffolds require Phoenix",
|
|
411
|
+
api: "Elixir API scaffolds require Phoenix",
|
|
412
|
+
realtime: "Elixir realtime scaffolds require Phoenix"
|
|
413
|
+
};
|
|
414
|
+
const ELIXIR_UNSUPPORTED_GRAPH_TOOL_MESSAGES = {
|
|
415
|
+
ecto: "Use Ecto SQL for generated Repo, migrations, schemas, and PostgreSQL wiring",
|
|
416
|
+
ueberauth: "Ueberauth is not generated yet; use phx.gen.auth or no auth",
|
|
417
|
+
guardian: "Guardian JWT wiring is not generated yet; use phx.gen.auth or no auth",
|
|
418
|
+
"nimble-options": "NimbleOptions is not generated yet; use Ecto Changesets or no extra validation",
|
|
419
|
+
nebulex: "Nebulex cache modules are not generated yet; use Cachex or no cache",
|
|
420
|
+
opentelemetry: "OpenTelemetry setup is not generated yet; use Phoenix telemetry or no extra observability",
|
|
421
|
+
prom_ex: "PromEx setup is not generated yet; use Phoenix telemetry or no extra observability",
|
|
422
|
+
mox: "Mox-specific test boundaries are not generated yet; use ExUnit",
|
|
423
|
+
bypass: "Bypass-specific HTTP tests are not generated yet; use ExUnit",
|
|
424
|
+
wallaby: "Wallaby browser tests are not generated yet; use ExUnit",
|
|
425
|
+
fly: "Fly.io config is not generated yet; use Docker or mix releases",
|
|
426
|
+
gigalixir: "Gigalixir config is not generated yet; use Docker or mix releases"
|
|
427
|
+
};
|
|
428
|
+
const ELIXIR_UNSUPPORTED_GRAPH_TOOLS = new Set(Object.keys(ELIXIR_UNSUPPORTED_GRAPH_TOOL_MESSAGES));
|
|
429
|
+
const ROLE_TARGET_PATHS = {
|
|
430
|
+
frontend: "apps/web",
|
|
431
|
+
backend: "apps/server",
|
|
432
|
+
mobile: "apps/native",
|
|
433
|
+
database: "packages/db"
|
|
434
|
+
};
|
|
435
|
+
const LEGACY_BACKEND_CATEGORY_BY_ECOSYSTEM = {
|
|
436
|
+
rust: "rustWebFramework",
|
|
437
|
+
python: "pythonWebFramework",
|
|
438
|
+
go: "goWebFramework",
|
|
439
|
+
java: "javaWebFramework",
|
|
440
|
+
dotnet: "dotnetWebFramework",
|
|
441
|
+
elixir: "elixirWebFramework"
|
|
442
|
+
};
|
|
443
|
+
const LEGACY_CAPABILITY_CATEGORIES_BY_ECOSYSTEM = {
|
|
444
|
+
rust: {
|
|
445
|
+
orm: "rustOrm",
|
|
446
|
+
api: "rustApi",
|
|
447
|
+
auth: "rustAuth"
|
|
448
|
+
},
|
|
449
|
+
python: {
|
|
450
|
+
orm: "pythonOrm",
|
|
451
|
+
api: "pythonApi",
|
|
452
|
+
auth: "pythonAuth"
|
|
453
|
+
},
|
|
454
|
+
go: {
|
|
455
|
+
orm: "goOrm",
|
|
456
|
+
api: "goApi",
|
|
457
|
+
auth: "goAuth"
|
|
458
|
+
},
|
|
459
|
+
java: {
|
|
460
|
+
orm: "javaOrm",
|
|
461
|
+
api: "javaApi",
|
|
462
|
+
auth: "javaAuth"
|
|
463
|
+
},
|
|
464
|
+
dotnet: {
|
|
465
|
+
orm: "dotnetOrm",
|
|
466
|
+
api: "dotnetApi",
|
|
467
|
+
auth: "dotnetAuth"
|
|
468
|
+
},
|
|
469
|
+
elixir: {
|
|
470
|
+
orm: "elixirOrm",
|
|
471
|
+
api: "elixirApi",
|
|
472
|
+
auth: "elixirAuth"
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
const LEGACY_TYPESCRIPT_BACKEND_SINGLE_CATEGORIES = {
|
|
476
|
+
logging: "logging",
|
|
477
|
+
email: "email",
|
|
478
|
+
search: "search",
|
|
479
|
+
caching: "caching",
|
|
480
|
+
observability: "observability",
|
|
481
|
+
jobQueue: "jobQueue",
|
|
482
|
+
rateLimit: "rateLimit",
|
|
483
|
+
fileStorage: "fileStorage",
|
|
484
|
+
featureFlags: "featureFlags",
|
|
485
|
+
payments: "payments",
|
|
486
|
+
realtime: "realtime",
|
|
487
|
+
ai: "ai",
|
|
488
|
+
cms: "cms",
|
|
489
|
+
validation: "validation",
|
|
490
|
+
effect: "effect"
|
|
491
|
+
};
|
|
492
|
+
const LEGACY_TYPESCRIPT_BACKEND_INFRA_CATEGORIES = {
|
|
493
|
+
runtime: "runtime",
|
|
494
|
+
deploy: "serverDeploy"
|
|
495
|
+
};
|
|
496
|
+
const LEGACY_TYPESCRIPT_FRONTEND_SINGLE_CATEGORIES = {
|
|
497
|
+
deploy: "webDeploy",
|
|
498
|
+
css: "cssFramework",
|
|
499
|
+
ui: "uiLibrary",
|
|
500
|
+
forms: "forms",
|
|
501
|
+
stateManagement: "stateManagement",
|
|
502
|
+
animation: "animation",
|
|
503
|
+
fileUpload: "fileUpload",
|
|
504
|
+
i18n: "i18n",
|
|
505
|
+
analytics: "analytics"
|
|
506
|
+
};
|
|
507
|
+
const LEGACY_DATABASE_SINGLE_CATEGORIES = { dbSetup: "dbSetup" };
|
|
508
|
+
const LEGACY_MOBILE_SINGLE_CATEGORIES = {
|
|
509
|
+
navigation: "mobileNavigation",
|
|
510
|
+
ui: "mobileUI",
|
|
511
|
+
storage: "mobileStorage",
|
|
512
|
+
testing: "mobileTesting"
|
|
513
|
+
};
|
|
514
|
+
const LEGACY_BACKEND_ARRAY_CATEGORIES_BY_ECOSYSTEM = {
|
|
515
|
+
rust: { libraries: "rustLibraries" },
|
|
516
|
+
python: {
|
|
517
|
+
ai: "pythonAi",
|
|
518
|
+
testing: "pythonTesting",
|
|
519
|
+
cli: "pythonCli"
|
|
520
|
+
},
|
|
521
|
+
go: { testing: "goTesting" },
|
|
522
|
+
java: {
|
|
523
|
+
libraries: "javaLibraries",
|
|
524
|
+
testing: "javaTestingLibraries"
|
|
525
|
+
},
|
|
526
|
+
dotnet: {
|
|
527
|
+
testing: "dotnetTesting",
|
|
528
|
+
observability: "dotnetObservability"
|
|
529
|
+
},
|
|
530
|
+
elixir: { libraries: "elixirLibraries" }
|
|
531
|
+
};
|
|
532
|
+
const CODE_QUALITY_ADDONS = new Set([
|
|
533
|
+
"biome",
|
|
534
|
+
"oxlint",
|
|
535
|
+
"ultracite",
|
|
536
|
+
"lefthook",
|
|
537
|
+
"husky"
|
|
538
|
+
]);
|
|
539
|
+
const DOCUMENTATION_ADDONS = new Set(["starlight", "fumadocs"]);
|
|
540
|
+
const FRONTEND_APP_PLATFORM_ADDONS = new Set([
|
|
541
|
+
"pwa",
|
|
542
|
+
"tauri",
|
|
543
|
+
"wxt",
|
|
544
|
+
"opentui"
|
|
545
|
+
]);
|
|
546
|
+
const FRONTEND_DATA_FETCHING_ADDONS = new Set([
|
|
547
|
+
"swr",
|
|
548
|
+
"tanstack-query",
|
|
549
|
+
"tanstack-table",
|
|
550
|
+
"tanstack-virtual",
|
|
551
|
+
"tanstack-db",
|
|
552
|
+
"tanstack-pacer"
|
|
553
|
+
]);
|
|
554
|
+
const FRONTEND_TESTING_ADDONS = new Set(["msw", "storybook"]);
|
|
555
|
+
const WORKSPACE_TOOLING_ADDONS = new Set([
|
|
556
|
+
"turborepo",
|
|
557
|
+
"docker-compose",
|
|
558
|
+
"ruler",
|
|
559
|
+
"mcp",
|
|
560
|
+
"skills",
|
|
561
|
+
"backend-utils"
|
|
562
|
+
]);
|
|
563
|
+
const LEGACY_ADDON_GRAPH_ROLES = new Set([
|
|
564
|
+
"appPlatform",
|
|
565
|
+
"codeQuality",
|
|
566
|
+
"dataFetching",
|
|
567
|
+
"documentation",
|
|
568
|
+
"testing",
|
|
569
|
+
"workspaceTooling"
|
|
570
|
+
]);
|
|
571
|
+
const LEGACY_ARRAY_CATEGORIES = new Set([
|
|
572
|
+
"addons",
|
|
573
|
+
"examples",
|
|
574
|
+
"rustLibraries",
|
|
575
|
+
"pythonAi",
|
|
576
|
+
"javaLibraries",
|
|
577
|
+
"javaTestingLibraries",
|
|
578
|
+
"dotnetTesting",
|
|
579
|
+
"dotnetObservability",
|
|
580
|
+
"goTesting",
|
|
581
|
+
"pythonTesting",
|
|
582
|
+
"pythonCli",
|
|
583
|
+
"elixirLibraries"
|
|
584
|
+
]);
|
|
585
|
+
function getAddonStackPartBinding(toolId) {
|
|
586
|
+
if (CODE_QUALITY_ADDONS.has(toolId)) return {
|
|
587
|
+
role: "codeQuality",
|
|
588
|
+
ecosystem: "universal"
|
|
589
|
+
};
|
|
590
|
+
if (DOCUMENTATION_ADDONS.has(toolId)) return {
|
|
591
|
+
role: "documentation",
|
|
592
|
+
ecosystem: "universal"
|
|
593
|
+
};
|
|
594
|
+
if (FRONTEND_APP_PLATFORM_ADDONS.has(toolId)) return {
|
|
595
|
+
role: "appPlatform",
|
|
596
|
+
ecosystem: "typescript",
|
|
597
|
+
ownerRole: "frontend"
|
|
598
|
+
};
|
|
599
|
+
if (FRONTEND_DATA_FETCHING_ADDONS.has(toolId)) return {
|
|
600
|
+
role: "dataFetching",
|
|
601
|
+
ecosystem: "typescript",
|
|
602
|
+
ownerRole: "frontend"
|
|
603
|
+
};
|
|
604
|
+
if (FRONTEND_TESTING_ADDONS.has(toolId)) return {
|
|
605
|
+
role: "testing",
|
|
606
|
+
ecosystem: "typescript",
|
|
607
|
+
ownerRole: "frontend"
|
|
608
|
+
};
|
|
609
|
+
if (WORKSPACE_TOOLING_ADDONS.has(toolId)) return {
|
|
610
|
+
role: "workspaceTooling",
|
|
611
|
+
ecosystem: "universal"
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
const OWNER_ROLES_BY_SCOPED_ROLE = {
|
|
615
|
+
...Object.fromEntries([...Object.keys(LEGACY_TYPESCRIPT_BACKEND_SINGLE_CATEGORIES), ...Object.keys(LEGACY_TYPESCRIPT_BACKEND_INFRA_CATEGORIES)].map((role) => [role, ["backend"]])),
|
|
616
|
+
...Object.fromEntries(Object.keys(LEGACY_TYPESCRIPT_FRONTEND_SINGLE_CATEGORIES).map((role) => [role, ["frontend"]])),
|
|
617
|
+
...Object.fromEntries(Object.keys(LEGACY_MOBILE_SINGLE_CATEGORIES).map((role) => [role, ["mobile"]])),
|
|
618
|
+
deploy: ["frontend", "backend"],
|
|
619
|
+
dbSetup: ["database"],
|
|
620
|
+
ui: ["frontend", "mobile"],
|
|
621
|
+
appPlatform: ["frontend"],
|
|
622
|
+
dataFetching: ["frontend"],
|
|
623
|
+
testing: [
|
|
624
|
+
"frontend",
|
|
625
|
+
"mobile",
|
|
626
|
+
"backend"
|
|
627
|
+
],
|
|
628
|
+
codeQuality: ["backend"],
|
|
629
|
+
buildTool: ["backend"],
|
|
630
|
+
cli: ["backend"],
|
|
631
|
+
errorHandling: ["backend"],
|
|
632
|
+
httpClient: ["backend"],
|
|
633
|
+
libraries: ["backend"]
|
|
634
|
+
};
|
|
635
|
+
const FRESH_UNSUPPORTED_STATE_MANAGEMENT_TOOLS = new Set([
|
|
636
|
+
"nanostores",
|
|
637
|
+
"xstate",
|
|
638
|
+
"tanstack-store"
|
|
639
|
+
]);
|
|
640
|
+
const RUNTIMELESS_TYPESCRIPT_BACKENDS = new Set([
|
|
641
|
+
"convex",
|
|
642
|
+
"none",
|
|
643
|
+
"self",
|
|
644
|
+
"self-next",
|
|
645
|
+
"self-vinext",
|
|
646
|
+
"self-tanstack-start",
|
|
647
|
+
"self-astro",
|
|
648
|
+
"self-nuxt",
|
|
649
|
+
"self-svelte",
|
|
650
|
+
"self-solid-start"
|
|
651
|
+
]);
|
|
652
|
+
const NO_SERVER_DEPLOY_TYPESCRIPT_BACKENDS = new Set([
|
|
653
|
+
"none",
|
|
654
|
+
"convex",
|
|
655
|
+
"self",
|
|
656
|
+
"self-next",
|
|
657
|
+
"self-vinext",
|
|
658
|
+
"self-tanstack-start",
|
|
659
|
+
"self-astro",
|
|
660
|
+
"self-nuxt",
|
|
661
|
+
"self-svelte",
|
|
662
|
+
"self-solid-start"
|
|
663
|
+
]);
|
|
664
|
+
const SHARED_NON_TYPESCRIPT_BACKEND_SERVICE_RULES = {
|
|
665
|
+
email: {
|
|
666
|
+
allowedToolId: "resend",
|
|
667
|
+
unsupportedToolMessage: "Only Resend email is available for non-TypeScript ecosystems",
|
|
668
|
+
javaNoBuildToolMessage: "Resend email for Java requires Maven or Gradle to manage the SDK dependency"
|
|
669
|
+
},
|
|
670
|
+
observability: {
|
|
671
|
+
allowedToolId: "sentry",
|
|
672
|
+
unsupportedToolMessage: "Only Sentry observability is available for non-TypeScript ecosystems",
|
|
673
|
+
javaNoBuildToolMessage: "Sentry observability for Java requires Maven or Gradle to manage the SDK dependency"
|
|
674
|
+
},
|
|
675
|
+
caching: {
|
|
676
|
+
allowedToolId: "upstash-redis",
|
|
677
|
+
unsupportedToolMessage: "Only Upstash Redis caching is available for non-TypeScript ecosystems",
|
|
678
|
+
javaNoBuildToolMessage: "Upstash Redis caching for Java requires Maven or Gradle to manage the Redis client dependency"
|
|
679
|
+
},
|
|
680
|
+
search: {
|
|
681
|
+
allowedToolId: "meilisearch",
|
|
682
|
+
unsupportedToolMessage: "Only Meilisearch search is available for non-TypeScript ecosystems",
|
|
683
|
+
javaNoBuildToolMessage: "Meilisearch search for Java requires Maven or Gradle to manage the SDK dependency"
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
function isNativeEcosystemBackendServiceTool(part) {
|
|
687
|
+
if (part.role === "email" && part.ecosystem === "elixir") return ELIXIR_EMAIL_VALUES.includes(part.toolId);
|
|
688
|
+
if (part.role === "caching") {
|
|
689
|
+
if (part.ecosystem === "elixir") return ELIXIR_CACHING_VALUES.includes(part.toolId);
|
|
690
|
+
if (part.ecosystem === "rust") return RUST_CACHING_VALUES.includes(part.toolId);
|
|
691
|
+
if (part.ecosystem === "dotnet") return DOTNET_CACHING_VALUES.includes(part.toolId);
|
|
692
|
+
if (part.ecosystem === "go") return GO_CACHING_VALUES.includes(part.toolId);
|
|
693
|
+
if (part.ecosystem === "python") return PYTHON_CACHING_VALUES.includes(part.toolId);
|
|
694
|
+
}
|
|
695
|
+
if (part.role === "observability") {
|
|
696
|
+
if (part.ecosystem === "elixir") return ELIXIR_OBSERVABILITY_VALUES.includes(part.toolId);
|
|
697
|
+
if (part.ecosystem === "dotnet") return DOTNET_OBSERVABILITY_VALUES.includes(part.toolId);
|
|
698
|
+
if (part.ecosystem === "go") return GO_OBSERVABILITY_VALUES.includes(part.toolId);
|
|
699
|
+
if (part.ecosystem === "python") return PYTHON_OBSERVABILITY_VALUES.includes(part.toolId);
|
|
700
|
+
if (part.ecosystem === "rust") return RUST_OBSERVABILITY_VALUES.includes(part.toolId);
|
|
701
|
+
}
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
const LEGACY_EXTRA_CATEGORIES_BY_ECOSYSTEM = {
|
|
705
|
+
rust: {
|
|
706
|
+
caching: "rustCaching",
|
|
707
|
+
cli: "rustCli",
|
|
708
|
+
logging: "rustLogging",
|
|
709
|
+
errorHandling: "rustErrorHandling",
|
|
710
|
+
realtime: "rustRealtime",
|
|
711
|
+
jobQueue: "rustMessageQueue",
|
|
712
|
+
observability: "rustObservability",
|
|
713
|
+
templating: "rustTemplating"
|
|
714
|
+
},
|
|
715
|
+
python: {
|
|
716
|
+
validation: "pythonValidation",
|
|
717
|
+
jobQueue: "pythonTaskQueue",
|
|
718
|
+
api: "pythonGraphql",
|
|
719
|
+
codeQuality: "pythonQuality",
|
|
720
|
+
caching: "pythonCaching",
|
|
721
|
+
realtime: "pythonRealtime",
|
|
722
|
+
observability: "pythonObservability"
|
|
723
|
+
},
|
|
724
|
+
go: {
|
|
725
|
+
cli: "goCli",
|
|
726
|
+
logging: "goLogging",
|
|
727
|
+
realtime: "goRealtime",
|
|
728
|
+
jobQueue: "goMessageQueue",
|
|
729
|
+
caching: "goCaching",
|
|
730
|
+
config: "goConfig",
|
|
731
|
+
observability: "goObservability"
|
|
732
|
+
},
|
|
733
|
+
java: {
|
|
734
|
+
buildTool: "javaBuildTool",
|
|
735
|
+
logging: "javaLogging"
|
|
736
|
+
},
|
|
737
|
+
dotnet: {
|
|
738
|
+
jobQueue: "dotnetJobQueue",
|
|
739
|
+
realtime: "dotnetRealtime",
|
|
740
|
+
caching: "dotnetCaching",
|
|
741
|
+
deploy: "dotnetDeploy",
|
|
742
|
+
validation: "dotnetValidation"
|
|
743
|
+
},
|
|
744
|
+
elixir: {
|
|
745
|
+
realtime: "elixirRealtime",
|
|
746
|
+
jobQueue: "elixirJobs",
|
|
747
|
+
validation: "elixirValidation",
|
|
748
|
+
httpClient: "elixirHttp",
|
|
749
|
+
email: "elixirEmail",
|
|
750
|
+
caching: "elixirCaching",
|
|
751
|
+
observability: "elixirObservability",
|
|
752
|
+
testing: "elixirTesting",
|
|
753
|
+
codeQuality: "elixirQuality",
|
|
754
|
+
deploy: "elixirDeploy"
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
const GRAPH_PROJECTION_DEFAULT_LEGACY_CATEGORIES = [
|
|
758
|
+
"backend",
|
|
759
|
+
"database",
|
|
760
|
+
"orm",
|
|
761
|
+
"api",
|
|
762
|
+
"auth",
|
|
763
|
+
"rustFrontend",
|
|
764
|
+
...Object.values(LEGACY_BACKEND_CATEGORY_BY_ECOSYSTEM),
|
|
765
|
+
...Object.values(LEGACY_CAPABILITY_CATEGORIES_BY_ECOSYSTEM).flatMap((categories) => Object.values(categories)),
|
|
766
|
+
...Object.values(LEGACY_TYPESCRIPT_BACKEND_SINGLE_CATEGORIES),
|
|
767
|
+
...Object.values(LEGACY_TYPESCRIPT_BACKEND_INFRA_CATEGORIES),
|
|
768
|
+
...Object.values(LEGACY_TYPESCRIPT_FRONTEND_SINGLE_CATEGORIES),
|
|
769
|
+
...Object.values(LEGACY_DATABASE_SINGLE_CATEGORIES),
|
|
770
|
+
...Object.values(LEGACY_MOBILE_SINGLE_CATEGORIES),
|
|
771
|
+
...Object.values(LEGACY_EXTRA_CATEGORIES_BY_ECOSYSTEM).flatMap((categories) => Object.values(categories))
|
|
772
|
+
];
|
|
773
|
+
function defineTools(values, role, ecosystem, legacyCategory, options = {}) {
|
|
774
|
+
return values.filter((toolId) => toolId !== "none").map((toolId) => {
|
|
775
|
+
const definition = {
|
|
776
|
+
toolId,
|
|
777
|
+
roles: [role],
|
|
778
|
+
ecosystems: [ecosystem],
|
|
779
|
+
legacyCategory
|
|
780
|
+
};
|
|
781
|
+
if (options.allowMultiple !== void 0) definition.allowMultiple = options.allowMultiple;
|
|
782
|
+
if (options.ownerless !== void 0) definition.ownerless = options.ownerless;
|
|
783
|
+
return definition;
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
const STACK_TOOL_DEFINITIONS = [
|
|
787
|
+
...defineTools(WEB_FRONTENDS, "frontend", "typescript", "frontend"),
|
|
788
|
+
...defineTools([...NATIVE_FRONTENDS], "mobile", "react-native", "frontend"),
|
|
789
|
+
...defineTools(BACKEND_VALUES.filter((value) => value !== "convex"), "backend", "typescript", "backend"),
|
|
790
|
+
...defineTools(DATABASE_VALUES, "database", "universal", "database"),
|
|
791
|
+
...defineTools(DATABASE_VALUES, "database", "typescript", "database"),
|
|
792
|
+
...defineTools(DATABASE_SETUP_VALUES, "dbSetup", "universal", "dbSetup"),
|
|
793
|
+
...defineTools(RUNTIME_VALUES, "runtime", "typescript", "runtime"),
|
|
794
|
+
...defineTools(WEB_DEPLOY_VALUES, "deploy", "typescript"),
|
|
795
|
+
...defineTools(SERVER_DEPLOY_VALUES, "deploy", "typescript"),
|
|
796
|
+
...defineTools(ORM_VALUES, "orm", "typescript", "orm"),
|
|
797
|
+
...defineTools(API_VALUES, "api", "typescript", "api"),
|
|
798
|
+
...defineTools(AUTH_VALUES, "auth", "typescript", "auth"),
|
|
799
|
+
...defineTools(CSS_FRAMEWORK_VALUES, "css", "typescript", "cssFramework"),
|
|
800
|
+
...defineTools(UI_LIBRARY_VALUES, "ui", "typescript", "uiLibrary"),
|
|
801
|
+
...defineTools(FORMS_VALUES, "forms", "typescript", "forms"),
|
|
802
|
+
...defineTools(STATE_MANAGEMENT_VALUES, "stateManagement", "typescript", "stateManagement"),
|
|
803
|
+
...defineTools(ANIMATION_VALUES, "animation", "typescript", "animation"),
|
|
804
|
+
...defineTools(FILE_UPLOAD_VALUES, "fileUpload", "typescript", "fileUpload"),
|
|
805
|
+
...defineTools(I18N_VALUES, "i18n", "typescript", "i18n"),
|
|
806
|
+
...defineTools(ANALYTICS_VALUES, "analytics", "typescript", "analytics"),
|
|
807
|
+
...defineTools(ADDONS_VALUES.filter((value) => CODE_QUALITY_ADDONS.has(value)), "codeQuality", "universal", void 0, {
|
|
808
|
+
allowMultiple: true,
|
|
809
|
+
ownerless: true
|
|
810
|
+
}),
|
|
811
|
+
...defineTools(ADDONS_VALUES.filter((value) => DOCUMENTATION_ADDONS.has(value)), "documentation", "universal", void 0, {
|
|
812
|
+
allowMultiple: true,
|
|
813
|
+
ownerless: true
|
|
814
|
+
}),
|
|
815
|
+
...defineTools(ADDONS_VALUES.filter((value) => FRONTEND_APP_PLATFORM_ADDONS.has(value)), "appPlatform", "typescript", void 0, { allowMultiple: true }),
|
|
816
|
+
...defineTools(ADDONS_VALUES.filter((value) => FRONTEND_DATA_FETCHING_ADDONS.has(value)), "dataFetching", "typescript", void 0, { allowMultiple: true }),
|
|
817
|
+
...defineTools(ADDONS_VALUES.filter((value) => FRONTEND_TESTING_ADDONS.has(value)), "testing", "typescript", void 0, { allowMultiple: true }),
|
|
818
|
+
...defineTools(ADDONS_VALUES.filter((value) => WORKSPACE_TOOLING_ADDONS.has(value)), "workspaceTooling", "universal", void 0, {
|
|
819
|
+
allowMultiple: true,
|
|
820
|
+
ownerless: true
|
|
821
|
+
}),
|
|
822
|
+
...defineTools(EXAMPLES_VALUES, "examples", "universal", void 0, {
|
|
823
|
+
allowMultiple: true,
|
|
824
|
+
ownerless: true
|
|
825
|
+
}),
|
|
826
|
+
...defineTools(LOGGING_VALUES, "logging", "typescript", "logging"),
|
|
827
|
+
...defineTools(EMAIL_VALUES, "email", "typescript", "email"),
|
|
828
|
+
...defineTools(SEARCH_VALUES, "search", "typescript", "search"),
|
|
829
|
+
...defineTools(CACHING_VALUES, "caching", "typescript", "caching"),
|
|
830
|
+
...defineTools(OBSERVABILITY_VALUES, "observability", "typescript", "observability"),
|
|
831
|
+
...defineTools(JOB_QUEUE_VALUES, "jobQueue", "typescript", "jobQueue"),
|
|
832
|
+
...defineTools(RATE_LIMIT_VALUES, "rateLimit", "typescript", "rateLimit"),
|
|
833
|
+
...defineTools(FILE_STORAGE_VALUES, "fileStorage", "typescript", "fileStorage"),
|
|
834
|
+
...defineTools(FEATURE_FLAGS_VALUES, "featureFlags", "typescript", "featureFlags"),
|
|
835
|
+
...defineTools(PAYMENTS_VALUES, "payments", "typescript", "payments"),
|
|
836
|
+
...defineTools(REALTIME_VALUES, "realtime", "typescript", "realtime"),
|
|
837
|
+
...defineTools(AI_VALUES, "ai", "typescript", "ai"),
|
|
838
|
+
...defineTools(CMS_VALUES, "cms", "typescript", "cms"),
|
|
839
|
+
...defineTools(VALIDATION_VALUES, "validation", "typescript", "validation"),
|
|
840
|
+
...defineTools(EFFECT_VALUES, "effect", "typescript", "effect"),
|
|
841
|
+
...defineTools(AUTH_VALUES, "auth", "react-native", "auth"),
|
|
842
|
+
...defineTools(MOBILE_NAVIGATION_VALUES, "navigation", "react-native", "mobileNavigation"),
|
|
843
|
+
...defineTools(MOBILE_UI_VALUES, "ui", "react-native", "mobileUI"),
|
|
844
|
+
...defineTools(MOBILE_STORAGE_VALUES, "storage", "react-native", "mobileStorage"),
|
|
845
|
+
...defineTools(MOBILE_TESTING_VALUES, "testing", "react-native", "mobileTesting"),
|
|
846
|
+
...defineTools(RUST_WEB_FRAMEWORK_VALUES, "backend", "rust", "rustWebFramework"),
|
|
847
|
+
...defineTools(RUST_FRONTEND_VALUES, "frontend", "rust", "rustFrontend"),
|
|
848
|
+
...defineTools(RUST_ORM_VALUES, "orm", "rust", "rustOrm"),
|
|
849
|
+
...defineTools(RUST_API_VALUES, "api", "rust", "rustApi"),
|
|
850
|
+
...defineTools(RUST_AUTH_VALUES, "auth", "rust", "rustAuth"),
|
|
851
|
+
...defineTools(RUST_REALTIME_VALUES, "realtime", "rust", "rustRealtime"),
|
|
852
|
+
...defineTools(RUST_MESSAGE_QUEUE_VALUES, "jobQueue", "rust", "rustMessageQueue"),
|
|
853
|
+
...defineTools(RUST_OBSERVABILITY_VALUES, "observability", "rust", "rustObservability"),
|
|
854
|
+
...defineTools(RUST_TEMPLATING_VALUES, "templating", "rust", "rustTemplating"),
|
|
855
|
+
...defineTools(RUST_CLI_VALUES, "cli", "rust", "rustCli"),
|
|
856
|
+
...defineTools(RUST_LIBRARIES_VALUES, "libraries", "rust", "rustLibraries", { allowMultiple: true }),
|
|
857
|
+
...defineTools(RUST_LOGGING_VALUES, "logging", "rust", "rustLogging"),
|
|
858
|
+
...defineTools(RUST_ERROR_HANDLING_VALUES, "errorHandling", "rust", "rustErrorHandling"),
|
|
859
|
+
...defineTools(RUST_CACHING_VALUES, "caching", "rust", "rustCaching"),
|
|
860
|
+
...defineTools(PYTHON_WEB_FRAMEWORK_VALUES, "backend", "python", "pythonWebFramework"),
|
|
861
|
+
...defineTools(PYTHON_ORM_VALUES, "orm", "python", "pythonOrm"),
|
|
862
|
+
...defineTools(PYTHON_VALIDATION_VALUES, "validation", "python", "pythonValidation"),
|
|
863
|
+
...defineTools(PYTHON_AI_VALUES, "ai", "python", "pythonAi", { allowMultiple: true }),
|
|
864
|
+
...defineTools(PYTHON_API_VALUES, "api", "python", "pythonApi"),
|
|
865
|
+
...defineTools(PYTHON_AUTH_VALUES, "auth", "python", "pythonAuth"),
|
|
866
|
+
...defineTools(PYTHON_TASK_QUEUE_VALUES, "jobQueue", "python", "pythonTaskQueue"),
|
|
867
|
+
...defineTools(PYTHON_GRAPHQL_VALUES, "api", "python", "pythonGraphql"),
|
|
868
|
+
...defineTools(PYTHON_QUALITY_VALUES, "codeQuality", "python", "pythonQuality"),
|
|
869
|
+
...defineTools(PYTHON_TESTING_VALUES, "testing", "python", "pythonTesting", { allowMultiple: true }),
|
|
870
|
+
...defineTools(PYTHON_CACHING_VALUES, "caching", "python", "pythonCaching"),
|
|
871
|
+
...defineTools(PYTHON_REALTIME_VALUES, "realtime", "python", "pythonRealtime"),
|
|
872
|
+
...defineTools(PYTHON_OBSERVABILITY_VALUES, "observability", "python", "pythonObservability"),
|
|
873
|
+
...defineTools(PYTHON_CLI_VALUES, "cli", "python", "pythonCli", { allowMultiple: true }),
|
|
874
|
+
...defineTools(GO_WEB_FRAMEWORK_VALUES, "backend", "go", "goWebFramework"),
|
|
875
|
+
...defineTools(GO_ORM_VALUES, "orm", "go", "goOrm"),
|
|
876
|
+
...defineTools(GO_API_VALUES, "api", "go", "goApi"),
|
|
877
|
+
...defineTools(GO_AUTH_VALUES, "auth", "go", "goAuth"),
|
|
878
|
+
...defineTools(GO_CLI_VALUES, "cli", "go", "goCli"),
|
|
879
|
+
...defineTools(GO_LOGGING_VALUES, "logging", "go", "goLogging"),
|
|
880
|
+
...defineTools(GO_TESTING_VALUES, "testing", "go", "goTesting", { allowMultiple: true }),
|
|
881
|
+
...defineTools(GO_REALTIME_VALUES, "realtime", "go", "goRealtime"),
|
|
882
|
+
...defineTools(GO_MESSAGE_QUEUE_VALUES, "jobQueue", "go", "goMessageQueue"),
|
|
883
|
+
...defineTools(GO_CACHING_VALUES, "caching", "go", "goCaching"),
|
|
884
|
+
...defineTools(GO_CONFIG_VALUES, "config", "go", "goConfig"),
|
|
885
|
+
...defineTools(GO_OBSERVABILITY_VALUES, "observability", "go", "goObservability"),
|
|
886
|
+
...defineTools(JAVA_WEB_FRAMEWORK_VALUES, "backend", "java", "javaWebFramework"),
|
|
887
|
+
...defineTools(JAVA_BUILD_TOOL_VALUES, "buildTool", "java", "javaBuildTool"),
|
|
888
|
+
...defineTools(JAVA_ORM_VALUES, "orm", "java", "javaOrm"),
|
|
889
|
+
...defineTools(JAVA_AUTH_VALUES, "auth", "java", "javaAuth"),
|
|
890
|
+
...defineTools(JAVA_API_VALUES, "api", "java", "javaApi"),
|
|
891
|
+
...defineTools(JAVA_LOGGING_VALUES, "logging", "java", "javaLogging"),
|
|
892
|
+
...defineTools(JAVA_LIBRARIES_VALUES, "libraries", "java", "javaLibraries", { allowMultiple: true }),
|
|
893
|
+
...defineTools(JAVA_TESTING_LIBRARIES_VALUES, "testing", "java", "javaTestingLibraries", { allowMultiple: true }),
|
|
894
|
+
...defineTools(DOTNET_WEB_FRAMEWORK_VALUES, "backend", "dotnet", "dotnetWebFramework"),
|
|
895
|
+
...defineTools(DOTNET_ORM_VALUES, "orm", "dotnet", "dotnetOrm"),
|
|
896
|
+
...defineTools(DOTNET_AUTH_VALUES, "auth", "dotnet", "dotnetAuth"),
|
|
897
|
+
...defineTools(DOTNET_API_VALUES, "api", "dotnet", "dotnetApi"),
|
|
898
|
+
...defineTools(DOTNET_TESTING_VALUES, "testing", "dotnet", "dotnetTesting", { allowMultiple: true }),
|
|
899
|
+
...defineTools(DOTNET_JOB_QUEUE_VALUES, "jobQueue", "dotnet", "dotnetJobQueue"),
|
|
900
|
+
...defineTools(DOTNET_REALTIME_VALUES, "realtime", "dotnet", "dotnetRealtime"),
|
|
901
|
+
...defineTools(DOTNET_OBSERVABILITY_VALUES, "observability", "dotnet", "dotnetObservability", { allowMultiple: true }),
|
|
902
|
+
...defineTools(DOTNET_VALIDATION_VALUES, "validation", "dotnet", "dotnetValidation"),
|
|
903
|
+
...defineTools(DOTNET_CACHING_VALUES, "caching", "dotnet", "dotnetCaching"),
|
|
904
|
+
...defineTools(DOTNET_DEPLOY_VALUES, "deploy", "dotnet", "dotnetDeploy"),
|
|
905
|
+
...defineTools(ELIXIR_WEB_FRAMEWORK_VALUES, "backend", "elixir", "elixirWebFramework"),
|
|
906
|
+
...defineTools(ELIXIR_ORM_VALUES, "orm", "elixir", "elixirOrm"),
|
|
907
|
+
...defineTools(ELIXIR_AUTH_VALUES, "auth", "elixir", "elixirAuth"),
|
|
908
|
+
...defineTools(ELIXIR_API_VALUES, "api", "elixir", "elixirApi"),
|
|
909
|
+
...defineTools(ELIXIR_LIBRARIES_VALUES, "libraries", "elixir", "elixirLibraries", { allowMultiple: true }),
|
|
910
|
+
...defineTools(ELIXIR_REALTIME_VALUES, "realtime", "elixir", "elixirRealtime"),
|
|
911
|
+
...defineTools(ELIXIR_JOBS_VALUES, "jobQueue", "elixir", "elixirJobs"),
|
|
912
|
+
...defineTools(ELIXIR_VALIDATION_VALUES, "validation", "elixir", "elixirValidation"),
|
|
913
|
+
...defineTools(ELIXIR_HTTP_VALUES, "httpClient", "elixir", "elixirHttp"),
|
|
914
|
+
...defineTools(ELIXIR_EMAIL_VALUES, "email", "elixir", "elixirEmail"),
|
|
915
|
+
...defineTools(ELIXIR_CACHING_VALUES, "caching", "elixir", "elixirCaching"),
|
|
916
|
+
...defineTools(ELIXIR_OBSERVABILITY_VALUES, "observability", "elixir", "elixirObservability"),
|
|
917
|
+
...defineTools(ELIXIR_TESTING_VALUES, "testing", "elixir", "elixirTesting"),
|
|
918
|
+
...defineTools(ELIXIR_QUALITY_VALUES, "codeQuality", "elixir", "elixirQuality"),
|
|
919
|
+
...defineTools(ELIXIR_DEPLOY_VALUES, "deploy", "elixir", "elixirDeploy"),
|
|
920
|
+
{
|
|
921
|
+
toolId: "convex",
|
|
922
|
+
roles: ["backend"],
|
|
923
|
+
ecosystems: ["typescript"],
|
|
924
|
+
legacyCategory: "backend",
|
|
925
|
+
provides: [{
|
|
926
|
+
role: "database",
|
|
927
|
+
toolId: "convex",
|
|
928
|
+
ecosystem: "typescript",
|
|
929
|
+
overrideable: false
|
|
930
|
+
}, {
|
|
931
|
+
role: "api",
|
|
932
|
+
toolId: "convex",
|
|
933
|
+
ecosystem: "typescript",
|
|
934
|
+
overrideable: false
|
|
935
|
+
}]
|
|
936
|
+
},
|
|
937
|
+
{
|
|
938
|
+
toolId: "convex",
|
|
939
|
+
roles: ["database", "api"],
|
|
940
|
+
ecosystems: ["typescript"],
|
|
941
|
+
selectable: false
|
|
942
|
+
}
|
|
943
|
+
];
|
|
944
|
+
function sanitizePartId(input) {
|
|
945
|
+
return input.trim().toLowerCase().replace(/[^a-z0-9:._-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
946
|
+
}
|
|
947
|
+
function getRoleTargetPath(role) {
|
|
948
|
+
return PRIMARY_ROLES.has(role) ? ROLE_TARGET_PATHS[role] : void 0;
|
|
949
|
+
}
|
|
950
|
+
function createStackPart(input) {
|
|
951
|
+
const roleKey = input.ownerPartId ? `${input.ownerPartId}.${input.role}` : input.role;
|
|
952
|
+
return {
|
|
953
|
+
id: input.id ?? sanitizePartId(`${roleKey}:${input.ecosystem}:${input.toolId}`),
|
|
954
|
+
role: input.role,
|
|
955
|
+
toolId: input.toolId,
|
|
956
|
+
ecosystem: input.ecosystem,
|
|
957
|
+
ownerPartId: input.ownerPartId,
|
|
958
|
+
source: input.source ?? "selected",
|
|
959
|
+
providedByPartId: input.providedByPartId,
|
|
960
|
+
targetPath: input.targetPath ?? getRoleTargetPath(input.role),
|
|
961
|
+
settings: input.settings
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
function isNoneTool(toolId) {
|
|
965
|
+
return !toolId || toolId === "none";
|
|
966
|
+
}
|
|
967
|
+
function isLegacyBackendEcosystem(ecosystem) {
|
|
968
|
+
return ecosystem !== void 0 && ecosystem in LEGACY_BACKEND_CATEGORY_BY_ECOSYSTEM;
|
|
969
|
+
}
|
|
970
|
+
function allowsCrossEcosystemOwner(part, context) {
|
|
971
|
+
if (part.ecosystem === "universal") return true;
|
|
972
|
+
if (part.role === "auth" && context.ownerRole === "mobile" && part.ecosystem === "react-native") return true;
|
|
973
|
+
return false;
|
|
974
|
+
}
|
|
975
|
+
function createStackGraphIssue(input) {
|
|
976
|
+
return {
|
|
977
|
+
code: input.code,
|
|
978
|
+
message: input.message,
|
|
979
|
+
partId: input.partId,
|
|
980
|
+
role: input.role,
|
|
981
|
+
toolId: input.toolId
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
function createTypeScriptFrontendCompatibilityIssue(part, context) {
|
|
985
|
+
if (part.ecosystem !== "typescript" || context.ownerRole !== "frontend") return void 0;
|
|
986
|
+
if (context.ownerToolId === "fresh") {
|
|
987
|
+
if (part.role === "forms" && part.toolId === "tanstack-form") return createStackGraphIssue({
|
|
988
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
989
|
+
partId: part.id,
|
|
990
|
+
role: part.role,
|
|
991
|
+
toolId: part.toolId,
|
|
992
|
+
message: "'tanstack-form' has no Preact adapter for the Fresh frontend."
|
|
993
|
+
});
|
|
994
|
+
if (part.role === "stateManagement" && FRESH_UNSUPPORTED_STATE_MANAGEMENT_TOOLS.has(part.toolId)) return createStackGraphIssue({
|
|
995
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
996
|
+
partId: part.id,
|
|
997
|
+
role: part.role,
|
|
998
|
+
toolId: part.toolId,
|
|
999
|
+
message: `'${part.toolId}' requires React bindings and is not compatible with Fresh.`
|
|
1000
|
+
});
|
|
1001
|
+
if (part.role === "animation" && part.toolId === "lottie") return createStackGraphIssue({
|
|
1002
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1003
|
+
partId: part.id,
|
|
1004
|
+
role: part.role,
|
|
1005
|
+
toolId: part.toolId,
|
|
1006
|
+
message: "'lottie' uses lottie-react and is not compatible with Fresh."
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
if (part.role === "i18n" && part.toolId === "next-intl" && context.ownerToolId !== "next") return createStackGraphIssue({
|
|
1010
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1011
|
+
partId: part.id,
|
|
1012
|
+
role: part.role,
|
|
1013
|
+
toolId: part.toolId,
|
|
1014
|
+
message: "'next-intl' can only be selected for a Next.js frontend."
|
|
1015
|
+
});
|
|
1016
|
+
if (part.role === "ui") {
|
|
1017
|
+
const compatibility = UI_LIBRARY_COMPATIBILITY[part.toolId];
|
|
1018
|
+
if (compatibility && context.ownerToolId && !compatibility.frontends.includes(context.ownerToolId)) return createStackGraphIssue({
|
|
1019
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1020
|
+
partId: part.id,
|
|
1021
|
+
role: part.role,
|
|
1022
|
+
toolId: part.toolId,
|
|
1023
|
+
message: `'${part.toolId}' is not compatible with the '${context.ownerToolId}' frontend.`
|
|
1024
|
+
});
|
|
1025
|
+
const cssTool = context.siblingToolIdsByRole?.css ?? "tailwind";
|
|
1026
|
+
if (compatibility && !compatibility.cssFrameworks.includes(cssTool)) return createStackGraphIssue({
|
|
1027
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1028
|
+
partId: part.id,
|
|
1029
|
+
role: part.role,
|
|
1030
|
+
toolId: part.toolId,
|
|
1031
|
+
message: `'${part.toolId}' is not compatible with the '${cssTool}' CSS framework.`
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
if (part.role === "css") {
|
|
1035
|
+
const uiTool = context.siblingToolIdsByRole?.ui;
|
|
1036
|
+
const compatibility = uiTool ? UI_LIBRARY_COMPATIBILITY[uiTool] : void 0;
|
|
1037
|
+
if (compatibility && !compatibility.cssFrameworks.includes(part.toolId)) return createStackGraphIssue({
|
|
1038
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1039
|
+
partId: part.id,
|
|
1040
|
+
role: part.role,
|
|
1041
|
+
toolId: part.toolId,
|
|
1042
|
+
message: `'${uiTool}' is not compatible with the '${part.toolId}' CSS framework.`
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
function createTypeScriptBackendCompatibilityIssue(part, context) {
|
|
1047
|
+
if (part.ecosystem !== "typescript" || context.ownerRole !== "backend") return void 0;
|
|
1048
|
+
if (part.role === "payments" && part.toolId === "polar") {
|
|
1049
|
+
const authTool = context.siblingToolIdsByRole?.auth;
|
|
1050
|
+
if (authTool !== "better-auth" && authTool !== "better-auth-organizations") return createStackGraphIssue({
|
|
1051
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1052
|
+
partId: part.id,
|
|
1053
|
+
role: part.role,
|
|
1054
|
+
toolId: part.toolId,
|
|
1055
|
+
message: "Polar requires Better Auth."
|
|
1056
|
+
});
|
|
1057
|
+
const frontendTool = context.primaryToolIdsByRole?.frontend;
|
|
1058
|
+
if (!frontendTool || frontendTool === "none") return createStackGraphIssue({
|
|
1059
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1060
|
+
partId: part.id,
|
|
1061
|
+
role: part.role,
|
|
1062
|
+
toolId: part.toolId,
|
|
1063
|
+
message: "Polar requires a web frontend."
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
if (part.role === "payments" && part.toolId === "dodo") {
|
|
1067
|
+
if (context.primaryToolIdsByRole?.frontend === "react-vite") return createStackGraphIssue({
|
|
1068
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1069
|
+
partId: part.id,
|
|
1070
|
+
role: part.role,
|
|
1071
|
+
toolId: part.toolId,
|
|
1072
|
+
message: "Dodo Payments are not yet supported for React + Vite projects."
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
if (part.role === "cms" && part.toolId === "payload") {
|
|
1076
|
+
if (context.primaryToolIdsByRole?.frontend !== "next") return createStackGraphIssue({
|
|
1077
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1078
|
+
partId: part.id,
|
|
1079
|
+
role: part.role,
|
|
1080
|
+
toolId: part.toolId,
|
|
1081
|
+
message: "Payload CMS v3 requires a Next.js frontend."
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
if (part.role === "ai") {
|
|
1085
|
+
const frontendTool = context.primaryToolIdsByRole?.frontend;
|
|
1086
|
+
const frontendTools = frontendTool ? [frontendTool] : [];
|
|
1087
|
+
const examples = context.selectedToolIdsByRoleList?.examples ?? [];
|
|
1088
|
+
const runtimeTool = context.siblingToolIdsByRole?.runtime ?? "bun";
|
|
1089
|
+
if (part.toolId !== "vercel-ai" && requiresChatSdkVercelAIForExamples(context.ownerToolId, runtimeTool, examples)) return createStackGraphIssue({
|
|
1090
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1091
|
+
partId: part.id,
|
|
1092
|
+
role: part.role,
|
|
1093
|
+
toolId: part.toolId,
|
|
1094
|
+
message: "Chat SDK example (Nuxt/Hono profile) requires Vercel AI SDK in v1."
|
|
1095
|
+
});
|
|
1096
|
+
if (part.toolId === "tanstack-ai" && !hasTanStackAICompatibleFrontend(frontendTools)) return createStackGraphIssue({
|
|
1097
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1098
|
+
partId: part.id,
|
|
1099
|
+
role: part.role,
|
|
1100
|
+
toolId: part.toolId,
|
|
1101
|
+
message: "TanStack AI requires React or Solid frontend (no Vue/Svelte/Angular adapter yet)."
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
function createSharedBackendServiceCompatibilityIssue(part, context) {
|
|
1106
|
+
if (part.ecosystem === "typescript" || part.ecosystem === "react-native" || context.ownerRole !== "backend" || part.toolId === "none") return;
|
|
1107
|
+
const rule = SHARED_NON_TYPESCRIPT_BACKEND_SERVICE_RULES[part.role];
|
|
1108
|
+
if (!rule) return void 0;
|
|
1109
|
+
if ((!part.id.startsWith("candidate:") || part.id.startsWith("candidate:native:")) && isNativeEcosystemBackendServiceTool(part)) return;
|
|
1110
|
+
if (part.toolId !== rule.allowedToolId) return createStackGraphIssue({
|
|
1111
|
+
code: "INCOMPATIBLE_ECOSYSTEM_TOOL",
|
|
1112
|
+
partId: part.id,
|
|
1113
|
+
role: part.role,
|
|
1114
|
+
toolId: part.toolId,
|
|
1115
|
+
message: rule.unsupportedToolMessage
|
|
1116
|
+
});
|
|
1117
|
+
if (part.ecosystem === "java" && isNoneTool(context.siblingToolIdsByRole?.buildTool)) return createStackGraphIssue({
|
|
1118
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1119
|
+
partId: part.id,
|
|
1120
|
+
role: part.role,
|
|
1121
|
+
toolId: part.toolId,
|
|
1122
|
+
message: rule.javaNoBuildToolMessage
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
function createMobileCompatibilityIssue(part, context) {
|
|
1126
|
+
if (part.ecosystem !== "react-native" || context.ownerRole !== "mobile") return void 0;
|
|
1127
|
+
if (part.role === "ui") {
|
|
1128
|
+
if (part.toolId === "uniwind" && context.ownerToolId !== "native-uniwind") return createStackGraphIssue({
|
|
1129
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1130
|
+
partId: part.id,
|
|
1131
|
+
role: part.role,
|
|
1132
|
+
toolId: part.toolId,
|
|
1133
|
+
message: "Uniwind mobile UI requires the Expo + Uniwind frontend."
|
|
1134
|
+
});
|
|
1135
|
+
if (part.toolId === "unistyles" && context.ownerToolId !== "native-unistyles") return createStackGraphIssue({
|
|
1136
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1137
|
+
partId: part.id,
|
|
1138
|
+
role: part.role,
|
|
1139
|
+
toolId: part.toolId,
|
|
1140
|
+
message: "Unistyles mobile UI requires the Expo + Unistyles frontend."
|
|
1141
|
+
});
|
|
1142
|
+
if (["tamagui", "gluestack-ui"].includes(part.toolId) && ["native-uniwind", "native-unistyles"].includes(context.ownerToolId ?? "")) return createStackGraphIssue({
|
|
1143
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1144
|
+
partId: part.id,
|
|
1145
|
+
role: part.role,
|
|
1146
|
+
toolId: part.toolId,
|
|
1147
|
+
message: "Tamagui and Gluestack UI require the base Expo frontend to avoid conflicting styling setup."
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
function createInfrastructureCompatibilityIssue(part, context) {
|
|
1152
|
+
if (part.role === "runtime" && part.ecosystem === "typescript") {
|
|
1153
|
+
if (context.ownerRole !== "backend") return void 0;
|
|
1154
|
+
if (RUNTIMELESS_TYPESCRIPT_BACKENDS.has(context.ownerToolId ?? "none")) return createStackGraphIssue({
|
|
1155
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1156
|
+
partId: part.id,
|
|
1157
|
+
role: part.role,
|
|
1158
|
+
toolId: part.toolId,
|
|
1159
|
+
message: `'${context.ownerToolId}' does not use a separate backend runtime.`
|
|
1160
|
+
});
|
|
1161
|
+
if (part.toolId === "workers" && context.ownerToolId !== "hono") return createStackGraphIssue({
|
|
1162
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1163
|
+
partId: part.id,
|
|
1164
|
+
role: part.role,
|
|
1165
|
+
toolId: part.toolId,
|
|
1166
|
+
message: "Workers runtime currently requires the Hono backend."
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
if (part.role === "deploy" && part.ecosystem === "typescript") {
|
|
1170
|
+
if (context.ownerRole === "frontend") {
|
|
1171
|
+
if (!WEB_DEPLOY_VALUES.includes(part.toolId)) return createStackGraphIssue({
|
|
1172
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1173
|
+
partId: part.id,
|
|
1174
|
+
role: part.role,
|
|
1175
|
+
toolId: part.toolId,
|
|
1176
|
+
message: `'${part.toolId}' is not a web deployment target.`
|
|
1177
|
+
});
|
|
1178
|
+
if (part.toolId === "vercel" && ["redwood", "fresh"].includes(context.ownerToolId ?? "")) return createStackGraphIssue({
|
|
1179
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1180
|
+
partId: part.id,
|
|
1181
|
+
role: part.role,
|
|
1182
|
+
toolId: part.toolId,
|
|
1183
|
+
message: `Vercel deployment is not available for the '${context.ownerToolId}' frontend.`
|
|
1184
|
+
});
|
|
1185
|
+
const unsupportedFrontend = getUnsupportedWebDeployFrontend(part.toolId, [context.ownerToolId ?? "none"]);
|
|
1186
|
+
if (unsupportedFrontend) return createStackGraphIssue({
|
|
1187
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1188
|
+
partId: part.id,
|
|
1189
|
+
role: part.role,
|
|
1190
|
+
toolId: part.toolId,
|
|
1191
|
+
message: `'${part.toolId}' web deployment is not wired for the '${unsupportedFrontend}' frontend.`
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
if (context.ownerRole === "backend") {
|
|
1195
|
+
if (!SERVER_DEPLOY_VALUES.includes(part.toolId)) return createStackGraphIssue({
|
|
1196
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1197
|
+
partId: part.id,
|
|
1198
|
+
role: part.role,
|
|
1199
|
+
toolId: part.toolId,
|
|
1200
|
+
message: `'${part.toolId}' is not a server deployment target.`
|
|
1201
|
+
});
|
|
1202
|
+
const backendTool = context.ownerToolId ?? "none";
|
|
1203
|
+
const runtimeTool = context.siblingToolIdsByRole?.runtime ?? "bun";
|
|
1204
|
+
if (part.toolId === "cloudflare") {
|
|
1205
|
+
if (runtimeTool !== "workers" || backendTool !== "hono") return createStackGraphIssue({
|
|
1206
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1207
|
+
partId: part.id,
|
|
1208
|
+
role: part.role,
|
|
1209
|
+
toolId: part.toolId,
|
|
1210
|
+
message: "Cloudflare server deploy requires Workers runtime with the Hono backend."
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
if (part.toolId === "vercel") {
|
|
1214
|
+
if (["nestjs", "adonisjs"].includes(backendTool)) return createStackGraphIssue({
|
|
1215
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1216
|
+
partId: part.id,
|
|
1217
|
+
role: part.role,
|
|
1218
|
+
toolId: part.toolId,
|
|
1219
|
+
message: "Vercel serverless functions are incompatible with NestJS and AdonisJS."
|
|
1220
|
+
});
|
|
1221
|
+
if (backendTool === "encore") return createStackGraphIssue({
|
|
1222
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1223
|
+
partId: part.id,
|
|
1224
|
+
role: part.role,
|
|
1225
|
+
toolId: part.toolId,
|
|
1226
|
+
message: "Encore manages its own deployment infrastructure."
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
if (part.toolId === "netlify") {
|
|
1230
|
+
if (backendTool !== "hono") return createStackGraphIssue({
|
|
1231
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1232
|
+
partId: part.id,
|
|
1233
|
+
role: part.role,
|
|
1234
|
+
toolId: part.toolId,
|
|
1235
|
+
message: "Netlify Functions server deploy is currently supported only with Hono."
|
|
1236
|
+
});
|
|
1237
|
+
if (runtimeTool !== "node") return createStackGraphIssue({
|
|
1238
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1239
|
+
partId: part.id,
|
|
1240
|
+
role: part.role,
|
|
1241
|
+
toolId: part.toolId,
|
|
1242
|
+
message: "Netlify Functions server deploy requires Node.js runtime."
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
if (NO_SERVER_DEPLOY_TYPESCRIPT_BACKENDS.has(backendTool)) return createStackGraphIssue({
|
|
1246
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1247
|
+
partId: part.id,
|
|
1248
|
+
role: part.role,
|
|
1249
|
+
toolId: part.toolId,
|
|
1250
|
+
message: `'${backendTool}' does not use a separate server deployment target.`
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
if (part.role === "dbSetup") {
|
|
1255
|
+
const databaseTool = context.ownerToolId ?? "none";
|
|
1256
|
+
const runtimeTool = context.selectedToolIdsByRole?.runtime ?? "bun";
|
|
1257
|
+
if (part.toolId === "turso" && databaseTool !== "sqlite") return createStackGraphIssue({
|
|
1258
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1259
|
+
partId: part.id,
|
|
1260
|
+
role: part.role,
|
|
1261
|
+
toolId: part.toolId,
|
|
1262
|
+
message: "Turso database setup requires SQLite."
|
|
1263
|
+
});
|
|
1264
|
+
if (part.toolId === "d1") {
|
|
1265
|
+
if (databaseTool !== "sqlite") return createStackGraphIssue({
|
|
1266
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1267
|
+
partId: part.id,
|
|
1268
|
+
role: part.role,
|
|
1269
|
+
toolId: part.toolId,
|
|
1270
|
+
message: "D1 database setup requires SQLite."
|
|
1271
|
+
});
|
|
1272
|
+
if (runtimeTool !== "workers") return createStackGraphIssue({
|
|
1273
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1274
|
+
partId: part.id,
|
|
1275
|
+
role: part.role,
|
|
1276
|
+
toolId: part.toolId,
|
|
1277
|
+
message: "D1 database setup requires Workers runtime."
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
if ((part.toolId === "neon" || part.toolId === "supabase" || part.toolId === "prisma-postgres") && databaseTool !== "postgres") return createStackGraphIssue({
|
|
1281
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1282
|
+
partId: part.id,
|
|
1283
|
+
role: part.role,
|
|
1284
|
+
toolId: part.toolId,
|
|
1285
|
+
message: `'${part.toolId}' database setup requires PostgreSQL.`
|
|
1286
|
+
});
|
|
1287
|
+
if (part.toolId === "mongodb-atlas" && databaseTool !== "mongodb") return createStackGraphIssue({
|
|
1288
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1289
|
+
partId: part.id,
|
|
1290
|
+
role: part.role,
|
|
1291
|
+
toolId: part.toolId,
|
|
1292
|
+
message: "MongoDB Atlas setup requires MongoDB."
|
|
1293
|
+
});
|
|
1294
|
+
if (part.toolId === "planetscale" && databaseTool !== "postgres" && databaseTool !== "mysql") return createStackGraphIssue({
|
|
1295
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1296
|
+
partId: part.id,
|
|
1297
|
+
role: part.role,
|
|
1298
|
+
toolId: part.toolId,
|
|
1299
|
+
message: "PlanetScale setup requires PostgreSQL or MySQL."
|
|
1300
|
+
});
|
|
1301
|
+
if (part.toolId === "docker") {
|
|
1302
|
+
if (databaseTool === "sqlite") return createStackGraphIssue({
|
|
1303
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1304
|
+
partId: part.id,
|
|
1305
|
+
role: part.role,
|
|
1306
|
+
toolId: part.toolId,
|
|
1307
|
+
message: "SQLite does not need Docker database setup."
|
|
1308
|
+
});
|
|
1309
|
+
if (runtimeTool === "workers") return createStackGraphIssue({
|
|
1310
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1311
|
+
partId: part.id,
|
|
1312
|
+
role: part.role,
|
|
1313
|
+
toolId: part.toolId,
|
|
1314
|
+
message: "Docker database setup is incompatible with Workers runtime."
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
function createAddonCompatibilityIssue(part, context) {
|
|
1320
|
+
const frontendTool = context.primaryToolIdsByRole?.frontend;
|
|
1321
|
+
const frontendTools = frontendTool ? [frontendTool] : [];
|
|
1322
|
+
const frontendEcosystem = context.primaryEcosystemsByRole?.frontend;
|
|
1323
|
+
const backendTool = context.primaryToolIdsByRole?.backend;
|
|
1324
|
+
const backendEcosystem = context.primaryEcosystemsByRole?.backend;
|
|
1325
|
+
const runtimeTool = context.selectedToolIdsByRole?.runtime ?? "bun";
|
|
1326
|
+
const apiTool = context.selectedToolIdsByRole?.api;
|
|
1327
|
+
if (part.role === "appPlatform") {
|
|
1328
|
+
if (part.toolId === "pwa" && !hasPWACompatibleFrontend(frontendTools)) return createStackGraphIssue({
|
|
1329
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1330
|
+
partId: part.id,
|
|
1331
|
+
role: part.role,
|
|
1332
|
+
toolId: part.toolId,
|
|
1333
|
+
message: "PWA requires a compatible web frontend."
|
|
1334
|
+
});
|
|
1335
|
+
if (part.toolId === "tauri" && !hasTauriCompatibleFrontend(frontendTools)) return createStackGraphIssue({
|
|
1336
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1337
|
+
partId: part.id,
|
|
1338
|
+
role: part.role,
|
|
1339
|
+
toolId: part.toolId,
|
|
1340
|
+
message: "Tauri requires a compatible web frontend."
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
if (part.role === "dataFetching") {
|
|
1344
|
+
if (part.toolId === "tanstack-query" && apiTool && apiTool !== "none") return createStackGraphIssue({
|
|
1345
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1346
|
+
partId: part.id,
|
|
1347
|
+
role: part.role,
|
|
1348
|
+
toolId: part.toolId,
|
|
1349
|
+
message: "TanStack Query is already included via the selected API layer."
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
if (part.role === "workspaceTooling") {
|
|
1353
|
+
if (part.toolId === "docker-compose") {
|
|
1354
|
+
if (backendTool === "convex") return createStackGraphIssue({
|
|
1355
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1356
|
+
partId: part.id,
|
|
1357
|
+
role: part.role,
|
|
1358
|
+
toolId: part.toolId,
|
|
1359
|
+
message: "Docker Compose is not compatible with Convex backend."
|
|
1360
|
+
});
|
|
1361
|
+
if (runtimeTool === "workers") return createStackGraphIssue({
|
|
1362
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1363
|
+
partId: part.id,
|
|
1364
|
+
role: part.role,
|
|
1365
|
+
toolId: part.toolId,
|
|
1366
|
+
message: "Docker Compose is not compatible with Cloudflare Workers runtime."
|
|
1367
|
+
});
|
|
1368
|
+
if (frontendEcosystem === "typescript" && frontendTool && !hasDockerComposeCompatibleFrontend(frontendTools)) return createStackGraphIssue({
|
|
1369
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1370
|
+
partId: part.id,
|
|
1371
|
+
role: part.role,
|
|
1372
|
+
toolId: part.toolId,
|
|
1373
|
+
message: "Docker Compose is not wired for the selected web frontend."
|
|
1374
|
+
});
|
|
1375
|
+
if (backendEcosystem === "typescript" && backendTool === "self" && frontendTool !== "next") return createStackGraphIssue({
|
|
1376
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1377
|
+
partId: part.id,
|
|
1378
|
+
role: part.role,
|
|
1379
|
+
toolId: part.toolId,
|
|
1380
|
+
message: "Docker Compose self-backend support currently requires Next.js."
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
if (part.toolId === "backend-utils") {
|
|
1384
|
+
if (backendEcosystem !== "typescript" || !isBackendUtilsCompatibleBackend(backendTool)) return createStackGraphIssue({
|
|
1385
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1386
|
+
partId: part.id,
|
|
1387
|
+
role: part.role,
|
|
1388
|
+
toolId: part.toolId,
|
|
1389
|
+
message: "Backend Utils requires a compatible TypeScript server stack."
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
if (part.role === "examples") {
|
|
1394
|
+
if (part.toolId === "ai" && !isExampleAIAllowed(backendTool, frontendTools)) return createStackGraphIssue({
|
|
1395
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1396
|
+
partId: part.id,
|
|
1397
|
+
role: part.role,
|
|
1398
|
+
toolId: part.toolId,
|
|
1399
|
+
message: "AI example is not compatible with the selected graph stack."
|
|
1400
|
+
});
|
|
1401
|
+
if (part.toolId === "chat-sdk") {
|
|
1402
|
+
if (backendEcosystem !== "typescript" || !isExampleChatSdkAllowed(backendTool, frontendTools, runtimeTool)) return createStackGraphIssue({
|
|
1403
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1404
|
+
partId: part.id,
|
|
1405
|
+
role: part.role,
|
|
1406
|
+
toolId: part.toolId,
|
|
1407
|
+
message: "Chat SDK example is not compatible with the selected graph stack."
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
function createJavaCompatibilityIssue(part, context) {
|
|
1413
|
+
if (part.ecosystem !== "java") return void 0;
|
|
1414
|
+
const buildTool = context.siblingToolIdsByRole?.buildTool;
|
|
1415
|
+
const hasBuildTool = !isNoneTool(buildTool);
|
|
1416
|
+
const javaLibraries = context.siblingToolIdsByRoleList?.libraries ?? [];
|
|
1417
|
+
if (part.role === "buildTool" && part.toolId === "none") {
|
|
1418
|
+
if (context.ownerToolId && context.ownerToolId !== "none") return createStackGraphIssue({
|
|
1419
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1420
|
+
partId: part.id,
|
|
1421
|
+
role: part.role,
|
|
1422
|
+
toolId: part.toolId,
|
|
1423
|
+
message: "Java web frameworks require Maven or Gradle"
|
|
1424
|
+
});
|
|
1425
|
+
if (!isNoneTool(context.siblingToolIdsByRole?.orm)) return createStackGraphIssue({
|
|
1426
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1427
|
+
partId: part.id,
|
|
1428
|
+
role: part.role,
|
|
1429
|
+
toolId: part.toolId,
|
|
1430
|
+
message: "Java ORM support requires Maven or Gradle"
|
|
1431
|
+
});
|
|
1432
|
+
if (!isNoneTool(context.siblingToolIdsByRole?.auth)) return createStackGraphIssue({
|
|
1433
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1434
|
+
partId: part.id,
|
|
1435
|
+
role: part.role,
|
|
1436
|
+
toolId: part.toolId,
|
|
1437
|
+
message: "Java auth support requires Maven or Gradle"
|
|
1438
|
+
});
|
|
1439
|
+
if (javaLibraries.some((library) => library !== "none")) return createStackGraphIssue({
|
|
1440
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1441
|
+
partId: part.id,
|
|
1442
|
+
role: part.role,
|
|
1443
|
+
toolId: part.toolId,
|
|
1444
|
+
message: "Java libraries require Maven or Gradle"
|
|
1445
|
+
});
|
|
1446
|
+
if ((context.siblingToolIdsByRoleList?.testing ?? []).some((library) => library !== "none")) return createStackGraphIssue({
|
|
1447
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1448
|
+
partId: part.id,
|
|
1449
|
+
role: part.role,
|
|
1450
|
+
toolId: part.toolId,
|
|
1451
|
+
message: "Java testing libraries require Maven or Gradle"
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
if ((part.role === "orm" || part.role === "auth") && part.toolId !== "none") {
|
|
1455
|
+
if (context.ownerToolId !== "spring-boot") return createStackGraphIssue({
|
|
1456
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1457
|
+
partId: part.id,
|
|
1458
|
+
role: part.role,
|
|
1459
|
+
toolId: part.toolId,
|
|
1460
|
+
message: part.role === "orm" ? "Java ORM support currently requires Spring Boot" : "Java auth support currently requires Spring Boot"
|
|
1461
|
+
});
|
|
1462
|
+
if (!hasBuildTool) return createStackGraphIssue({
|
|
1463
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1464
|
+
partId: part.id,
|
|
1465
|
+
role: part.role,
|
|
1466
|
+
toolId: part.toolId,
|
|
1467
|
+
message: part.role === "orm" ? "Java ORM support requires Maven or Gradle" : "Java auth support requires Maven or Gradle"
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
if (part.role === "libraries" && part.toolId !== "none") {
|
|
1471
|
+
if (context.ownerToolId !== "spring-boot") return createStackGraphIssue({
|
|
1472
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1473
|
+
partId: part.id,
|
|
1474
|
+
role: part.role,
|
|
1475
|
+
toolId: part.toolId,
|
|
1476
|
+
message: "Spring libraries currently require Spring Boot in the Java scaffold"
|
|
1477
|
+
});
|
|
1478
|
+
if (!hasBuildTool) return createStackGraphIssue({
|
|
1479
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1480
|
+
partId: part.id,
|
|
1481
|
+
role: part.role,
|
|
1482
|
+
toolId: part.toolId,
|
|
1483
|
+
message: "Java libraries require Maven or Gradle"
|
|
1484
|
+
});
|
|
1485
|
+
if (part.toolId === "flyway" && context.siblingToolIdsByRole?.orm !== "spring-data-jpa") return createStackGraphIssue({
|
|
1486
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1487
|
+
partId: part.id,
|
|
1488
|
+
role: part.role,
|
|
1489
|
+
toolId: part.toolId,
|
|
1490
|
+
message: "Flyway currently requires Spring Data JPA in the Java scaffold"
|
|
1491
|
+
});
|
|
1492
|
+
if (part.toolId === "liquibase" && context.siblingToolIdsByRole?.orm !== "spring-data-jpa") return createStackGraphIssue({
|
|
1493
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1494
|
+
partId: part.id,
|
|
1495
|
+
role: part.role,
|
|
1496
|
+
toolId: part.toolId,
|
|
1497
|
+
message: "Liquibase currently requires Spring Data JPA in the Java scaffold"
|
|
1498
|
+
});
|
|
1499
|
+
if (part.toolId === "flyway" && javaLibraries.includes("liquibase")) return createStackGraphIssue({
|
|
1500
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1501
|
+
partId: part.id,
|
|
1502
|
+
role: part.role,
|
|
1503
|
+
toolId: part.toolId,
|
|
1504
|
+
message: "Flyway cannot be combined with Liquibase in the current Java scaffold"
|
|
1505
|
+
});
|
|
1506
|
+
if (part.toolId === "liquibase" && javaLibraries.includes("flyway")) return createStackGraphIssue({
|
|
1507
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1508
|
+
partId: part.id,
|
|
1509
|
+
role: part.role,
|
|
1510
|
+
toolId: part.toolId,
|
|
1511
|
+
message: "Liquibase cannot be combined with Flyway in the current Java scaffold"
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
if (part.role === "testing" && part.toolId !== "none" && !hasBuildTool) return createStackGraphIssue({
|
|
1515
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1516
|
+
partId: part.id,
|
|
1517
|
+
role: part.role,
|
|
1518
|
+
toolId: part.toolId,
|
|
1519
|
+
message: "Java testing libraries require Maven or Gradle"
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
function getStackPartCompatibilityIssue(part, context) {
|
|
1523
|
+
if (context.ownerEcosystem && part.ecosystem !== context.ownerEcosystem && !allowsCrossEcosystemOwner(part, context)) return createStackGraphIssue({
|
|
1524
|
+
code: "INCOMPATIBLE_OWNER_ECOSYSTEM",
|
|
1525
|
+
partId: part.id,
|
|
1526
|
+
role: part.role,
|
|
1527
|
+
toolId: part.toolId,
|
|
1528
|
+
message: `'${part.toolId}' uses the ${part.ecosystem} adapter but its owner uses ${context.ownerEcosystem}.`
|
|
1529
|
+
});
|
|
1530
|
+
const expectedOwnerRoles = part.ecosystem === "typescript" || part.role === "dbSetup" ? OWNER_ROLES_BY_SCOPED_ROLE[part.role] : void 0;
|
|
1531
|
+
if (expectedOwnerRoles && context.ownerRole && !expectedOwnerRoles.includes(context.ownerRole)) return createStackGraphIssue({
|
|
1532
|
+
code: "INCOMPATIBLE_OWNER_ROLE",
|
|
1533
|
+
partId: part.id,
|
|
1534
|
+
role: part.role,
|
|
1535
|
+
toolId: part.toolId,
|
|
1536
|
+
message: `'${part.toolId}' must be owned by one of: ${expectedOwnerRoles.join(", ")}.`
|
|
1537
|
+
});
|
|
1538
|
+
const frontendCompatibilityIssue = createTypeScriptFrontendCompatibilityIssue(part, context);
|
|
1539
|
+
if (frontendCompatibilityIssue) return frontendCompatibilityIssue;
|
|
1540
|
+
const backendCompatibilityIssue = createTypeScriptBackendCompatibilityIssue(part, context);
|
|
1541
|
+
if (backendCompatibilityIssue) return backendCompatibilityIssue;
|
|
1542
|
+
const sharedBackendServiceCompatibilityIssue = createSharedBackendServiceCompatibilityIssue(part, context);
|
|
1543
|
+
if (sharedBackendServiceCompatibilityIssue) return sharedBackendServiceCompatibilityIssue;
|
|
1544
|
+
const mobileCompatibilityIssue = createMobileCompatibilityIssue(part, context);
|
|
1545
|
+
if (mobileCompatibilityIssue) return mobileCompatibilityIssue;
|
|
1546
|
+
const infrastructureCompatibilityIssue = createInfrastructureCompatibilityIssue(part, context);
|
|
1547
|
+
if (infrastructureCompatibilityIssue) return infrastructureCompatibilityIssue;
|
|
1548
|
+
const addonCompatibilityIssue = createAddonCompatibilityIssue(part, context);
|
|
1549
|
+
if (addonCompatibilityIssue) return addonCompatibilityIssue;
|
|
1550
|
+
const javaCompatibilityIssue = createJavaCompatibilityIssue(part, context);
|
|
1551
|
+
if (javaCompatibilityIssue) return javaCompatibilityIssue;
|
|
1552
|
+
if (part.ecosystem === "python" && part.role === "api" && DJANGO_API_TOOLS.has(part.toolId)) {
|
|
1553
|
+
if (context.ownerToolId !== "django") return createStackGraphIssue({
|
|
1554
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1555
|
+
partId: part.id,
|
|
1556
|
+
role: part.role,
|
|
1557
|
+
toolId: part.toolId,
|
|
1558
|
+
message: `'${part.toolId}' can only be selected for a Django backend.`
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
if (part.ecosystem === "java" && (part.role === "orm" || part.role === "auth") && JAVA_SPRING_CAPABILITY_TOOLS.has(part.toolId)) {
|
|
1562
|
+
if (context.ownerToolId !== "spring-boot") return createStackGraphIssue({
|
|
1563
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1564
|
+
partId: part.id,
|
|
1565
|
+
role: part.role,
|
|
1566
|
+
toolId: part.toolId,
|
|
1567
|
+
message: `'${part.toolId}' can only be selected for a Spring Boot backend.`
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
if (part.ecosystem === "typescript" && part.role === "api" && part.toolId === "trpc") {
|
|
1571
|
+
const frontendTool = context.primaryToolIdsByRole?.frontend;
|
|
1572
|
+
if (frontendTool && TYPESCRIPT_TRPC_INCOMPATIBLE_FRONTENDS.has(frontendTool)) return createStackGraphIssue({
|
|
1573
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1574
|
+
partId: part.id,
|
|
1575
|
+
role: part.role,
|
|
1576
|
+
toolId: part.toolId,
|
|
1577
|
+
message: `'trpc' cannot be selected with the '${frontendTool}' frontend.`
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
if (part.ecosystem === "typescript" && part.role === "auth" && (part.toolId === "better-auth" || part.toolId === "better-auth-organizations")) {
|
|
1581
|
+
if (context.primaryToolIdsByRole?.database === "redis") return createStackGraphIssue({
|
|
1582
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1583
|
+
partId: part.id,
|
|
1584
|
+
role: part.role,
|
|
1585
|
+
toolId: part.toolId,
|
|
1586
|
+
message: "'better-auth' cannot use Redis as the primary database."
|
|
1587
|
+
});
|
|
1588
|
+
const ormTool = context.siblingToolIdsByRole?.orm;
|
|
1589
|
+
if (ormTool && BETTER_AUTH_UNSUPPORTED_ORM_TOOLS.has(ormTool)) return createStackGraphIssue({
|
|
1590
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1591
|
+
partId: part.id,
|
|
1592
|
+
role: part.role,
|
|
1593
|
+
toolId: part.toolId,
|
|
1594
|
+
message: `'better-auth' is not compatible with the '${ormTool}' ORM selection.`
|
|
1595
|
+
});
|
|
1596
|
+
}
|
|
1597
|
+
if (part.ecosystem === "elixir" && ELIXIR_ECTO_REQUIRED_TOOLS.has(part.toolId) && (part.role === "api" || part.role === "auth") && context.ownerRole === "backend") {
|
|
1598
|
+
const ormTool = context.siblingToolIdsByRole?.orm;
|
|
1599
|
+
if (isNoneTool(ormTool)) return createStackGraphIssue({
|
|
1600
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1601
|
+
partId: part.id,
|
|
1602
|
+
role: part.role,
|
|
1603
|
+
toolId: part.toolId,
|
|
1604
|
+
message: part.toolId === "phx-gen-auth" ? "phx.gen.auth requires Ecto" : "Absinthe GraphQL requires Ecto in the current Phoenix scaffold"
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
if (part.ecosystem === "elixir" && ELIXIR_ECTO_SQL_REQUIRED_TOOLS.has(part.toolId) && (!context.ownerRole || context.ownerRole === "backend")) {
|
|
1608
|
+
if ((context.siblingToolIdsByRole?.orm ?? context.selectedToolIdsByRole?.orm) !== "ecto-sql") return createStackGraphIssue({
|
|
1609
|
+
code: "INCOMPATIBLE_GRAPH_SELECTION",
|
|
1610
|
+
partId: part.id,
|
|
1611
|
+
role: part.role,
|
|
1612
|
+
toolId: part.toolId,
|
|
1613
|
+
message: "Oban requires Ecto SQL with PostgreSQL in the current Phoenix scaffold"
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
if (part.ecosystem === "elixir" && part.toolId === "live-view-streams" && context.ownerRole === "backend") {
|
|
1617
|
+
if (context.ownerToolId !== "phoenix-live-view") return createStackGraphIssue({
|
|
1618
|
+
code: "INCOMPATIBLE_OWNER_TOOL",
|
|
1619
|
+
partId: part.id,
|
|
1620
|
+
role: part.role,
|
|
1621
|
+
toolId: part.toolId,
|
|
1622
|
+
message: "LiveView Streams require Phoenix LiveView"
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
const unsupportedElixirGraphToolMessage = part.ecosystem === "elixir" ? ELIXIR_UNSUPPORTED_GRAPH_TOOL_MESSAGES[part.toolId] : void 0;
|
|
1626
|
+
if (unsupportedElixirGraphToolMessage) return createStackGraphIssue({
|
|
1627
|
+
code: "UNSUPPORTED_GRAPH_TOOL",
|
|
1628
|
+
partId: part.id,
|
|
1629
|
+
role: part.role,
|
|
1630
|
+
toolId: part.toolId,
|
|
1631
|
+
message: unsupportedElixirGraphToolMessage
|
|
1632
|
+
});
|
|
1633
|
+
if (part.ecosystem === "elixir" && !context.ownerRole) {
|
|
1634
|
+
const phoenixRequiredMessage = ELIXIR_PHOENIX_REQUIRED_ROLE_MESSAGES[part.role];
|
|
1635
|
+
if (phoenixRequiredMessage && part.toolId !== "none") return createStackGraphIssue({
|
|
1636
|
+
code: "MISSING_OWNER_PART",
|
|
1637
|
+
partId: part.id,
|
|
1638
|
+
role: part.role,
|
|
1639
|
+
toolId: part.toolId,
|
|
1640
|
+
message: phoenixRequiredMessage
|
|
1641
|
+
});
|
|
1642
|
+
if (part.role === "observability" && part.toolId === "phoenix-telemetry") return createStackGraphIssue({
|
|
1643
|
+
code: "MISSING_OWNER_PART",
|
|
1644
|
+
partId: part.id,
|
|
1645
|
+
role: part.role,
|
|
1646
|
+
toolId: part.toolId,
|
|
1647
|
+
message: "Phoenix telemetry requires Phoenix"
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
function getStackPartCompatibilityIssueForPart(part, parts) {
|
|
1652
|
+
return getStackPartCompatibilityIssue(part, getStackPartOptionContextForPart(part, parts));
|
|
1653
|
+
}
|
|
1654
|
+
function getStackToolDefinitions(context) {
|
|
1655
|
+
if (!context) return [...STACK_TOOL_DEFINITIONS];
|
|
1656
|
+
return STACK_TOOL_DEFINITIONS.filter((definition) => {
|
|
1657
|
+
if (!definition.roles.includes(context.role)) return false;
|
|
1658
|
+
if (context.ecosystem && !definition.ecosystems.includes(context.ecosystem)) return false;
|
|
1659
|
+
if (definition.selectable === false) return false;
|
|
1660
|
+
const ecosystem = context.ecosystem ?? definition.ecosystems[0];
|
|
1661
|
+
if (getStackPartCompatibilityIssue({
|
|
1662
|
+
id: `${context.ownerRole ?? "root"}:${context.role}:${definition.toolId}`,
|
|
1663
|
+
role: context.role,
|
|
1664
|
+
toolId: definition.toolId,
|
|
1665
|
+
ecosystem
|
|
1666
|
+
}, context)) return false;
|
|
1667
|
+
return true;
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
function getStackPartOptions(context) {
|
|
1671
|
+
return [...new Set(getStackToolDefinitions(context).map((definition) => definition.toolId))];
|
|
1672
|
+
}
|
|
1673
|
+
function findDefinition(part) {
|
|
1674
|
+
return STACK_TOOL_DEFINITIONS.find((definition) => definition.toolId === part.toolId && definition.roles.includes(part.role) && definition.ecosystems.includes(part.ecosystem));
|
|
1675
|
+
}
|
|
1676
|
+
function isOwnerlessDefinition(definition) {
|
|
1677
|
+
return definition?.ownerless === true;
|
|
1678
|
+
}
|
|
1679
|
+
function allowsMultipleSelectedParts(parts) {
|
|
1680
|
+
return parts.filter((part) => part.source !== "provided").every((part) => findDefinition(part)?.allowMultiple === true);
|
|
1681
|
+
}
|
|
1682
|
+
function parseRolePath(rolePath) {
|
|
1683
|
+
const segments = rolePath.split(".");
|
|
1684
|
+
const rawRole = segments.length === 1 ? segments[0] : segments[segments.length - 1];
|
|
1685
|
+
const rawOwnerRole = segments.length > 1 ? segments[0] : void 0;
|
|
1686
|
+
const role = StackPartRoleSchema.parse(rawRole);
|
|
1687
|
+
const ownerRole = rawOwnerRole ? StackPartRoleSchema.parse(rawOwnerRole) : void 0;
|
|
1688
|
+
if (ownerRole && !PRIMARY_ROLES.has(ownerRole)) throw new Error(`Stack part owner role '${ownerRole}' must be a primary role.`);
|
|
1689
|
+
return {
|
|
1690
|
+
role,
|
|
1691
|
+
ownerRole
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
function parseStackPartSpecs(specs, source = "selected") {
|
|
1695
|
+
const unresolved = specs.map((spec) => {
|
|
1696
|
+
const [rolePath, ecosystem, toolId, customId] = spec.split(":");
|
|
1697
|
+
if (!rolePath || !ecosystem || !toolId || spec.split(":").length > 4) throw new Error(`Invalid --part '${spec}'. Use role:ecosystem:tool or owner.role:ecosystem:tool.`);
|
|
1698
|
+
const { role, ownerRole } = parseRolePath(rolePath);
|
|
1699
|
+
return {
|
|
1700
|
+
role,
|
|
1701
|
+
ownerRole,
|
|
1702
|
+
ecosystem,
|
|
1703
|
+
toolId,
|
|
1704
|
+
customId
|
|
1705
|
+
};
|
|
1706
|
+
});
|
|
1707
|
+
const primaryParts = unresolved.filter((part) => !part.ownerRole).map((part) => createStackPart({
|
|
1708
|
+
role: part.role,
|
|
1709
|
+
ecosystem: part.ecosystem,
|
|
1710
|
+
toolId: part.toolId,
|
|
1711
|
+
source,
|
|
1712
|
+
id: part.customId
|
|
1713
|
+
}));
|
|
1714
|
+
const primaryByRole = new Map(primaryParts.map((part) => [part.role, part]));
|
|
1715
|
+
const primaryByRoleAndEcosystem = new Map(primaryParts.map((part) => [`${part.role}:${part.ecosystem}`, part]));
|
|
1716
|
+
const scopedParts = unresolved.filter((part) => part.ownerRole !== void 0).map((part) => createStackPart({
|
|
1717
|
+
role: part.role,
|
|
1718
|
+
ecosystem: part.ecosystem,
|
|
1719
|
+
toolId: part.toolId,
|
|
1720
|
+
ownerPartId: primaryByRoleAndEcosystem.get(`${part.ownerRole}:${part.ecosystem}`)?.id ?? primaryByRole.get(part.ownerRole)?.id,
|
|
1721
|
+
source,
|
|
1722
|
+
id: part.customId
|
|
1723
|
+
}));
|
|
1724
|
+
return materializeProvidedStackParts([...primaryParts, ...scopedParts]);
|
|
1725
|
+
}
|
|
1726
|
+
function formatStackPartSpec(part, parts = []) {
|
|
1727
|
+
const owner = part.ownerPartId ? parts.find((candidate) => candidate.id === part.ownerPartId) : void 0;
|
|
1728
|
+
return `${owner ? `${owner.role}.${part.role}` : part.role}:${part.ecosystem}:${part.toolId}`;
|
|
1729
|
+
}
|
|
1730
|
+
function materializeProvidedStackParts(parts) {
|
|
1731
|
+
const next = [...parts];
|
|
1732
|
+
for (const part of parts) {
|
|
1733
|
+
const definition = findDefinition(part);
|
|
1734
|
+
for (const provided of definition?.provides ?? []) next.push(createStackPart({
|
|
1735
|
+
role: provided.role,
|
|
1736
|
+
toolId: provided.toolId,
|
|
1737
|
+
ecosystem: provided.ecosystem ?? part.ecosystem,
|
|
1738
|
+
ownerPartId: part.id,
|
|
1739
|
+
source: "provided",
|
|
1740
|
+
providedByPartId: part.id
|
|
1741
|
+
}));
|
|
1742
|
+
}
|
|
1743
|
+
return next;
|
|
1744
|
+
}
|
|
1745
|
+
function getPrimaryPart(parts, role) {
|
|
1746
|
+
return parts.find((part) => part.role === role && !part.ownerPartId);
|
|
1747
|
+
}
|
|
1748
|
+
function getSelectedToolIdsByRole(parts) {
|
|
1749
|
+
const selectedToolIdsByRole = {};
|
|
1750
|
+
for (const part of parts) if (part.source !== "provided") selectedToolIdsByRole[part.role] = part.toolId;
|
|
1751
|
+
return selectedToolIdsByRole;
|
|
1752
|
+
}
|
|
1753
|
+
function getSelectedToolIdsByRoleList(parts) {
|
|
1754
|
+
const selectedToolIdsByRoleList = {};
|
|
1755
|
+
for (const part of parts) {
|
|
1756
|
+
if (part.source === "provided") continue;
|
|
1757
|
+
selectedToolIdsByRoleList[part.role] ??= [];
|
|
1758
|
+
selectedToolIdsByRoleList[part.role]?.push(part.toolId);
|
|
1759
|
+
}
|
|
1760
|
+
return selectedToolIdsByRoleList;
|
|
1761
|
+
}
|
|
1762
|
+
function getPrimaryToolContext(parts) {
|
|
1763
|
+
const primaryToolIdsByRole = {};
|
|
1764
|
+
const primaryEcosystemsByRole = {};
|
|
1765
|
+
for (const role of PRIMARY_ROLES) {
|
|
1766
|
+
const primaryPart = getPrimaryPart(parts, role);
|
|
1767
|
+
if (primaryPart) {
|
|
1768
|
+
primaryToolIdsByRole[role] = primaryPart.toolId;
|
|
1769
|
+
primaryEcosystemsByRole[role] = primaryPart.ecosystem;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
return {
|
|
1773
|
+
primaryToolIdsByRole,
|
|
1774
|
+
primaryEcosystemsByRole
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
function getSiblingToolIdsByRole(part, parts) {
|
|
1778
|
+
const siblingToolIdsByRole = {};
|
|
1779
|
+
for (const sibling of parts) {
|
|
1780
|
+
if (sibling.ownerPartId !== part.ownerPartId || sibling.source === "provided") continue;
|
|
1781
|
+
siblingToolIdsByRole[sibling.role] = sibling.toolId;
|
|
1782
|
+
}
|
|
1783
|
+
return siblingToolIdsByRole;
|
|
1784
|
+
}
|
|
1785
|
+
function getSiblingToolIdsByRoleList(part, parts) {
|
|
1786
|
+
const siblingToolIdsByRoleList = {};
|
|
1787
|
+
for (const sibling of parts) {
|
|
1788
|
+
if (sibling.ownerPartId !== part.ownerPartId || sibling.source === "provided") continue;
|
|
1789
|
+
siblingToolIdsByRoleList[sibling.role] ??= [];
|
|
1790
|
+
siblingToolIdsByRoleList[sibling.role]?.push(sibling.toolId);
|
|
1791
|
+
}
|
|
1792
|
+
return siblingToolIdsByRoleList;
|
|
1793
|
+
}
|
|
1794
|
+
function getStackPartOptionContextForPart(part, parts) {
|
|
1795
|
+
const partsById = new Map(parts.map((candidate) => [candidate.id, candidate]));
|
|
1796
|
+
const owner = part.ownerPartId ? partsById.get(part.ownerPartId) : void 0;
|
|
1797
|
+
const { primaryToolIdsByRole, primaryEcosystemsByRole } = getPrimaryToolContext(parts);
|
|
1798
|
+
return {
|
|
1799
|
+
role: part.role,
|
|
1800
|
+
ecosystem: part.ecosystem,
|
|
1801
|
+
ownerRole: owner && PRIMARY_ROLES.has(owner.role) ? owner.role : void 0,
|
|
1802
|
+
ownerToolId: owner?.toolId,
|
|
1803
|
+
ownerEcosystem: owner?.ecosystem,
|
|
1804
|
+
siblingToolIdsByRole: getSiblingToolIdsByRole(part, parts),
|
|
1805
|
+
siblingToolIdsByRoleList: getSiblingToolIdsByRoleList(part, parts),
|
|
1806
|
+
selectedToolIdsByRole: getSelectedToolIdsByRole(parts),
|
|
1807
|
+
selectedToolIdsByRoleList: getSelectedToolIdsByRoleList(parts),
|
|
1808
|
+
primaryToolIdsByRole,
|
|
1809
|
+
primaryEcosystemsByRole
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
function addLegacyPart(parts, role, ecosystem, toolId, source, ownerPartId) {
|
|
1813
|
+
if (!toolId || toolId === "none") return void 0;
|
|
1814
|
+
const part = createStackPart({
|
|
1815
|
+
role,
|
|
1816
|
+
ecosystem,
|
|
1817
|
+
toolId,
|
|
1818
|
+
source,
|
|
1819
|
+
ownerPartId
|
|
1820
|
+
});
|
|
1821
|
+
parts.push(part);
|
|
1822
|
+
return part;
|
|
1823
|
+
}
|
|
1824
|
+
function addLegacyAddonPart(parts, toolId, source, frontendPart) {
|
|
1825
|
+
if (!toolId || toolId === "none") return;
|
|
1826
|
+
const binding = getAddonStackPartBinding(toolId);
|
|
1827
|
+
if (!binding) return;
|
|
1828
|
+
const ownerPartId = binding.ownerRole === "frontend" ? frontendPart?.id : void 0;
|
|
1829
|
+
if (binding.ownerRole === "frontend" && !ownerPartId) return;
|
|
1830
|
+
addLegacyPart(parts, binding.role, binding.ecosystem, toolId, source, ownerPartId);
|
|
1831
|
+
}
|
|
1832
|
+
function appendUniqueLegacyArrayValue(config, category, value) {
|
|
1833
|
+
if (!LEGACY_ARRAY_CATEGORIES.has(category)) return;
|
|
1834
|
+
const current = config[category] ?? [];
|
|
1835
|
+
if (!current.includes(value)) config[category] = [...current, value];
|
|
1836
|
+
}
|
|
1837
|
+
function getLegacyArrayCategoryForPart(part) {
|
|
1838
|
+
if (part.role === "examples") return "examples";
|
|
1839
|
+
if (LEGACY_ADDON_GRAPH_ROLES.has(part.role) && getAddonStackPartBinding(part.toolId)) return "addons";
|
|
1840
|
+
const legacyCategory = findDefinition(part)?.legacyCategory;
|
|
1841
|
+
if (legacyCategory && LEGACY_ARRAY_CATEGORIES.has(legacyCategory)) return legacyCategory;
|
|
1842
|
+
}
|
|
1843
|
+
function legacyProjectConfigToStackParts(config, source = "legacy") {
|
|
1844
|
+
const parts = [];
|
|
1845
|
+
const frontends = config.frontend ?? [];
|
|
1846
|
+
const webFrontends = frontends.filter((frontend) => frontend !== "none" && !NATIVE_FRONTENDS.has(frontend));
|
|
1847
|
+
const nativeFrontends = frontends.filter((frontend) => NATIVE_FRONTENDS.has(frontend));
|
|
1848
|
+
if (webFrontends.length > 1) throw new Error("Multiple primary web frontends are not supported in the stack graph.");
|
|
1849
|
+
if (nativeFrontends.length > 1) throw new Error("Multiple primary mobile frontends are not supported in the stack graph.");
|
|
1850
|
+
const webFrontend = webFrontends[0];
|
|
1851
|
+
const nativeFrontend = nativeFrontends[0];
|
|
1852
|
+
const frontendPart = addLegacyPart(parts, "frontend", "typescript", webFrontend, source);
|
|
1853
|
+
const mobilePart = addLegacyPart(parts, "mobile", "react-native", nativeFrontend, source);
|
|
1854
|
+
let backendPart;
|
|
1855
|
+
if ((config.ecosystem === void 0 || config.ecosystem === "typescript") && config.backend && config.backend !== "none") backendPart = addLegacyPart(parts, "backend", "typescript", config.backend, source);
|
|
1856
|
+
if (config.ecosystem === "rust" && config.rustFrontend && config.rustFrontend !== "none") addLegacyPart(parts, "frontend", "rust", config.rustFrontend, source);
|
|
1857
|
+
if (isLegacyBackendEcosystem(config.ecosystem)) {
|
|
1858
|
+
const category = LEGACY_BACKEND_CATEGORY_BY_ECOSYSTEM[config.ecosystem];
|
|
1859
|
+
backendPart = addLegacyPart(parts, "backend", config.ecosystem, config[category], source);
|
|
1860
|
+
}
|
|
1861
|
+
const databasePart = addLegacyPart(parts, "database", "universal", config.database, source);
|
|
1862
|
+
const capabilityOwner = backendPart?.id ?? frontendPart?.id ?? mobilePart?.id ?? databasePart?.id;
|
|
1863
|
+
if (databasePart) addLegacyPart(parts, "dbSetup", "universal", config.dbSetup, source, databasePart.id);
|
|
1864
|
+
for (const addon of config.addons ?? []) addLegacyAddonPart(parts, addon, source, frontendPart);
|
|
1865
|
+
for (const example of config.examples ?? []) addLegacyPart(parts, "examples", "universal", example, source);
|
|
1866
|
+
if (config.ecosystem === void 0 || config.ecosystem === "typescript" || config.ecosystem === "react-native") {
|
|
1867
|
+
addLegacyPart(parts, "orm", "typescript", config.orm, source, capabilityOwner);
|
|
1868
|
+
addLegacyPart(parts, "api", "typescript", config.api, source, capabilityOwner);
|
|
1869
|
+
addLegacyPart(parts, "auth", config.ecosystem ?? "typescript", config.auth, source, capabilityOwner);
|
|
1870
|
+
if (frontendPart?.ecosystem === "typescript") for (const [role, category] of Object.entries(LEGACY_TYPESCRIPT_FRONTEND_SINGLE_CATEGORIES)) addLegacyPart(parts, role, "typescript", config[category], source, frontendPart.id);
|
|
1871
|
+
if (mobilePart?.ecosystem === "react-native") for (const [role, category] of Object.entries(LEGACY_MOBILE_SINGLE_CATEGORIES)) addLegacyPart(parts, role, "react-native", config[category], source, mobilePart.id);
|
|
1872
|
+
if (backendPart?.ecosystem === "typescript") {
|
|
1873
|
+
for (const [role, category] of Object.entries(LEGACY_TYPESCRIPT_BACKEND_INFRA_CATEGORIES)) addLegacyPart(parts, role, "typescript", config[category], source, backendPart.id);
|
|
1874
|
+
for (const [role, category] of Object.entries(LEGACY_TYPESCRIPT_BACKEND_SINGLE_CATEGORIES)) addLegacyPart(parts, role, "typescript", config[category], source, backendPart.id);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
if (isLegacyBackendEcosystem(config.ecosystem)) {
|
|
1878
|
+
for (const [role, category] of Object.entries(LEGACY_CAPABILITY_CATEGORIES_BY_ECOSYSTEM[config.ecosystem])) addLegacyPart(parts, role, config.ecosystem, config[category], source, backendPart?.id);
|
|
1879
|
+
if (backendPart) {
|
|
1880
|
+
for (const [role, category] of Object.entries(LEGACY_EXTRA_CATEGORIES_BY_ECOSYSTEM[config.ecosystem])) {
|
|
1881
|
+
const toolId = config[category];
|
|
1882
|
+
if (config.ecosystem === "elixir" && toolId && ELIXIR_UNSUPPORTED_GRAPH_TOOLS.has(toolId)) continue;
|
|
1883
|
+
if (parts.some((part) => part.ownerPartId === backendPart.id && part.role === role && part.ecosystem === config.ecosystem && part.source !== "provided")) continue;
|
|
1884
|
+
addLegacyPart(parts, role, config.ecosystem, toolId, source, backendPart.id);
|
|
1885
|
+
}
|
|
1886
|
+
for (const [role, category] of Object.entries(LEGACY_BACKEND_ARRAY_CATEGORIES_BY_ECOSYSTEM[config.ecosystem])) {
|
|
1887
|
+
const values = config[category];
|
|
1888
|
+
if (!Array.isArray(values)) continue;
|
|
1889
|
+
for (const toolId of values) {
|
|
1890
|
+
if (typeof toolId !== "string") continue;
|
|
1891
|
+
addLegacyPart(parts, role, config.ecosystem, toolId, source, backendPart.id);
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
return materializeProvidedStackParts(parts);
|
|
1897
|
+
}
|
|
1898
|
+
function ecosystemForLegacy(parts) {
|
|
1899
|
+
const frontend = getPrimaryPart(parts, "frontend");
|
|
1900
|
+
if (frontend?.ecosystem && frontend.ecosystem !== "universal") return frontend.ecosystem;
|
|
1901
|
+
if (getPrimaryPart(parts, "mobile")) return "react-native";
|
|
1902
|
+
const backend = getPrimaryPart(parts, "backend");
|
|
1903
|
+
if (backend?.ecosystem && backend.ecosystem !== "universal") return backend.ecosystem;
|
|
1904
|
+
return "typescript";
|
|
1905
|
+
}
|
|
1906
|
+
function stackPartsToLegacyProjectConfigPartial(parts) {
|
|
1907
|
+
const config = {
|
|
1908
|
+
stackParts: [...parts],
|
|
1909
|
+
ecosystem: ecosystemForLegacy(parts),
|
|
1910
|
+
frontend: [],
|
|
1911
|
+
backend: "none",
|
|
1912
|
+
database: "none",
|
|
1913
|
+
orm: "none",
|
|
1914
|
+
api: "none",
|
|
1915
|
+
auth: "none"
|
|
1916
|
+
};
|
|
1917
|
+
const hasTypeScriptBackend = parts.some((part) => part.role === "backend" && part.ecosystem === "typescript" && !part.ownerPartId && part.source !== "provided");
|
|
1918
|
+
const hasTypeScriptFrontend = parts.some((part) => part.role === "frontend" && part.ecosystem === "typescript" && !part.ownerPartId && part.source !== "provided");
|
|
1919
|
+
const hasMobileFrontend = parts.some((part) => part.role === "mobile" && part.ecosystem === "react-native" && !part.ownerPartId && part.source !== "provided");
|
|
1920
|
+
if (hasTypeScriptBackend) for (const category of [...Object.values(LEGACY_TYPESCRIPT_BACKEND_SINGLE_CATEGORIES), ...Object.values(LEGACY_TYPESCRIPT_BACKEND_INFRA_CATEGORIES)]) config[category] = "none";
|
|
1921
|
+
if (hasTypeScriptFrontend) for (const category of Object.values(LEGACY_TYPESCRIPT_FRONTEND_SINGLE_CATEGORIES)) config[category] = "none";
|
|
1922
|
+
if (hasMobileFrontend) for (const category of Object.values(LEGACY_MOBILE_SINGLE_CATEGORIES)) config[category] = "none";
|
|
1923
|
+
for (const part of parts) {
|
|
1924
|
+
if (part.source === "provided") continue;
|
|
1925
|
+
if (!part.ownerPartId) {
|
|
1926
|
+
if (part.role === "frontend" && part.ecosystem === "typescript") config.frontend = [...config.frontend ?? [], part.toolId];
|
|
1927
|
+
else if (part.role === "frontend" && part.ecosystem === "rust") config.rustFrontend = part.toolId;
|
|
1928
|
+
else if (part.role === "mobile") config.frontend = [...config.frontend ?? [], part.toolId];
|
|
1929
|
+
else {
|
|
1930
|
+
const legacyArrayCategory$1 = getLegacyArrayCategoryForPart(part);
|
|
1931
|
+
if (legacyArrayCategory$1) {
|
|
1932
|
+
appendUniqueLegacyArrayValue(config, legacyArrayCategory$1, part.toolId);
|
|
1933
|
+
continue;
|
|
1934
|
+
}
|
|
1935
|
+
const legacyCategory$1 = getLegacyCategoryForPart(part, parts);
|
|
1936
|
+
if (legacyCategory$1) config[legacyCategory$1] = part.toolId;
|
|
1937
|
+
}
|
|
1938
|
+
continue;
|
|
1939
|
+
}
|
|
1940
|
+
const legacyArrayCategory = getLegacyArrayCategoryForPart(part);
|
|
1941
|
+
if (legacyArrayCategory) {
|
|
1942
|
+
appendUniqueLegacyArrayValue(config, legacyArrayCategory, part.toolId);
|
|
1943
|
+
continue;
|
|
1944
|
+
}
|
|
1945
|
+
const legacyCategory = getLegacyCategoryForPart(part, parts);
|
|
1946
|
+
if (legacyCategory) {
|
|
1947
|
+
config[legacyCategory] = part.toolId;
|
|
1948
|
+
continue;
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
return config;
|
|
1952
|
+
}
|
|
1953
|
+
function getSelectedPrimaryPart(parts, role) {
|
|
1954
|
+
return parts.find((part) => part.role === role && !part.ownerPartId && part.source !== "provided");
|
|
1955
|
+
}
|
|
1956
|
+
function getSelectedScopedPart(parts, owner, role) {
|
|
1957
|
+
if (!owner) return void 0;
|
|
1958
|
+
return parts.find((part) => part.role === role && part.ownerPartId === owner.id && part.source !== "provided");
|
|
1959
|
+
}
|
|
1960
|
+
function getLegacyCategoryForPart(part, parts) {
|
|
1961
|
+
const owner = part.ownerPartId ? parts.find((candidate) => candidate.id === part.ownerPartId) : void 0;
|
|
1962
|
+
if (owner?.role === "frontend" && owner.ecosystem === "typescript" && part.ecosystem === "typescript") {
|
|
1963
|
+
if (part.role === "auth") return "auth";
|
|
1964
|
+
const legacyCategory = LEGACY_TYPESCRIPT_FRONTEND_SINGLE_CATEGORIES[part.role];
|
|
1965
|
+
if (legacyCategory) return legacyCategory;
|
|
1966
|
+
}
|
|
1967
|
+
if (owner?.role === "backend" && owner.ecosystem === "typescript" && part.ecosystem === "typescript") {
|
|
1968
|
+
if (part.role === "orm" || part.role === "api" || part.role === "auth") return part.role;
|
|
1969
|
+
const legacyCategory = LEGACY_TYPESCRIPT_BACKEND_SINGLE_CATEGORIES[part.role] ?? LEGACY_TYPESCRIPT_BACKEND_INFRA_CATEGORIES[part.role];
|
|
1970
|
+
if (legacyCategory) return legacyCategory;
|
|
1971
|
+
}
|
|
1972
|
+
if (owner?.role === "mobile" && part.ecosystem === "react-native" && part.role === "auth") return "auth";
|
|
1973
|
+
if (owner?.role === "database" && part.ecosystem === "universal") {
|
|
1974
|
+
const legacyCategory = LEGACY_DATABASE_SINGLE_CATEGORIES[part.role];
|
|
1975
|
+
if (legacyCategory) return legacyCategory;
|
|
1976
|
+
}
|
|
1977
|
+
return findDefinition(part)?.legacyCategory;
|
|
1978
|
+
}
|
|
1979
|
+
function projectLegacyCategoryFromPart(config, part, ecosystem, parts) {
|
|
1980
|
+
if (!part) return;
|
|
1981
|
+
const legacyArrayCategory = getLegacyArrayCategoryForPart(part);
|
|
1982
|
+
if (legacyArrayCategory) {
|
|
1983
|
+
appendUniqueLegacyArrayValue(config, legacyArrayCategory, part.toolId);
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
const legacyCategory = getLegacyCategoryForPart(part, parts);
|
|
1987
|
+
if (!legacyCategory || legacyCategory === "frontend") return;
|
|
1988
|
+
if (legacyCategory === "database" || legacyCategory === "dbSetup" || part.ecosystem === ecosystem || legacyCategory === "auth" && (ecosystem === "typescript" || ecosystem === "react-native") && (part.ecosystem === "typescript" || part.ecosystem === "react-native")) config[legacyCategory] = part.toolId;
|
|
1989
|
+
}
|
|
1990
|
+
function stackGraphToLegacyProjectConfigForEcosystem(config, ecosystem) {
|
|
1991
|
+
const parts = config.stackParts ?? [];
|
|
1992
|
+
const backend = parts.find((part) => part.role === "backend" && part.ecosystem === ecosystem && !part.ownerPartId);
|
|
1993
|
+
const frontend = parts.find((part) => part.role === "frontend" && part.ecosystem === ecosystem && !part.ownerPartId);
|
|
1994
|
+
const mobile = parts.find((part) => part.role === "mobile" && part.ecosystem === "react-native" && !part.ownerPartId);
|
|
1995
|
+
const database = getSelectedPrimaryPart(parts, "database") ?? getSelectedScopedPart(parts, backend, "database");
|
|
1996
|
+
const orm = getSelectedScopedPart(parts, backend, "orm");
|
|
1997
|
+
const api = getSelectedScopedPart(parts, backend, "api");
|
|
1998
|
+
const auth = getSelectedScopedPart(parts, backend, "auth") ?? getSelectedScopedPart(parts, frontend, "auth") ?? getSelectedScopedPart(parts, mobile, "auth");
|
|
1999
|
+
const projected = {
|
|
2000
|
+
...config,
|
|
2001
|
+
ecosystem,
|
|
2002
|
+
frontend: [...frontend && frontend.ecosystem === "typescript" ? [frontend.toolId] : [], ...mobile && ["typescript", "react-native"].includes(ecosystem) ? [mobile.toolId] : []]
|
|
2003
|
+
};
|
|
2004
|
+
for (const category of GRAPH_PROJECTION_DEFAULT_LEGACY_CATEGORIES) projected[category] = "none";
|
|
2005
|
+
projected.addons = [];
|
|
2006
|
+
projected.examples = [];
|
|
2007
|
+
projectLegacyCategoryFromPart(projected, backend, ecosystem, parts);
|
|
2008
|
+
projectLegacyCategoryFromPart(projected, frontend, ecosystem, parts);
|
|
2009
|
+
projectLegacyCategoryFromPart(projected, database, ecosystem, parts);
|
|
2010
|
+
projectLegacyCategoryFromPart(projected, orm, ecosystem, parts);
|
|
2011
|
+
projectLegacyCategoryFromPart(projected, api, ecosystem, parts);
|
|
2012
|
+
projectLegacyCategoryFromPart(projected, auth, ecosystem, parts);
|
|
2013
|
+
const backendScopedPartRoles = new Set([
|
|
2014
|
+
"database",
|
|
2015
|
+
"orm",
|
|
2016
|
+
"api",
|
|
2017
|
+
"auth"
|
|
2018
|
+
]);
|
|
2019
|
+
for (const part of parts) {
|
|
2020
|
+
if (part.source === "provided" || part.ownerPartId !== backend?.id || backendScopedPartRoles.has(part.role)) continue;
|
|
2021
|
+
projectLegacyCategoryFromPart(projected, part, ecosystem, parts);
|
|
2022
|
+
}
|
|
2023
|
+
const frontendScopedPartRoles = new Set(["auth"]);
|
|
2024
|
+
for (const part of parts) {
|
|
2025
|
+
if (part.source === "provided" || part.ownerPartId !== frontend?.id || frontendScopedPartRoles.has(part.role)) continue;
|
|
2026
|
+
projectLegacyCategoryFromPart(projected, part, ecosystem, parts);
|
|
2027
|
+
}
|
|
2028
|
+
const mobileScopedPartRoles = new Set(["auth"]);
|
|
2029
|
+
for (const part of parts) {
|
|
2030
|
+
if (part.source === "provided" || part.ownerPartId !== mobile?.id || mobileScopedPartRoles.has(part.role)) continue;
|
|
2031
|
+
projectLegacyCategoryFromPart(projected, part, ecosystem, parts);
|
|
2032
|
+
}
|
|
2033
|
+
for (const part of parts) {
|
|
2034
|
+
if (part.source === "provided" || part.ownerPartId !== database?.id) continue;
|
|
2035
|
+
projectLegacyCategoryFromPart(projected, part, ecosystem, parts);
|
|
2036
|
+
}
|
|
2037
|
+
for (const part of parts) {
|
|
2038
|
+
if (part.source === "provided" || part.ownerPartId) continue;
|
|
2039
|
+
projectLegacyCategoryFromPart(projected, part, ecosystem, parts);
|
|
2040
|
+
}
|
|
2041
|
+
return projected;
|
|
2042
|
+
}
|
|
2043
|
+
function validateStackParts(parts) {
|
|
2044
|
+
const issues = [];
|
|
2045
|
+
const partsById = new Map(parts.map((part) => [part.id, part]));
|
|
2046
|
+
for (const role of PRIMARY_ROLES) if (parts.filter((part) => part.role === role && !part.ownerPartId).length > 1) issues.push({
|
|
2047
|
+
code: "DUPLICATE_PRIMARY_ROLE",
|
|
2048
|
+
role,
|
|
2049
|
+
message: `Only one primary '${role}' stack part is supported in v1.`
|
|
2050
|
+
});
|
|
2051
|
+
for (const part of parts) {
|
|
2052
|
+
const definition = findDefinition(part);
|
|
2053
|
+
if (!definition) issues.push({
|
|
2054
|
+
code: "UNSUPPORTED_ROLE_BINDING",
|
|
2055
|
+
partId: part.id,
|
|
2056
|
+
role: part.role,
|
|
2057
|
+
toolId: part.toolId,
|
|
2058
|
+
message: `'${part.toolId}' is not a valid ${part.ecosystem} tool for role '${part.role}'.`
|
|
2059
|
+
});
|
|
2060
|
+
if (!PRIMARY_ROLES.has(part.role) && !part.ownerPartId && !isOwnerlessDefinition(definition)) issues.push({
|
|
2061
|
+
code: "MISSING_OWNER_PART",
|
|
2062
|
+
partId: part.id,
|
|
2063
|
+
role: part.role,
|
|
2064
|
+
toolId: part.toolId,
|
|
2065
|
+
message: `Capability stack part '${part.id}' must reference an owning primary part.`
|
|
2066
|
+
});
|
|
2067
|
+
if (part.ownerPartId && !partsById.has(part.ownerPartId)) issues.push({
|
|
2068
|
+
code: "MISSING_OWNER_PART",
|
|
2069
|
+
partId: part.id,
|
|
2070
|
+
role: part.role,
|
|
2071
|
+
toolId: part.toolId,
|
|
2072
|
+
message: `Stack part '${part.id}' references missing owner '${part.ownerPartId}'.`
|
|
2073
|
+
});
|
|
2074
|
+
const owner = part.ownerPartId ? partsById.get(part.ownerPartId) : void 0;
|
|
2075
|
+
if (definition && (PRIMARY_ROLES.has(part.role) || owner || isOwnerlessDefinition(definition))) {
|
|
2076
|
+
const compatibilityIssue = getStackPartCompatibilityIssueForPart(part, parts);
|
|
2077
|
+
if (compatibilityIssue) issues.push(compatibilityIssue);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
2081
|
+
for (const part of parts) {
|
|
2082
|
+
const key = `${part.ownerPartId ?? "root"}:${part.role}`;
|
|
2083
|
+
byScope.set(key, [...byScope.get(key) ?? [], part]);
|
|
2084
|
+
}
|
|
2085
|
+
for (const scopedParts of byScope.values()) {
|
|
2086
|
+
const selectedParts = scopedParts.filter((part) => part.source !== "provided");
|
|
2087
|
+
if (selectedParts.length > 1 && !allowsMultipleSelectedParts(scopedParts)) {
|
|
2088
|
+
const [first] = selectedParts;
|
|
2089
|
+
issues.push({
|
|
2090
|
+
code: "DUPLICATE_ROLE_SCOPE",
|
|
2091
|
+
partId: first?.id,
|
|
2092
|
+
role: first?.role,
|
|
2093
|
+
toolId: first?.toolId,
|
|
2094
|
+
message: `Only one selected '${first?.role}' stack part is supported per owner scope.`
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
const selected = scopedParts.find((part) => part.source !== "provided");
|
|
2098
|
+
const provided = scopedParts.find((part) => part.source === "provided");
|
|
2099
|
+
if (!selected || !provided || selected.toolId === provided.toolId) continue;
|
|
2100
|
+
const provider = provided.providedByPartId ? partsById.get(provided.providedByPartId) : void 0;
|
|
2101
|
+
if (!((provider ? findDefinition(provider) : void 0)?.provides?.find((capability) => capability.role === provided.role && capability.toolId === provided.toolId))?.overrideable) issues.push({
|
|
2102
|
+
code: "PROVIDED_CAPABILITY_CONFLICT",
|
|
2103
|
+
partId: selected.id,
|
|
2104
|
+
role: selected.role,
|
|
2105
|
+
toolId: selected.toolId,
|
|
2106
|
+
message: `'${selected.toolId}' conflicts with '${provided.toolId}' provided by '${provider?.toolId ?? "another part"}'.`
|
|
2107
|
+
});
|
|
2108
|
+
}
|
|
2109
|
+
return { issues };
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
//#endregion
|
|
2113
|
+
export { isBackendUtilsCompatibleBackend as C, requiresChatSdkVercelAIForExamples as E, hasTauriCompatibleFrontend as S, isExampleChatSdkAllowed as T, UI_LIBRARY_COMPATIBILITY as _, getAddonStackPartBinding as a, hasPWACompatibleFrontend as b, getStackPartOptions as c, materializeProvidedStackParts as d, parseStackPartSpecs as f, BACKEND_UTILS_COMPATIBLE_BACKENDS as g, validateStackParts as h, formatStackPartSpec as i, getStackToolDefinitions as l, stackPartsToLegacyProjectConfigPartial as m, STACK_TOOL_DEFINITIONS as n, getRoleTargetPath as o, stackGraphToLegacyProjectConfigForEcosystem as p, createStackPart as r, getStackPartCompatibilityIssueForPart as s, ELIXIR_UNSUPPORTED_GRAPH_TOOLS as t, legacyProjectConfigToStackParts as u, getUnsupportedWebDeployFrontend as v, isExampleAIAllowed as w, hasTanStackAICompatibleFrontend as x, hasDockerComposeCompatibleFrontend as y };
|
|
2114
|
+
//# sourceMappingURL=stack-graph-DXh8F1TL.mjs.map
|