saasaloy 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/index.js +413 -21
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/schemas/manifest.schema.json +2 -1
- package/schemas/registry-item.schema.json +47 -2
- package/templates/base/AGENTS.md +8 -1
- package/templates/base/DESIGN.md +4 -0
- package/templates/base/_agents/skills/saasaloy-design/SKILL.md +1 -1
- package/templates/base/apps/web/package.json +1 -1
- package/templates/base/package.json +4 -4
- package/templates/base/packages/ui/package.json +3 -3
- package/templates/base/packages/ui/src/blocks/footer.tsx +5 -1
- package/templates/base/packages/ui/src/components/avatar.tsx +107 -0
- package/templates/base/packages/ui/src/components/collapsible.tsx +19 -0
- package/templates/base/packages/ui/src/components/dropdown-menu.tsx +270 -0
- package/templates/base/packages/ui/src/components/scroll-area.tsx +52 -0
- package/templates/base/packages/ui/src/components/sheet.tsx +132 -0
- package/templates/base/packages/ui/src/components/table.tsx +114 -0
- package/templates/base/packages/ui/src/components/tabs.tsx +81 -0
- package/templates/base/packages/ui/src/components/tooltip.tsx +64 -0
- package/templates/base/packages/ui/src/lib/sentinel.ts +1 -1
- package/templates/base/packages/ui/src/lib/theme.ts +124 -1
- package/templates/base/packages/ui/src/styles/globals.css +28 -1
- package/templates/base/stylelint.config.js +8 -0
package/README.md
CHANGED
|
@@ -91,9 +91,9 @@ Modules come in tiers. A **capability** scaffolds a workspace and sets conventio
|
|
|
91
91
|
|
|
92
92
|
| Tier | Modules |
|
|
93
93
|
|---|---|
|
|
94
|
-
| Capability | `api`, `database`, `validators`, `logger`, `auth`, `admin`, `email`, `sms`, `queue`, `infra` |
|
|
95
|
-
| Feature | `waitlist`, `teams`, `email-react` |
|
|
96
|
-
| Provider | `email-console`, `email-cloudflare`, `email-plunk`, `logger-console`, `sms-console`, `queue-cloudflare`, `queue-memory` |
|
|
94
|
+
| Capability | `api`, `database`, `validators`, `logger`, `auth`, `admin`, `email`, `sms`, `queue`, `kv`, `infra` |
|
|
95
|
+
| Feature | `waitlist`, `teams`, `email-react`, `ratelimit`, `feature-flags` |
|
|
96
|
+
| Provider | `email-console`, `email-cloudflare`, `email-plunk`, `logger-console`, `sms-console`, `queue-cloudflare`, `queue-memory`, `kv-cloudflare`, `kv-memory` |
|
|
97
97
|
| Driver | `database-d1`, `database-postgres` |
|
|
98
98
|
|
|
99
99
|
`saasaloy add <name> --dry-run` prints what a module would do to your project before it does it. The one-table map of every module, what it gives you, and what it depends on is on the [Modules](https://github.com/mimukit/saasaloy/blob/main/docs/wiki/modules.md) page.
|
|
@@ -113,8 +113,9 @@ A few modules need something the free tier does not cover:
|
|
|
113
113
|
| `database-postgres` | a Postgres server reachable from a Worker, with its URL in `DATABASE_URL`. Install instead of `database-d1`, never alongside |
|
|
114
114
|
| `sms` | a third-party SMS account for any real send. Cloudflare has no SMS product. `sms-console` is free |
|
|
115
115
|
| `queue-cloudflare` | a Workers paid plan, and the two queues created once with `wrangler queues create`. Install `queue-memory` instead for local work |
|
|
116
|
+
| `kv-cloudflare` | a Workers KV namespace created with `wrangler kv namespace create`, and its id pasted into `wrangler.jsonc`. `kv-memory` needs nothing |
|
|
116
117
|
|
|
117
|
-
The local providers (`email-console`, `sms-console`, `logger-console`, `queue-memory`) log or run inline instead of calling a service, so local development needs no plan, domain, or key. Details for each are in the [Reference](https://github.com/mimukit/saasaloy/blob/main/docs/wiki/reference.md#email-providers).
|
|
118
|
+
The local providers (`email-console`, `sms-console`, `logger-console`, `queue-memory`, `kv-memory`) log or run inline instead of calling a service, so local development needs no plan, domain, or key. Details for each are in the [Reference](https://github.com/mimukit/saasaloy/blob/main/docs/wiki/reference.md#email-providers).
|
|
118
119
|
|
|
119
120
|
## Deploy
|
|
120
121
|
|
package/dist/index.js
CHANGED
|
@@ -490,7 +490,7 @@ function insertChainedRoute(source, patch) {
|
|
|
490
490
|
return source;
|
|
491
491
|
}
|
|
492
492
|
if (!(patch.import.name in mod.imports)) {
|
|
493
|
-
mod.imports.$
|
|
493
|
+
mod.imports.$prepend({
|
|
494
494
|
from: patch.import.from,
|
|
495
495
|
imported: patch.import.name,
|
|
496
496
|
local: patch.import.name
|
|
@@ -864,6 +864,193 @@ function toDiff(before, after, filename) {
|
|
|
864
864
|
return patch;
|
|
865
865
|
}
|
|
866
866
|
|
|
867
|
+
// src/lib/patch/drizzle-column.ts
|
|
868
|
+
import { generateCode as generateCode2, parseModule as parseModule3 } from "magicast";
|
|
869
|
+
var COLUMNS_ARG = 1;
|
|
870
|
+
function insertDrizzleColumn(source, patch) {
|
|
871
|
+
const mod = parseModule3(source);
|
|
872
|
+
const columns = columnsObject(mod, patch);
|
|
873
|
+
if (!columns) {
|
|
874
|
+
return source;
|
|
875
|
+
}
|
|
876
|
+
if (indexOfColumn(columns, patch.column) !== -1) {
|
|
877
|
+
return source;
|
|
878
|
+
}
|
|
879
|
+
parseExpressionNode(patch.value);
|
|
880
|
+
if (patch.import && foreignBinding(mod.imports, patch.import)) {
|
|
881
|
+
return source;
|
|
882
|
+
}
|
|
883
|
+
const withColumn = spliceColumn(source, columns, patch);
|
|
884
|
+
if (!withColumn) {
|
|
885
|
+
return source;
|
|
886
|
+
}
|
|
887
|
+
return addImport(withColumn, patch);
|
|
888
|
+
}
|
|
889
|
+
function removeDrizzleColumn(source, patch) {
|
|
890
|
+
const mod = parseModule3(source);
|
|
891
|
+
const columns = columnsObject(mod, patch);
|
|
892
|
+
const at = columns ? indexOfColumn(columns, patch.column) : -1;
|
|
893
|
+
if (!columns || at === -1) {
|
|
894
|
+
return source;
|
|
895
|
+
}
|
|
896
|
+
if (valueDrift(columns, at, patch)) {
|
|
897
|
+
return source;
|
|
898
|
+
}
|
|
899
|
+
if (patch.import && removeBindingConflict2(mod.imports, patch.import)) {
|
|
900
|
+
return source;
|
|
901
|
+
}
|
|
902
|
+
const withoutColumn = cutColumn(source, columns, at);
|
|
903
|
+
if (!withoutColumn) {
|
|
904
|
+
return source;
|
|
905
|
+
}
|
|
906
|
+
return dropImport(withoutColumn, patch);
|
|
907
|
+
}
|
|
908
|
+
function spliceColumn(source, columns, patch) {
|
|
909
|
+
const last = columns.properties.at(-1);
|
|
910
|
+
const end = offset(last, "end");
|
|
911
|
+
if (end === void 0) {
|
|
912
|
+
return void 0;
|
|
913
|
+
}
|
|
914
|
+
const indent = indentOf(source, offset(last, "start") ?? end);
|
|
915
|
+
const comma = source.slice(end).trimStart().startsWith(",");
|
|
916
|
+
const at = comma ? end + source.slice(end).indexOf(",") + 1 : end;
|
|
917
|
+
const line = `${indent}${patch.column}: ${patch.value}`;
|
|
918
|
+
return comma ? `${source.slice(0, at)}
|
|
919
|
+
${line},${source.slice(at)}` : `${source.slice(0, at)},
|
|
920
|
+
${line}${source.slice(at)}`;
|
|
921
|
+
}
|
|
922
|
+
function cutColumn(source, columns, at) {
|
|
923
|
+
const property = columns.properties[at];
|
|
924
|
+
const start = offset(property, "start");
|
|
925
|
+
const end = offset(property, "end");
|
|
926
|
+
if (start === void 0 || end === void 0) {
|
|
927
|
+
return void 0;
|
|
928
|
+
}
|
|
929
|
+
const from = source.lastIndexOf("\n", start) + 1;
|
|
930
|
+
const after = source.indexOf("\n", end);
|
|
931
|
+
const to = after === -1 ? source.length : after + 1;
|
|
932
|
+
const head = source.slice(from, start);
|
|
933
|
+
const tail = source.slice(end, to);
|
|
934
|
+
if (head.trim() !== "" || tail.replace(/^,/u, "").trim() !== "") {
|
|
935
|
+
return void 0;
|
|
936
|
+
}
|
|
937
|
+
return source.slice(0, from) + source.slice(to);
|
|
938
|
+
}
|
|
939
|
+
function indentOf(source, at) {
|
|
940
|
+
const from = source.lastIndexOf("\n", at) + 1;
|
|
941
|
+
return /^[\t ]*/u.exec(source.slice(from, at))?.[0] ?? "";
|
|
942
|
+
}
|
|
943
|
+
function offset(node, end) {
|
|
944
|
+
const value = node?.[end];
|
|
945
|
+
return typeof value === "number" ? value : void 0;
|
|
946
|
+
}
|
|
947
|
+
function addImport(source, patch) {
|
|
948
|
+
if (!patch.import) {
|
|
949
|
+
return source;
|
|
950
|
+
}
|
|
951
|
+
const mod = parseModule3(source);
|
|
952
|
+
if (patch.import.name in mod.imports) {
|
|
953
|
+
return source;
|
|
954
|
+
}
|
|
955
|
+
mod.imports.$prepend({
|
|
956
|
+
from: patch.import.from,
|
|
957
|
+
imported: patch.import.name,
|
|
958
|
+
local: patch.import.name
|
|
959
|
+
});
|
|
960
|
+
return print(source, mod);
|
|
961
|
+
}
|
|
962
|
+
function dropImport(source, patch) {
|
|
963
|
+
if (!patch.import) {
|
|
964
|
+
return source;
|
|
965
|
+
}
|
|
966
|
+
const mod = parseModule3(source);
|
|
967
|
+
const program = mod.$ast;
|
|
968
|
+
if (!(patch.import.name in mod.imports) || isReferenced(program, patch.import.name)) {
|
|
969
|
+
return source;
|
|
970
|
+
}
|
|
971
|
+
delete mod.imports[patch.import.name];
|
|
972
|
+
return print(source, mod);
|
|
973
|
+
}
|
|
974
|
+
function drizzleColumnInsertRefusal(source, patch) {
|
|
975
|
+
const mod = parseModule3(source);
|
|
976
|
+
if (!mod.exports[patch.exportName]) {
|
|
977
|
+
return `drizzle-column could not find an export named ${JSON.stringify(patch.exportName)}`;
|
|
978
|
+
}
|
|
979
|
+
const columns = columnsObject(mod, patch);
|
|
980
|
+
if (!columns) {
|
|
981
|
+
return `${JSON.stringify(patch.exportName)} is not a Drizzle table call with a columns object as its second argument`;
|
|
982
|
+
}
|
|
983
|
+
const at = indexOfColumn(columns, patch.column);
|
|
984
|
+
if (at !== -1 && valueDrift(columns, at, patch)) {
|
|
985
|
+
return `${JSON.stringify(patch.column)} on ${JSON.stringify(patch.exportName)} holds ${printed(columns, at)}, not the ${patch.value} this patch adds`;
|
|
986
|
+
}
|
|
987
|
+
if (at === -1 && patch.import) {
|
|
988
|
+
return insertBindingConflict2(mod.imports, patch);
|
|
989
|
+
}
|
|
990
|
+
return void 0;
|
|
991
|
+
}
|
|
992
|
+
function drizzleColumnRemoveRefusal(source, patch) {
|
|
993
|
+
const mod = parseModule3(source);
|
|
994
|
+
const columns = columnsObject(mod, patch);
|
|
995
|
+
const at = columns ? indexOfColumn(columns, patch.column) : -1;
|
|
996
|
+
if (!columns || at === -1) {
|
|
997
|
+
return void 0;
|
|
998
|
+
}
|
|
999
|
+
return valueDrift(columns, at, patch) ?? (patch.import ? removeBindingConflict2(mod.imports, patch.import) : void 0);
|
|
1000
|
+
}
|
|
1001
|
+
function columnsObject(mod, patch) {
|
|
1002
|
+
const { exports } = mod;
|
|
1003
|
+
const node = exports[patch.exportName]?.$args?.[COLUMNS_ARG]?.$ast;
|
|
1004
|
+
return node?.type === "ObjectExpression" ? node : void 0;
|
|
1005
|
+
}
|
|
1006
|
+
function indexOfColumn(columns, column) {
|
|
1007
|
+
return columns.properties.findIndex(
|
|
1008
|
+
(property) => property.key?.name === column || property.key?.value === column
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
function parseExpressionNode(code) {
|
|
1012
|
+
const declaration = parseModule3(`const __drizzleColumn = ${code};`).$ast;
|
|
1013
|
+
const init = declaration.body.length === 1 ? declaration.body[0]?.declarations?.[0]?.init : void 0;
|
|
1014
|
+
if (!init) {
|
|
1015
|
+
throw new TypeError(
|
|
1016
|
+
`drizzle-column value is not a single expression: ${JSON.stringify(code)}`
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
return init;
|
|
1020
|
+
}
|
|
1021
|
+
function printed(columns, at) {
|
|
1022
|
+
return generateCode2(columns.properties[at]?.value).code;
|
|
1023
|
+
}
|
|
1024
|
+
function valueDrift(columns, at, patch) {
|
|
1025
|
+
if (collapse(printed(columns, at)) === collapse(patch.value)) {
|
|
1026
|
+
return void 0;
|
|
1027
|
+
}
|
|
1028
|
+
return `${JSON.stringify(patch.column)} on ${JSON.stringify(patch.exportName)} reads ${printed(columns, at)} now, not the ${patch.value} that was applied`;
|
|
1029
|
+
}
|
|
1030
|
+
function collapse(code) {
|
|
1031
|
+
return code.replaceAll(/\s+/gu, "");
|
|
1032
|
+
}
|
|
1033
|
+
function insertBindingConflict2(imports, patch) {
|
|
1034
|
+
const held = patch.import ? foreignBinding(imports, patch.import) : void 0;
|
|
1035
|
+
if (!held || !patch.import) {
|
|
1036
|
+
return void 0;
|
|
1037
|
+
}
|
|
1038
|
+
return `${describeBinding(patch.import.name, held)}, so ${JSON.stringify(patch.column)} would read the wrong builder`;
|
|
1039
|
+
}
|
|
1040
|
+
function removeBindingConflict2(imports, want) {
|
|
1041
|
+
const held = foreignBinding(imports, want);
|
|
1042
|
+
if (!held) {
|
|
1043
|
+
return void 0;
|
|
1044
|
+
}
|
|
1045
|
+
return `${describeBinding(want.name, held)} now, not from ${JSON.stringify(want.from)}, so the column and its import are not the ones that were applied`;
|
|
1046
|
+
}
|
|
1047
|
+
function print(source, mod) {
|
|
1048
|
+
return keepTerminator(
|
|
1049
|
+
source,
|
|
1050
|
+
generateCode2(mod, { format: { objectCurlySpacing: true } }).code
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
867
1054
|
// src/lib/patch/jsonc.ts
|
|
868
1055
|
import {
|
|
869
1056
|
applyEdits,
|
|
@@ -1138,9 +1325,9 @@ function upsertPackageJsonScript(source, patch) {
|
|
|
1138
1325
|
}
|
|
1139
1326
|
|
|
1140
1327
|
// src/lib/patch/ts-module.ts
|
|
1141
|
-
import { builders as builders2, generateCode as
|
|
1328
|
+
import { builders as builders2, generateCode as generateCode3, parseModule as parseModule4 } from "magicast";
|
|
1142
1329
|
function insertIntoPluginArray(source, patch) {
|
|
1143
|
-
const mod =
|
|
1330
|
+
const mod = parseModule4(source);
|
|
1144
1331
|
const exported = mod.exports[patch.exportName];
|
|
1145
1332
|
const callArg = exported?.$args?.[0];
|
|
1146
1333
|
if (!callArg) {
|
|
@@ -1151,7 +1338,7 @@ function insertIntoPluginArray(source, patch) {
|
|
|
1151
1338
|
return source;
|
|
1152
1339
|
}
|
|
1153
1340
|
if (!(patch.import.name in mod.imports)) {
|
|
1154
|
-
mod.imports.$
|
|
1341
|
+
mod.imports.$prepend({
|
|
1155
1342
|
from: patch.import.from,
|
|
1156
1343
|
imported: patch.import.name,
|
|
1157
1344
|
local: patch.import.name
|
|
@@ -1163,10 +1350,10 @@ function insertIntoPluginArray(source, patch) {
|
|
|
1163
1350
|
} else {
|
|
1164
1351
|
callArg[patch.arrayProp] = [newCall];
|
|
1165
1352
|
}
|
|
1166
|
-
return
|
|
1353
|
+
return print2(source, mod);
|
|
1167
1354
|
}
|
|
1168
1355
|
function removeFromPluginArray(source, patch) {
|
|
1169
|
-
const mod =
|
|
1356
|
+
const mod = parseModule4(source);
|
|
1170
1357
|
const exported = mod.exports[patch.exportName];
|
|
1171
1358
|
const callArg = exported?.$args?.[0];
|
|
1172
1359
|
if (!callArg) {
|
|
@@ -1183,7 +1370,7 @@ function removeFromPluginArray(source, patch) {
|
|
|
1183
1370
|
if (argumentDrift(array[index], patch)) {
|
|
1184
1371
|
return source;
|
|
1185
1372
|
}
|
|
1186
|
-
if (
|
|
1373
|
+
if (removeBindingConflict3(mod.imports, patch)) {
|
|
1187
1374
|
return source;
|
|
1188
1375
|
}
|
|
1189
1376
|
array.splice(index, 1);
|
|
@@ -1191,10 +1378,10 @@ function removeFromPluginArray(source, patch) {
|
|
|
1191
1378
|
if (patch.import.name in mod.imports && !isReferenced(program, patch.import.name)) {
|
|
1192
1379
|
delete mod.imports[patch.import.name];
|
|
1193
1380
|
}
|
|
1194
|
-
return
|
|
1381
|
+
return print2(source, mod);
|
|
1195
1382
|
}
|
|
1196
1383
|
function pluginArrayRemoveRefusal(source, patch) {
|
|
1197
|
-
const mod =
|
|
1384
|
+
const mod = parseModule4(source);
|
|
1198
1385
|
const callArg = mod.exports[patch.exportName]?.$args?.[0];
|
|
1199
1386
|
if (!callArg) {
|
|
1200
1387
|
return void 0;
|
|
@@ -1207,12 +1394,12 @@ function pluginArrayRemoveRefusal(source, patch) {
|
|
|
1207
1394
|
if (index === -1) {
|
|
1208
1395
|
return void 0;
|
|
1209
1396
|
}
|
|
1210
|
-
return argumentDrift(array[index], patch) ??
|
|
1397
|
+
return argumentDrift(array[index], patch) ?? removeBindingConflict3(mod.imports, patch);
|
|
1211
1398
|
}
|
|
1212
|
-
function
|
|
1399
|
+
function print2(source, mod) {
|
|
1213
1400
|
return keepTerminator(
|
|
1214
1401
|
source,
|
|
1215
|
-
|
|
1402
|
+
generateCode3(mod, { format: { objectCurlySpacing: true } }).code
|
|
1216
1403
|
);
|
|
1217
1404
|
}
|
|
1218
1405
|
function findCallIndex(array, call) {
|
|
@@ -1230,7 +1417,7 @@ function argumentDrift(element, patch) {
|
|
|
1230
1417
|
}
|
|
1231
1418
|
return `${patch.call}() in ${JSON.stringify(patch.arrayProp)} takes arguments now, not the bare call that was applied`;
|
|
1232
1419
|
}
|
|
1233
|
-
function
|
|
1420
|
+
function removeBindingConflict3(imports, patch) {
|
|
1234
1421
|
const held = foreignBinding(imports, patch.import);
|
|
1235
1422
|
if (!held) {
|
|
1236
1423
|
return void 0;
|
|
@@ -1245,6 +1432,7 @@ function isFunctionCall(value) {
|
|
|
1245
1432
|
var PATCH_KIND_KEYS = {
|
|
1246
1433
|
"chained-route": true,
|
|
1247
1434
|
"const-array": true,
|
|
1435
|
+
"drizzle-column": true,
|
|
1248
1436
|
"package-json-dependency": true,
|
|
1249
1437
|
"package-json-script": true,
|
|
1250
1438
|
"plugin-array": true,
|
|
@@ -1281,6 +1469,9 @@ function applyCodemod(source, patch) {
|
|
|
1281
1469
|
case "const-array": {
|
|
1282
1470
|
return insertIntoConstArray(source, patch);
|
|
1283
1471
|
}
|
|
1472
|
+
case "drizzle-column": {
|
|
1473
|
+
return insertDrizzleColumn(source, patch);
|
|
1474
|
+
}
|
|
1284
1475
|
case "wrangler-binding": {
|
|
1285
1476
|
return upsertWranglerBinding(source, patch);
|
|
1286
1477
|
}
|
|
@@ -1305,6 +1496,7 @@ function applyCodemod(source, patch) {
|
|
|
1305
1496
|
var INVERSES = {
|
|
1306
1497
|
"chained-route": removeChainedRoute,
|
|
1307
1498
|
"const-array": removeFromConstArray,
|
|
1499
|
+
"drizzle-column": removeDrizzleColumn,
|
|
1308
1500
|
"plugin-array": removeFromPluginArray,
|
|
1309
1501
|
"wrangler-binding": removeWranglerBinding
|
|
1310
1502
|
};
|
|
@@ -1328,10 +1520,12 @@ function reversePatch(source, patch, filename) {
|
|
|
1328
1520
|
var REFUSALS = {
|
|
1329
1521
|
"chained-route": chainedRouteInsertRefusal,
|
|
1330
1522
|
"const-array": constArrayInsertRefusal,
|
|
1523
|
+
"drizzle-column": drizzleColumnInsertRefusal,
|
|
1331
1524
|
"package-json-script": packageJsonScriptRefusal
|
|
1332
1525
|
};
|
|
1333
1526
|
var REVERSAL_REFUSALS = {
|
|
1334
1527
|
"chained-route": chainedRouteRemoveRefusal,
|
|
1528
|
+
"drizzle-column": drizzleColumnRemoveRefusal,
|
|
1335
1529
|
"plugin-array": pluginArrayRemoveRefusal,
|
|
1336
1530
|
"wrangler-binding": wranglerBindingRemoveRefusal
|
|
1337
1531
|
};
|
|
@@ -3687,7 +3881,7 @@ import { fileURLToPath as fileURLToPath3 } from "url";
|
|
|
3687
3881
|
// package.json
|
|
3688
3882
|
var package_default = {
|
|
3689
3883
|
name: "saasaloy",
|
|
3690
|
-
version: "0.
|
|
3884
|
+
version: "0.3.0",
|
|
3691
3885
|
description: "Composable SaaS accelerator kit.",
|
|
3692
3886
|
keywords: [
|
|
3693
3887
|
"saas",
|
|
@@ -3738,19 +3932,19 @@ var package_default = {
|
|
|
3738
3932
|
},
|
|
3739
3933
|
devDependencies: {
|
|
3740
3934
|
"@release-it/conventional-changelog": "12.0.0",
|
|
3741
|
-
"@types/node": "26.
|
|
3742
|
-
"@vitest/coverage-v8": "
|
|
3935
|
+
"@types/node": "26.4.1",
|
|
3936
|
+
"@vitest/coverage-v8": "5.0.0",
|
|
3743
3937
|
"release-it": "21.0.2",
|
|
3744
3938
|
tsup: "8.5.1",
|
|
3745
|
-
vitest: "
|
|
3939
|
+
vitest: "5.0.0"
|
|
3746
3940
|
},
|
|
3747
3941
|
dependencies: {
|
|
3748
3942
|
"@clack/prompts": "1.7.0",
|
|
3749
|
-
ajv: "8.
|
|
3943
|
+
ajv: "8.20.0",
|
|
3750
3944
|
diff: "9.0.0",
|
|
3751
|
-
giget: "3.3.
|
|
3945
|
+
giget: "3.3.1",
|
|
3752
3946
|
"jsonc-parser": "3.3.1",
|
|
3753
|
-
magicast: "0.5.
|
|
3947
|
+
magicast: "0.5.4",
|
|
3754
3948
|
picocolors: "1.1.1"
|
|
3755
3949
|
}
|
|
3756
3950
|
};
|
|
@@ -4055,6 +4249,8 @@ async function baseUpdateInput(args) {
|
|
|
4055
4249
|
}
|
|
4056
4250
|
|
|
4057
4251
|
// src/lib/doctor.ts
|
|
4252
|
+
import { findNodeAtLocation as findNodeAtLocation4, getNodeValue as getNodeValue4, parseTree as parseTree4 } from "jsonc-parser";
|
|
4253
|
+
import { parseModule as parseModule5 } from "magicast";
|
|
4058
4254
|
import { readFile as readFile13 } from "fs/promises";
|
|
4059
4255
|
import { basename, dirname as dirname6, join as join11, posix as posix7, resolve as resolve3 } from "path";
|
|
4060
4256
|
var SKILL_PREFIX = "saasaloy-";
|
|
@@ -4311,6 +4507,200 @@ function checkProject(args) {
|
|
|
4311
4507
|
)
|
|
4312
4508
|
);
|
|
4313
4509
|
}
|
|
4510
|
+
var KV_INDEX_FILE = "packages/kv/src/index.ts";
|
|
4511
|
+
var WRANGLER_FILE = "apps/api/wrangler.jsonc";
|
|
4512
|
+
var CLOUDFLARE_KV_MODULE = "kv-cloudflare";
|
|
4513
|
+
function checkPolicyBindings(args) {
|
|
4514
|
+
const bound = new Set(rateLimitBindingNames(args.wrangler));
|
|
4515
|
+
const findings = [];
|
|
4516
|
+
for (const entry of registeredPolicies(args)) {
|
|
4517
|
+
if (entry.name === void 0) {
|
|
4518
|
+
findings.push(
|
|
4519
|
+
finding(
|
|
4520
|
+
"ratelimit",
|
|
4521
|
+
`/policies/${String(entry.index)}`,
|
|
4522
|
+
`the policy at index ${String(entry.index)} of the \`policies\` array in ${KV_INDEX_FILE} has no name this check can read, so its RL_<NAME> binding in ${WRANGLER_FILE} was not checked. Write it as \`definePolicy({ name: "..." })\`, or as a factory exported from a file under packages/kv/src/policies/.`
|
|
4523
|
+
)
|
|
4524
|
+
);
|
|
4525
|
+
continue;
|
|
4526
|
+
}
|
|
4527
|
+
if (!bound.has(bindingFor(entry.name))) {
|
|
4528
|
+
findings.push(
|
|
4529
|
+
finding(
|
|
4530
|
+
"ratelimit",
|
|
4531
|
+
`/policies/${entry.name}`,
|
|
4532
|
+
`policy "${entry.name}" has no ${bindingFor(entry.name)} entry in ${WRANGLER_FILE} \u2014 consume({ policy: "${entry.name}" }) throws not_supported on the first request. Add the binding, or drop the policy from ${KV_INDEX_FILE}.`
|
|
4533
|
+
)
|
|
4534
|
+
);
|
|
4535
|
+
}
|
|
4536
|
+
}
|
|
4537
|
+
return findings;
|
|
4538
|
+
}
|
|
4539
|
+
function bindingFor(policy) {
|
|
4540
|
+
return `RL_${policy.toUpperCase()}`;
|
|
4541
|
+
}
|
|
4542
|
+
function rateLimitBindingNames(wrangler) {
|
|
4543
|
+
const root = parseTree4(wrangler);
|
|
4544
|
+
if (!root) {
|
|
4545
|
+
return [];
|
|
4546
|
+
}
|
|
4547
|
+
const node = findNodeAtLocation4(root, ["ratelimits"]);
|
|
4548
|
+
if (node?.type !== "array") {
|
|
4549
|
+
return [];
|
|
4550
|
+
}
|
|
4551
|
+
const names = [];
|
|
4552
|
+
for (const child of node.children ?? []) {
|
|
4553
|
+
const value = getNodeValue4(child);
|
|
4554
|
+
const name = asRecord(value).name;
|
|
4555
|
+
if (typeof name === "string") {
|
|
4556
|
+
names.push(name);
|
|
4557
|
+
}
|
|
4558
|
+
}
|
|
4559
|
+
return names;
|
|
4560
|
+
}
|
|
4561
|
+
function registeredPolicies(args) {
|
|
4562
|
+
let mod;
|
|
4563
|
+
try {
|
|
4564
|
+
mod = parseModule5(args.index);
|
|
4565
|
+
} catch {
|
|
4566
|
+
return [];
|
|
4567
|
+
}
|
|
4568
|
+
const callArg = mod.exports.kv?.$args?.[0];
|
|
4569
|
+
const array = callArg?.policies;
|
|
4570
|
+
if (!Array.isArray(array)) {
|
|
4571
|
+
return [];
|
|
4572
|
+
}
|
|
4573
|
+
const imports = mod.imports;
|
|
4574
|
+
const policies = [];
|
|
4575
|
+
for (let i = 0; i < array.length; i++) {
|
|
4576
|
+
const element = array[i];
|
|
4577
|
+
policies.push({ index: i, name: policyNameOf(element, args, imports) });
|
|
4578
|
+
}
|
|
4579
|
+
return policies;
|
|
4580
|
+
}
|
|
4581
|
+
function policyNameOf(element, args, imports) {
|
|
4582
|
+
const callee = calleeName(element);
|
|
4583
|
+
if (callee === void 0) {
|
|
4584
|
+
return void 0;
|
|
4585
|
+
}
|
|
4586
|
+
const inline = inlineNameArgument(element);
|
|
4587
|
+
if (inline !== void 0) {
|
|
4588
|
+
return inline;
|
|
4589
|
+
}
|
|
4590
|
+
const from = imports[callee]?.from;
|
|
4591
|
+
const source = typeof from === "string" ? args.policySources[from] : void 0;
|
|
4592
|
+
return source === void 0 ? void 0 : policyNameIn(source, callee);
|
|
4593
|
+
}
|
|
4594
|
+
function inlineNameArgument(element) {
|
|
4595
|
+
const args = element.$args;
|
|
4596
|
+
if (!Array.isArray(args)) {
|
|
4597
|
+
return void 0;
|
|
4598
|
+
}
|
|
4599
|
+
const name = asRecord(args[0]).name;
|
|
4600
|
+
return typeof name === "string" ? name : void 0;
|
|
4601
|
+
}
|
|
4602
|
+
function calleeName(element) {
|
|
4603
|
+
if (typeof element !== "object" || element === null) {
|
|
4604
|
+
return void 0;
|
|
4605
|
+
}
|
|
4606
|
+
const record = element;
|
|
4607
|
+
return record.$type === "function-call" && typeof record.$callee === "string" ? record.$callee : void 0;
|
|
4608
|
+
}
|
|
4609
|
+
function policyNameIn(source, callee) {
|
|
4610
|
+
let program;
|
|
4611
|
+
try {
|
|
4612
|
+
program = parseModule5(source).$ast;
|
|
4613
|
+
} catch {
|
|
4614
|
+
return void 0;
|
|
4615
|
+
}
|
|
4616
|
+
const factory = findNode(
|
|
4617
|
+
program,
|
|
4618
|
+
(node) => (node.type === "FunctionDeclaration" || node.type === "VariableDeclarator") && asRecord(node.id).name === callee
|
|
4619
|
+
);
|
|
4620
|
+
if (!factory) {
|
|
4621
|
+
return void 0;
|
|
4622
|
+
}
|
|
4623
|
+
const call = findNode(
|
|
4624
|
+
factory,
|
|
4625
|
+
(node) => node.type === "CallExpression" && asRecord(node.callee).name === "definePolicy"
|
|
4626
|
+
);
|
|
4627
|
+
const argument = asArray(call?.arguments)[0];
|
|
4628
|
+
const property = asArray(asRecord(argument).properties).find(
|
|
4629
|
+
(entry) => asRecord(asRecord(entry).key).name === "name"
|
|
4630
|
+
);
|
|
4631
|
+
const value = asRecord(asRecord(property).value).value;
|
|
4632
|
+
return typeof value === "string" ? value : void 0;
|
|
4633
|
+
}
|
|
4634
|
+
function findNode(root, accept) {
|
|
4635
|
+
if (Array.isArray(root)) {
|
|
4636
|
+
for (const child of root) {
|
|
4637
|
+
const found = findNode(child, accept);
|
|
4638
|
+
if (found) {
|
|
4639
|
+
return found;
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
4642
|
+
return void 0;
|
|
4643
|
+
}
|
|
4644
|
+
if (typeof root !== "object" || root === null) {
|
|
4645
|
+
return void 0;
|
|
4646
|
+
}
|
|
4647
|
+
const node = root;
|
|
4648
|
+
if (typeof node.type === "string") {
|
|
4649
|
+
const typed = node;
|
|
4650
|
+
if (accept(typed)) {
|
|
4651
|
+
return typed;
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4654
|
+
for (const [key, value] of Object.entries(node)) {
|
|
4655
|
+
if (key === "loc" || key === "comments" || key === "original") {
|
|
4656
|
+
continue;
|
|
4657
|
+
}
|
|
4658
|
+
const found = findNode(value, accept);
|
|
4659
|
+
if (found) {
|
|
4660
|
+
return found;
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
return void 0;
|
|
4664
|
+
}
|
|
4665
|
+
async function readPolicyState(root, installed) {
|
|
4666
|
+
if (!installed.includes(CLOUDFLARE_KV_MODULE)) {
|
|
4667
|
+
return void 0;
|
|
4668
|
+
}
|
|
4669
|
+
const index = await readIfPresent(resolveWithinRoot(root, KV_INDEX_FILE));
|
|
4670
|
+
const wrangler = await readIfPresent(resolveWithinRoot(root, WRANGLER_FILE));
|
|
4671
|
+
if (index === void 0 || wrangler === void 0) {
|
|
4672
|
+
return void 0;
|
|
4673
|
+
}
|
|
4674
|
+
const policySources = {};
|
|
4675
|
+
for (const specifier of relativeImportsIn(index)) {
|
|
4676
|
+
const source = await readIfPresent(
|
|
4677
|
+
resolveWithinRoot(
|
|
4678
|
+
root,
|
|
4679
|
+
posix7.join(posix7.dirname(KV_INDEX_FILE), `${specifier}.ts`)
|
|
4680
|
+
)
|
|
4681
|
+
);
|
|
4682
|
+
if (source !== void 0) {
|
|
4683
|
+
policySources[specifier] = source;
|
|
4684
|
+
}
|
|
4685
|
+
}
|
|
4686
|
+
return { index, policySources, wrangler };
|
|
4687
|
+
}
|
|
4688
|
+
function relativeImportsIn(index) {
|
|
4689
|
+
let mod;
|
|
4690
|
+
try {
|
|
4691
|
+
mod = parseModule5(index);
|
|
4692
|
+
} catch {
|
|
4693
|
+
return [];
|
|
4694
|
+
}
|
|
4695
|
+
const imports = mod.imports;
|
|
4696
|
+
const specifiers = /* @__PURE__ */ new Set();
|
|
4697
|
+
for (const held of Object.values(imports)) {
|
|
4698
|
+
if (typeof held?.from === "string" && held.from.startsWith(".")) {
|
|
4699
|
+
specifiers.add(held.from);
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
return [...specifiers];
|
|
4703
|
+
}
|
|
4314
4704
|
async function resolveDoctorTarget(path) {
|
|
4315
4705
|
const dir = resolve3(path);
|
|
4316
4706
|
if (await pathExists(join11(dir, "registry-item.json"))) {
|
|
@@ -4456,9 +4846,11 @@ async function reportProject(path) {
|
|
|
4456
4846
|
seedFiles: (await readBaseDeclaration(await baseTemplateDir())).seedFiles
|
|
4457
4847
|
});
|
|
4458
4848
|
note2(wrapForNote(renderBase(base)), "Base");
|
|
4849
|
+
const policyState = await readPolicyState(path, config.installed);
|
|
4459
4850
|
const findings = [
|
|
4460
4851
|
...checkProject({ config, manifest }),
|
|
4461
|
-
...checkPartialInstalls({ installed: config.installed, manifest })
|
|
4852
|
+
...checkPartialInstalls({ installed: config.installed, manifest }),
|
|
4853
|
+
...policyState ? checkPolicyBindings(policyState) : []
|
|
4462
4854
|
];
|
|
4463
4855
|
if (findings.length === 0) {
|
|
4464
4856
|
const count = config.installed.length;
|