@supacloud/compiler 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1213,11 +1213,11 @@ export function createGraphqlClient(options: GraphqlClientOptions) {
1213
1213
  }
1214
1214
  endpoint.pathname = endpoint.pathname.replace(/\\/$/, "") + "/graphql/v1";
1215
1215
  const fetcher = options.fetch ?? globalThis.fetch.bind(globalThis);
1216
- return getSdk<GraphqlRequestOptions>(async <R, V>(
1216
+ return getSdk<GraphqlRequestOptions>(async (
1217
1217
  query: string,
1218
- variables?: V,
1218
+ variables?: unknown,
1219
1219
  request?: GraphqlRequestOptions,
1220
- ): Promise<R> => {
1220
+ ): Promise<unknown> => {
1221
1221
  const headers = new Headers({ "Content-Type": "application/json", Accept: "application/json" });
1222
1222
  if (options.publishableKey) headers.set("apikey", options.publishableKey);
1223
1223
  const token = await options.getAccessToken?.();
@@ -1226,7 +1226,7 @@ export function createGraphqlClient(options: GraphqlClientOptions) {
1226
1226
  method: "POST",
1227
1227
  headers,
1228
1228
  body: JSON.stringify({ query, variables }),
1229
- signal: request?.signal,
1229
+ ...(request?.signal ? { signal: request.signal } : {}),
1230
1230
  redirect: "error",
1231
1231
  });
1232
1232
  if (!response.ok) {
@@ -1252,8 +1252,8 @@ export function createGraphqlClient(options: GraphqlClientOptions) {
1252
1252
  if (!("data" in envelope) || !envelope.data || typeof envelope.data !== "object" || Array.isArray(envelope.data)) {
1253
1253
  throw new GraphqlRequestError("invalid-response", "GraphQL returned no result object.", response.status);
1254
1254
  }
1255
- // Operation types describe the schema snapshot, not runtime response validation.
1256
- return envelope.data as R;
1255
+ // The operation-specific parser in getSdk validates selected field values.
1256
+ return envelope.data;
1257
1257
  });
1258
1258
  }
1259
1259
  `;
@@ -1327,6 +1327,165 @@ var init_graphql_inputs = __esm(() => {
1327
1327
  init_graphql_options();
1328
1328
  });
1329
1329
 
1330
+ // src/graphql-runtime.ts
1331
+ import * as ts6 from "@typescript/typescript6";
1332
+ import { resolve as resolve4 } from "node:path";
1333
+ function renderGraphqlValidators(source, operationNames) {
1334
+ const fileName = resolve4("/__supacloud_graphql__/contracts.ts");
1335
+ const options = {
1336
+ strict: true,
1337
+ noUncheckedIndexedAccess: true,
1338
+ exactOptionalPropertyTypes: true,
1339
+ noImplicitOverride: true,
1340
+ noPropertyAccessFromIndexSignature: true,
1341
+ noFallthroughCasesInSwitch: true,
1342
+ skipLibCheck: false,
1343
+ target: ts6.ScriptTarget.ES2022,
1344
+ lib: ["lib.es2022.d.ts"],
1345
+ types: [],
1346
+ noEmit: true
1347
+ };
1348
+ const host = ts6.createCompilerHost(options);
1349
+ const getSourceFile = host.getSourceFile.bind(host);
1350
+ host.getSourceFile = (path, languageVersion, onError, shouldCreateNewSourceFile) => path === fileName ? ts6.createSourceFile(path, source, languageVersion, true) : getSourceFile(path, languageVersion, onError, shouldCreateNewSourceFile);
1351
+ const program = ts6.createProgram([fileName], options, host);
1352
+ const diagnostics = ts6.getPreEmitDiagnostics(program);
1353
+ if (diagnostics.length) {
1354
+ throw new Error(`Generated GraphQL types cannot be validated: ${diagnostics.map((item) => ts6.flattenDiagnosticMessageText(item.messageText, `
1355
+ `)).join("; ")}`);
1356
+ }
1357
+ const checker = program.getTypeChecker();
1358
+ const entry = program.getSourceFile(fileName);
1359
+ const module = entry && checker.getSymbolAtLocation(entry);
1360
+ if (!module)
1361
+ throw new Error("Generated GraphQL types have no module");
1362
+ const exports = new Map(checker.getExportsOfModule(module).map((symbol) => [symbol.name, symbol]));
1363
+ if (exports.has("GraphqlQueryResults")) {
1364
+ throw new Error("GraphQL type GraphqlQueryResults conflicts with the generated result registry.");
1365
+ }
1366
+ const names = new Map;
1367
+ const definitions = new Map;
1368
+ function unsupported(type) {
1369
+ throw new Error(`Unsupported GraphQL result wire type: ${checker.typeToString(type)}. Map custom scalars to JSON wire types or unknown.`);
1370
+ }
1371
+ function reference(type) {
1372
+ const existing = names.get(type);
1373
+ if (existing)
1374
+ return existing;
1375
+ const name = `checkGraphqlValue${names.size}`;
1376
+ names.set(type, name);
1377
+ definitions.set(name, "");
1378
+ definitions.set(name, `function ${name}(value: unknown): boolean {
1379
+ return ${expression(type)};
1380
+ }`);
1381
+ return name;
1382
+ }
1383
+ function expression(type) {
1384
+ if (type.flags & ts6.TypeFlags.Any)
1385
+ return unsupported(type);
1386
+ if (type.flags & ts6.TypeFlags.Unknown)
1387
+ return "true";
1388
+ if (type.flags & ts6.TypeFlags.Never)
1389
+ return "false";
1390
+ if (type.flags & ts6.TypeFlags.Null)
1391
+ return "value === null";
1392
+ if (type.flags & ts6.TypeFlags.Undefined)
1393
+ return "value === undefined";
1394
+ if (type.isStringLiteral() || type.isNumberLiteral())
1395
+ return `value === ${JSON.stringify(type.value)}`;
1396
+ if (type.flags & ts6.TypeFlags.BooleanLiteral)
1397
+ return `value === ${checker.typeToString(type)}`;
1398
+ if (type.flags & ts6.TypeFlags.String)
1399
+ return 'typeof value === "string"';
1400
+ if (type.flags & ts6.TypeFlags.Number)
1401
+ return 'typeof value === "number" && Number.isFinite(value)';
1402
+ if (type.flags & ts6.TypeFlags.Boolean)
1403
+ return 'typeof value === "boolean"';
1404
+ if (type.isUnion())
1405
+ return type.types.map((part) => `${reference(part)}(value)`).join(" || ");
1406
+ if (type.isIntersection())
1407
+ return type.types.map((part) => `${reference(part)}(value)`).join(" && ");
1408
+ if (checker.isTupleType(type))
1409
+ return unsupported(type);
1410
+ if (checker.isArrayType(type)) {
1411
+ const item = checker.getIndexTypeOfType(type, ts6.IndexKind.Number);
1412
+ if (!item)
1413
+ return unsupported(type);
1414
+ return `isGraphqlArray(value) && Array.from(value).every(${reference(item)})`;
1415
+ }
1416
+ if (type.flags & ts6.TypeFlags.Object) {
1417
+ if (type.getCallSignatures().length || type.getConstructSignatures().length)
1418
+ return unsupported(type);
1419
+ const indexes = checker.getIndexInfosOfType(type);
1420
+ if (indexes.some((index) => !(index.keyType.flags & ts6.TypeFlags.String)))
1421
+ return unsupported(type);
1422
+ const properties = checker.getPropertiesOfType(type).map((property) => {
1423
+ const declaration = property.valueDeclaration ?? property.declarations?.[0];
1424
+ if (!declaration)
1425
+ return unsupported(type);
1426
+ const check = reference(checker.getTypeOfSymbolAtLocation(property, declaration));
1427
+ const key = JSON.stringify(property.name);
1428
+ const present = `Object.prototype.hasOwnProperty.call(value, ${key})`;
1429
+ return property.flags & ts6.SymbolFlags.Optional ? `(!${present} || ${check}(value[${key}]))` : `(${present} && ${check}(value[${key}]))`;
1430
+ });
1431
+ const indexedValues = indexes.map((index) => `Object.values(value).every(${reference(index.type)})`);
1432
+ return ["isGraphqlRecord(value)", ...properties, ...indexedValues].join(" && ");
1433
+ }
1434
+ return unsupported(type);
1435
+ }
1436
+ const operations = [...operationNames].sort();
1437
+ const parsers = operations.map((name) => {
1438
+ const typeName = `${name}Query`;
1439
+ const symbol = exports.get(typeName);
1440
+ if (!symbol)
1441
+ throw new Error(`Missing generated operation type: ${typeName}`);
1442
+ const check = reference(checker.getDeclaredTypeOfSymbol(symbol));
1443
+ return `export function is${typeName}(value: unknown): value is ${typeName} {
1444
+ return ${check}(value);
1445
+ }
1446
+ export function parse${typeName}(value: unknown): ${typeName} {
1447
+ if (!is${typeName}(value)) {
1448
+ throw new GraphqlRequestError("invalid-response", ${JSON.stringify(`GraphQL result does not match ${name}.`)});
1449
+ }
1450
+ return value;
1451
+ }`;
1452
+ });
1453
+ return `
1454
+ function isGraphqlRecord(value: unknown): value is Record<string, unknown> {
1455
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1456
+ }
1457
+ function isGraphqlArray(value: unknown): value is unknown[] {
1458
+ return Array.isArray(value);
1459
+ }
1460
+ ${[...definitions.values()].join(`
1461
+ `)}
1462
+ ${parsers.join(`
1463
+ `)}
1464
+ export interface GraphqlQueryResults {
1465
+ ${operations.map((name) => ` ${JSON.stringify(name)}: ${name}Query;`).join(`
1466
+ `)}
1467
+ }
1468
+ export function isGraphqlResult<Name extends keyof GraphqlQueryResults>(
1469
+ name: Name, value: unknown,
1470
+ ): value is GraphqlQueryResults[Name] {
1471
+ switch (name) {
1472
+ ${operations.map((name) => ` case ${JSON.stringify(name)}: return is${name}Query(value);`).join(`
1473
+ `)}
1474
+ default: return false;
1475
+ }
1476
+ }
1477
+ export function parseGraphqlResult<Name extends keyof GraphqlQueryResults>(
1478
+ name: Name, value: unknown,
1479
+ ): GraphqlQueryResults[Name] {
1480
+ if (!isGraphqlResult(name, value)) {
1481
+ throw new GraphqlRequestError("invalid-response", "GraphQL result does not match operation " + name + ".");
1482
+ }
1483
+ return value;
1484
+ }
1485
+ `;
1486
+ }
1487
+ var init_graphql_runtime = () => {};
1488
+
1330
1489
  // src/graphql.ts
1331
1490
  var exports_graphql = {};
1332
1491
  __export(exports_graphql, {
@@ -1350,7 +1509,6 @@ import {
1350
1509
  validateSchema
1351
1510
  } from "graphql";
1352
1511
  import { codegen } from "@graphql-codegen/core";
1353
- import * as typescript from "@graphql-codegen/typescript";
1354
1512
  import * as operations from "@graphql-codegen/typescript-operations";
1355
1513
  async function renderGraphql(options) {
1356
1514
  const result = { diagnostics: [], files: {} };
@@ -1456,9 +1614,8 @@ async function renderGraphql(options) {
1456
1614
  try {
1457
1615
  const config = {
1458
1616
  useTypeImports: true,
1459
- skipTypename: true,
1460
- enumsAsTypes: true,
1461
- onlyOperationTypes: true,
1617
+ nonOptionalTypename: false,
1618
+ enumType: "string-literal",
1462
1619
  namingConvention: "keep",
1463
1620
  dedupeOperationSuffix: false,
1464
1621
  omitOperationSuffix: false,
@@ -1471,21 +1628,21 @@ async function renderGraphql(options) {
1471
1628
  schemaAst: schema,
1472
1629
  documents,
1473
1630
  config,
1474
- plugins: [{ typescript: {} }, { operations: {} }],
1475
- pluginMap: { typescript, operations }
1631
+ plugins: [{ operations: {} }],
1632
+ pluginMap: { operations }
1476
1633
  });
1477
1634
  const separated = separateOperations(combined);
1478
1635
  const methods = Object.entries(separated).sort(([a], [b]) => a.localeCompare(b)).map(([name, document]) => {
1479
1636
  const operation = document.definitions.find((node) => node.kind === Kind.OPERATION_DEFINITION);
1480
- if (operation.kind !== Kind.OPERATION_DEFINITION)
1637
+ if (!operation || operation.kind !== Kind.OPERATION_DEFINITION)
1481
1638
  throw new Error("Missing query operation");
1482
1639
  const required = operation.variableDefinitions?.some((variable) => variable.type.kind === Kind.NON_NULL_TYPE && !variable.defaultValue);
1483
- return ` ${JSON.stringify(name)}(variables${required ? "" : "?"}: ${name}QueryVariables, options?: C): Promise<${name}Query> {
1484
- return requester<${name}Query, ${name}QueryVariables>(${JSON.stringify(print(document))}, variables, options);
1640
+ return ` async ${JSON.stringify(name)}(variables${required ? "" : "?"}: ${name}QueryVariables, options?: C): Promise<${name}Query> {
1641
+ return parse${name}Query(await requester(${JSON.stringify(print(document))}, variables, options));
1485
1642
  }`;
1486
1643
  });
1487
1644
  const facade = `
1488
- export type Requester<C> = <R, V>(query: string, variables?: V, options?: C) => Promise<R>;
1645
+ export type Requester<C> = (query: string, variables?: unknown, options?: C) => Promise<unknown>;
1489
1646
  export function getSdk<C>(requester: Requester<C>) {
1490
1647
  return {
1491
1648
  ${methods.join(`,
@@ -1493,8 +1650,9 @@ ${methods.join(`,
1493
1650
  };
1494
1651
  }
1495
1652
  `;
1653
+ const validators = renderGraphqlValidators(generated, queryEntries.map((entry) => entry.name));
1496
1654
  result.files["graphql.ts"] = `// GENERATED BY @supacloud/compiler. DO NOT EDIT.
1497
- ` + generated + facade + GRAPHQL_CLIENT_SOURCE;
1655
+ ` + generated + validators + facade + GRAPHQL_CLIENT_SOURCE;
1498
1656
  if (options.graphql.typedDocuments) {
1499
1657
  const typedDocuments = await import("@graphql-codegen/typed-document-node");
1500
1658
  result.files["graphql.documents.ts"] = `// GENERATED BY @supacloud/compiler. DO NOT EDIT.
@@ -1504,8 +1662,8 @@ ${methods.join(`,
1504
1662
  schemaAst: schema,
1505
1663
  documents,
1506
1664
  config,
1507
- plugins: [{ typescript: {} }, { operations: {} }, { typedDocuments: {} }],
1508
- pluginMap: { typescript, operations, typedDocuments }
1665
+ plugins: [{ operations: {} }, { typedDocuments: {} }],
1666
+ pluginMap: { operations, typedDocuments }
1509
1667
  });
1510
1668
  }
1511
1669
  result.files["graphql.manifest.json"] = JSON.stringify({
@@ -1523,6 +1681,7 @@ ${methods.join(`,
1523
1681
  var init_graphql = __esm(() => {
1524
1682
  init_graphql_inputs();
1525
1683
  init_graphql_options();
1684
+ init_graphql_runtime();
1526
1685
  });
1527
1686
 
1528
1687
  // src/graphql-schema.ts
@@ -1532,7 +1691,7 @@ __export(exports_graphql_schema, {
1532
1691
  });
1533
1692
  import { mkdir as mkdir2, readFile as readFile4 } from "node:fs/promises";
1534
1693
  import { createHash as createHash7 } from "node:crypto";
1535
- import { dirname as dirname7, resolve as resolve8 } from "node:path";
1694
+ import { dirname as dirname7, resolve as resolve9 } from "node:path";
1536
1695
  async function pullGraphqlSchema(options) {
1537
1696
  assertGraphqlOptions({ schema: options.output });
1538
1697
  const endpoint = new URL(options.url);
@@ -1572,7 +1731,7 @@ async function pullGraphqlSchema(options) {
1572
1731
  throw new Error("GraphQL schema export failed. Verify caller grants and enable introspection only in the intended development environment.");
1573
1732
  }
1574
1733
  const schema = lexicographicSortSchema(buildClientSchema2(data));
1575
- const path = resolve8(options.output);
1734
+ const path = resolve9(options.output);
1576
1735
  const content = path.endsWith(".json") ? JSON.stringify(introspectionFromSchema(schema), null, 2) + `
1577
1736
  ` : `# GENERATED BY supacloud-compiler graphql-schema. DO NOT EDIT.
1578
1737
  # Database First: change database declarations, apply migrations, then re-export for the intended role.
@@ -1599,7 +1758,7 @@ var init_graphql_schema = __esm(() => {
1599
1758
  });
1600
1759
 
1601
1760
  // src/cli.ts
1602
- import { resolve as resolve9 } from "node:path";
1761
+ import { resolve as resolve10 } from "node:path";
1603
1762
  import { readFile as readFile5 } from "node:fs/promises";
1604
1763
 
1605
1764
  // src/analyze.ts
@@ -5702,12 +5861,12 @@ function exportGraphDot(graph) {
5702
5861
 
5703
5862
  // src/watch.ts
5704
5863
  import { existsSync as existsSync5, watch } from "node:fs";
5705
- import { dirname as dirname5, relative as relative7, resolve as resolve5, sep as sep7 } from "node:path";
5864
+ import { dirname as dirname5, relative as relative7, resolve as resolve6, sep as sep7 } from "node:path";
5706
5865
 
5707
5866
  // src/incremental.ts
5708
5867
  import { createHash as createHash6 } from "node:crypto";
5709
5868
  import { access as access2, readdir, readFile as readFile2 } from "node:fs/promises";
5710
- import { isAbsolute, relative as relative6, resolve as resolve4, sep as sep6 } from "node:path";
5869
+ import { isAbsolute, relative as relative6, resolve as resolve5, sep as sep6 } from "node:path";
5711
5870
  init_graphql_inputs();
5712
5871
  function createDependencyGraphCache() {
5713
5872
  return {
@@ -5782,11 +5941,11 @@ function createIncrementalCompiler() {
5782
5941
  };
5783
5942
  }
5784
5943
  async function updateSnapshot(previous, options, changedPaths) {
5785
- const rootDir = resolve4(options.rootDir);
5786
- const outDir = resolve4(options.outDir);
5944
+ const rootDir = resolve5(options.rootDir);
5945
+ const outDir = resolve5(options.outDir);
5787
5946
  const files = { ...previous.files };
5788
5947
  for (const changedPath of changedPaths) {
5789
- const absolutePath = isAbsolute(changedPath) ? resolve4(changedPath) : resolve4(rootDir, changedPath);
5948
+ const absolutePath = isAbsolute(changedPath) ? resolve5(changedPath) : resolve5(rootDir, changedPath);
5790
5949
  const relativeChangedPath = relative6(rootDir, absolutePath);
5791
5950
  if (relativeChangedPath === ".." || relativeChangedPath.startsWith(`..${sep6}`))
5792
5951
  continue;
@@ -5804,8 +5963,8 @@ async function updateSnapshot(previous, options, changedPaths) {
5804
5963
  return { files, optionsKey: optionsKeyOf(options) };
5805
5964
  }
5806
5965
  async function createSnapshot(options) {
5807
- const rootDir = resolve4(options.rootDir);
5808
- const outDir = resolve4(options.outDir);
5966
+ const rootDir = resolve5(options.rootDir);
5967
+ const outDir = resolve5(options.outDir);
5809
5968
  const paths = await listSourceFiles(rootDir, outDir);
5810
5969
  const files = {};
5811
5970
  for (const path of paths) {
@@ -5824,8 +5983,8 @@ async function createSnapshot(options) {
5824
5983
  }
5825
5984
  function optionsKeyOf(options) {
5826
5985
  return JSON.stringify({
5827
- rootDir: resolve4(options.rootDir),
5828
- outDir: resolve4(options.outDir),
5986
+ rootDir: resolve5(options.rootDir),
5987
+ outDir: resolve5(options.outDir),
5829
5988
  include: options.include,
5830
5989
  strict: options.strict,
5831
5990
  writeOnError: options.writeOnError,
@@ -5847,7 +6006,7 @@ async function listSourceFiles(rootDir, outDir) {
5847
6006
  const result = [];
5848
6007
  const visit = async (directory) => {
5849
6008
  for (const entry of await readdir(directory, { withFileTypes: true })) {
5850
- const path = resolve4(directory, entry.name);
6009
+ const path = resolve5(directory, entry.name);
5851
6010
  if (entry.isDirectory()) {
5852
6011
  if (entry.name === "node_modules" || entry.name === ".git" || path === outDir)
5853
6012
  continue;
@@ -5971,8 +6130,8 @@ function findAffectedModules(previous, current, changedFiles) {
5971
6130
  // src/watch.ts
5972
6131
  var DEFAULT_DEBOUNCE_MS = 100;
5973
6132
  function watchProject(options) {
5974
- const rootDir = resolve5(options.rootDir);
5975
- const outDir = resolve5(options.outDir);
6133
+ const rootDir = resolve6(options.rootDir);
6134
+ const outDir = resolve6(options.outDir);
5976
6135
  const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
5977
6136
  let timer;
5978
6137
  let closed = false;
@@ -5981,7 +6140,7 @@ function watchProject(options) {
5981
6140
  const pendingPaths = new Set;
5982
6141
  let watcher;
5983
6142
  let schemaWatcher;
5984
- const schemaPath = options.graphql ? resolve5(rootDir, options.graphql.schema) : undefined;
6143
+ const schemaPath = options.graphql ? resolve6(rootDir, options.graphql.schema) : undefined;
5985
6144
  const incremental = createIncrementalCompiler();
5986
6145
  let initialEvent;
5987
6146
  let resolveReady = () => {
@@ -6058,7 +6217,7 @@ function watchProject(options) {
6058
6217
  watcher = watch(rootDir, { recursive: true }, (_eventType, filename) => {
6059
6218
  if (!filename)
6060
6219
  return schedule();
6061
- const changedPath = resolve5(rootDir, filename.toString());
6220
+ const changedPath = resolve6(rootDir, filename.toString());
6062
6221
  const relativePath = relative7(outDir, changedPath);
6063
6222
  if (!relativePath.startsWith("..") && relativePath !== "")
6064
6223
  return;
@@ -6071,7 +6230,7 @@ function watchProject(options) {
6071
6230
  while (!existsSync5(directory) && dirname5(directory) !== directory)
6072
6231
  directory = dirname5(directory);
6073
6232
  schemaWatcher = watch(directory, { recursive: true }, (_eventType, filename) => {
6074
- if (!filename || resolve5(directory, filename.toString()) === schemaPath)
6233
+ if (!filename || resolve6(directory, filename.toString()) === schemaPath)
6075
6234
  schedule(schemaPath);
6076
6235
  });
6077
6236
  }
@@ -6098,7 +6257,7 @@ function watchProject(options) {
6098
6257
  // src/config.ts
6099
6258
  init_graphql_options();
6100
6259
  import { existsSync as existsSync6 } from "node:fs";
6101
- import { join as join6, resolve as resolve6 } from "node:path";
6260
+ import { join as join6, resolve as resolve7 } from "node:path";
6102
6261
  import { pathToFileURL } from "node:url";
6103
6262
  var DEFAULT_SUPACLOUD_CONFIG = {
6104
6263
  graphql: false,
@@ -6115,6 +6274,7 @@ var DEFAULT_SUPACLOUD_CONFIG = {
6115
6274
  function defineSupacloudConfig(config = {}) {
6116
6275
  if (config.graphql !== undefined && config.graphql !== false)
6117
6276
  assertGraphqlOptions(config.graphql);
6277
+ validateGovernanceConfig(config);
6118
6278
  return {
6119
6279
  ...DEFAULT_SUPACLOUD_CONFIG,
6120
6280
  ...config,
@@ -6122,11 +6282,35 @@ function defineSupacloudConfig(config = {}) {
6122
6282
  graphql: config.graphql ?? DEFAULT_SUPACLOUD_CONFIG.graphql
6123
6283
  };
6124
6284
  }
6285
+ function validateGovernanceConfig(config) {
6286
+ const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
6287
+ const isStrings = (value) => Array.isArray(value) && Array.from(value).every((item) => typeof item === "string" && item.trim().length > 0);
6288
+ if (config.moduleBoundaries !== undefined) {
6289
+ if (!Array.isArray(config.moduleBoundaries))
6290
+ throw new Error("moduleBoundaries must be an array of module tag rules.");
6291
+ const rules = config.moduleBoundaries;
6292
+ for (const rule of rules) {
6293
+ if (!isRecord(rule) || typeof rule["sourceTag"] !== "string" || !rule["sourceTag"].trim() || Object.keys(rule).some((key) => !["sourceTag", "onlyDependOnLibsWithTags", "bannedDependenciesWithTags"].includes(key)) || rule["onlyDependOnLibsWithTags"] !== undefined && !isStrings(rule["onlyDependOnLibsWithTags"]) || rule["bannedDependenciesWithTags"] !== undefined && !isStrings(rule["bannedDependenciesWithTags"])) {
6294
+ throw new Error("moduleBoundaries rules require sourceTag and optional string arrays onlyDependOnLibsWithTags/bannedDependenciesWithTags.");
6295
+ }
6296
+ }
6297
+ }
6298
+ if (config.typeSafety !== undefined) {
6299
+ const rules = config.typeSafety;
6300
+ if (!isRecord(rules) || Object.keys(rules).some((key) => !["scanProductionSource", "noAnyInGenerated", "exclude"].includes(key)) || rules["scanProductionSource"] !== undefined && typeof rules["scanProductionSource"] !== "boolean" || rules["noAnyInGenerated"] !== undefined && typeof rules["noAnyInGenerated"] !== "boolean" || rules["exclude"] !== undefined && !isStrings(rules["exclude"])) {
6301
+ throw new Error("typeSafety accepts boolean scanProductionSource/noAnyInGenerated and a string array exclude.");
6302
+ }
6303
+ }
6304
+ for (const key of ["allowRouteCommandBindings", "disallowControllerDirectDb", "detectOrphanModules"]) {
6305
+ if (config[key] !== undefined && typeof config[key] !== "boolean")
6306
+ throw new Error(`${key} must be a boolean.`);
6307
+ }
6308
+ }
6125
6309
  function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
6126
6310
  const resolved = defineSupacloudConfig(config);
6127
6311
  return {
6128
- rootDir: resolve6(cwd, resolved.root ?? DEFAULT_SUPACLOUD_CONFIG.root),
6129
- outDir: resolve6(cwd, resolved.outDir ?? DEFAULT_SUPACLOUD_CONFIG.outDir),
6312
+ rootDir: resolve7(cwd, resolved.root ?? DEFAULT_SUPACLOUD_CONFIG.root),
6313
+ outDir: resolve7(cwd, resolved.outDir ?? DEFAULT_SUPACLOUD_CONFIG.outDir),
6130
6314
  include: resolved.include ?? [...DEFAULT_SUPACLOUD_CONFIG.include],
6131
6315
  strict: resolved.strict ?? DEFAULT_SUPACLOUD_CONFIG.strict,
6132
6316
  requireRouteContracts: resolved.requireRouteContracts ?? DEFAULT_SUPACLOUD_CONFIG.requireRouteContracts,
@@ -6134,10 +6318,15 @@ function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
6134
6318
  generatePermissions: resolved.generatePermissions ?? DEFAULT_SUPACLOUD_CONFIG.generatePermissions,
6135
6319
  moduleBoundaryPreset: resolved.moduleBoundaryPreset ?? DEFAULT_SUPACLOUD_CONFIG.moduleBoundaryPreset,
6136
6320
  commandCapabilities: resolved.commandCapabilities,
6321
+ ...resolved.moduleBoundaries ? { moduleBoundaries: resolved.moduleBoundaries } : {},
6322
+ ...resolved.typeSafety ? { typeSafety: resolved.typeSafety } : {},
6323
+ ...resolved.allowRouteCommandBindings === undefined ? {} : { allowRouteCommandBindings: resolved.allowRouteCommandBindings },
6324
+ ...resolved.disallowControllerDirectDb === undefined ? {} : { disallowControllerDirectDb: resolved.disallowControllerDirectDb },
6325
+ ...resolved.detectOrphanModules === undefined ? {} : { detectOrphanModules: resolved.detectOrphanModules },
6137
6326
  treeShakeUnusedProviders: resolved.treeShakeUnusedProviders ?? DEFAULT_SUPACLOUD_CONFIG.treeShakeUnusedProviders,
6138
6327
  graphql: resolved.graphql ? {
6139
6328
  ...resolved.graphql,
6140
- schema: resolve6(cwd, resolved.graphql.schema)
6329
+ schema: resolve7(cwd, resolved.graphql.schema)
6141
6330
  } : undefined
6142
6331
  };
6143
6332
  }
@@ -6155,32 +6344,19 @@ async function loadSupacloudConfig(cwd = process.cwd()) {
6155
6344
  return defineSupacloudConfig(imported.default ?? {});
6156
6345
  }
6157
6346
  function compileOptionsFromConfig(config, cwd = process.cwd()) {
6158
- const resolved = resolveSupacloudConfig(config, cwd);
6159
- return {
6160
- rootDir: resolved.rootDir,
6161
- outDir: resolved.outDir,
6162
- include: resolved.include,
6163
- strict: resolved.strict,
6164
- requireRouteContracts: resolved.requireRouteContracts,
6165
- generateClient: resolved.generateClient,
6166
- generatePermissions: resolved.generatePermissions,
6167
- moduleBoundaryPreset: resolved.moduleBoundaryPreset,
6168
- commandCapabilities: resolved.commandCapabilities,
6169
- treeShakeUnusedProviders: resolved.treeShakeUnusedProviders,
6170
- graphql: resolved.graphql
6171
- };
6347
+ return resolveSupacloudConfig(config, cwd);
6172
6348
  }
6173
6349
 
6174
6350
  // src/fixes.ts
6175
6351
  import { randomUUID } from "node:crypto";
6176
6352
  import { lstat, readFile as readFile3, realpath, rename as rename2, unlink as unlink2, writeFile as writeFile2 } from "node:fs/promises";
6177
- import { dirname as dirname6, isAbsolute as isAbsolute2, relative as relative8, resolve as resolve7, sep as sep8 } from "node:path";
6178
- import * as ts6 from "@typescript/typescript6";
6353
+ import { dirname as dirname6, isAbsolute as isAbsolute2, relative as relative8, resolve as resolve8, sep as sep8 } from "node:path";
6354
+ import * as ts7 from "@typescript/typescript6";
6179
6355
  async function applyDiagnosticFix(fix, options = {}) {
6180
6356
  if (!fix || typeof fix.targetFile !== "string")
6181
6357
  throw new Error("Invalid DiagnosticFix");
6182
6358
  const root = await realpath(options.rootDir ?? process.cwd());
6183
- const file = resolve7(root, fix.targetFile);
6359
+ const file = resolve8(root, fix.targetFile);
6184
6360
  const stat = await lstat(file);
6185
6361
  const resolved = await realpath(file);
6186
6362
  const relativePath = relative8(root, resolved);
@@ -6200,7 +6376,7 @@ async function applyDiagnosticFix(fix, options = {}) {
6200
6376
  if (!current || current.initializer.getText(source) !== fix.expectedExpression) {
6201
6377
  throw new Error("Command mode changed since diagnosis; analyze the project again");
6202
6378
  }
6203
- content = replaceProperty(source, object, fix.property, ts6.factory.createStringLiteral(fix.value));
6379
+ content = replaceProperty(source, object, fix.property, ts7.factory.createStringLiteral(fix.value));
6204
6380
  break;
6205
6381
  }
6206
6382
  case "add_module_import": {
@@ -6211,15 +6387,15 @@ async function applyDiagnosticFix(fix, options = {}) {
6211
6387
  source = parse2(file, withImport);
6212
6388
  const object = unique(moduleObjects(source).filter((candidate) => !fix.targetModule || stringProperty(candidate, "name") === fix.targetModule), "target module");
6213
6389
  const imports = property(object, "imports");
6214
- if (imports && !ts6.isArrayLiteralExpression(imports.initializer)) {
6390
+ if (imports && !ts7.isArrayLiteralExpression(imports.initializer)) {
6215
6391
  throw new Error("Module imports must be a static array");
6216
6392
  }
6217
- const values = imports && ts6.isArrayLiteralExpression(imports.initializer) ? imports.initializer.elements : [];
6218
- if (values.some(ts6.isSpreadElement))
6393
+ const values = imports && ts7.isArrayLiteralExpression(imports.initializer) ? imports.initializer.elements : [];
6394
+ if (values.some(ts7.isSpreadElement))
6219
6395
  throw new Error("Module imports cannot contain spread elements");
6220
- content = values.some((value) => ts6.isIdentifier(value) && value.text === fix.symbol) ? withImport : replaceProperty(source, object, "imports", ts6.factory.createArrayLiteralExpression([
6396
+ content = values.some((value) => ts7.isIdentifier(value) && value.text === fix.symbol) ? withImport : replaceProperty(source, object, "imports", ts7.factory.createArrayLiteralExpression([
6221
6397
  ...values,
6222
- ts6.factory.createIdentifier(fix.symbol)
6398
+ ts7.factory.createIdentifier(fix.symbol)
6223
6399
  ]));
6224
6400
  break;
6225
6401
  }
@@ -6231,24 +6407,24 @@ async function applyDiagnosticFix(fix, options = {}) {
6231
6407
  const command = findClass(source, fix.command);
6232
6408
  const object = decoratorObject(command, "Command");
6233
6409
  const current = property(object, "permission");
6234
- if (current && (!ts6.isStringLiteral(current.initializer) || current.initializer.text !== permission)) {
6410
+ if (current && (!ts7.isStringLiteral(current.initializer) || current.initializer.text !== permission)) {
6235
6411
  throw new Error("Command permission already exists with a different value");
6236
6412
  }
6237
- content = current ? original : replaceProperty(source, object, "permission", ts6.factory.createStringLiteral(permission));
6413
+ content = current ? original : replaceProperty(source, object, "permission", ts7.factory.createStringLiteral(permission));
6238
6414
  break;
6239
6415
  }
6240
6416
  case "add_route_parameter_binding": {
6241
6417
  const controller = findClass(source, fix.controller);
6242
- const method = unique(controller.members.filter((member) => ts6.isMethodDeclaration(member) && nameOf(member.name) === fix.route), "route handler");
6243
- const parameter = unique(method.parameters.filter((candidate) => ts6.isIdentifier(candidate.name) && candidate.name.text === fix.parameter), "same-named route parameter");
6418
+ const method = unique(controller.members.filter((member) => ts7.isMethodDeclaration(member) && nameOf(member.name) === fix.route), "route handler");
6419
+ const parameter = unique(method.parameters.filter((candidate) => ts7.isIdentifier(candidate.name) && candidate.name.text === fix.parameter), "same-named route parameter");
6244
6420
  const binding = fix.binding === "param" ? "Param" : fix.binding === "query" ? "Query" : undefined;
6245
6421
  if (!binding)
6246
6422
  throw new Error("Invalid route binding");
6247
- const decorators = ts6.getDecorators(parameter) ?? [];
6423
+ const decorators = ts7.getDecorators(parameter) ?? [];
6248
6424
  if (decorators.length > 0)
6249
6425
  throw new Error("Parameter already has a decorator");
6250
- const framework = unique(source.statements.filter((statement) => ts6.isImportDeclaration(statement) && statement.importClause?.namedBindings && ts6.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some((element) => ["Controller", "Get", "Post", "Put", "Patch", "Delete", "Head", "Options"].includes(element.name.text))), "framework import");
6251
- if (!ts6.isImportDeclaration(framework) || !ts6.isStringLiteral(framework.moduleSpecifier)) {
6426
+ const framework = unique(source.statements.filter((statement) => ts7.isImportDeclaration(statement) && statement.importClause?.namedBindings && ts7.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some((element) => ["Controller", "Get", "Post", "Put", "Patch", "Delete", "Head", "Options"].includes(element.name.text))), "framework import");
6427
+ if (!ts7.isImportDeclaration(framework) || !ts7.isStringLiteral(framework.moduleSpecifier)) {
6252
6428
  throw new Error("Framework import must be static");
6253
6429
  }
6254
6430
  const edited = original.slice(0, parameter.getStart(source)) + `@${binding}(${JSON.stringify(fix.parameter)}) ` + original.slice(parameter.getStart(source));
@@ -6274,15 +6450,15 @@ async function applyDiagnosticFix(fix, options = {}) {
6274
6450
  return result;
6275
6451
  }
6276
6452
  function parse2(file, text) {
6277
- const result = ts6.transpileModule(text, {
6453
+ const result = ts7.transpileModule(text, {
6278
6454
  fileName: file,
6279
6455
  reportDiagnostics: true,
6280
- compilerOptions: { target: ts6.ScriptTarget.ESNext, experimentalDecorators: true }
6456
+ compilerOptions: { target: ts7.ScriptTarget.ESNext, experimentalDecorators: true }
6281
6457
  });
6282
- if (result.diagnostics?.some((item) => item.category === ts6.DiagnosticCategory.Error)) {
6458
+ if (result.diagnostics?.some((item) => item.category === ts7.DiagnosticCategory.Error)) {
6283
6459
  throw new Error("Cannot fix syntactically invalid TypeScript");
6284
6460
  }
6285
- return ts6.createSourceFile(file, text, ts6.ScriptTarget.Latest, true, ts6.ScriptKind.TS);
6461
+ return ts7.createSourceFile(file, text, ts7.ScriptTarget.Latest, true, ts7.ScriptKind.TS);
6286
6462
  }
6287
6463
  function unique(items, description) {
6288
6464
  if (items.length !== 1)
@@ -6296,50 +6472,50 @@ function identifier(value) {
6296
6472
  function nameOf(name) {
6297
6473
  if (!name)
6298
6474
  return "";
6299
- return ts6.isIdentifier(name) || ts6.isStringLiteral(name) || ts6.isNumericLiteral(name) ? name.text : "";
6475
+ return ts7.isIdentifier(name) || ts7.isStringLiteral(name) || ts7.isNumericLiteral(name) ? name.text : "";
6300
6476
  }
6301
6477
  function property(object, key) {
6302
- if (object.properties.some((item) => !ts6.isPropertyAssignment(item) || ts6.isComputedPropertyName(item.name))) {
6478
+ if (object.properties.some((item) => !ts7.isPropertyAssignment(item) || ts7.isComputedPropertyName(item.name))) {
6303
6479
  throw new Error("Fix requires explicit static object properties");
6304
6480
  }
6305
- const values = object.properties.filter((item) => ts6.isPropertyAssignment(item) && nameOf(item.name) === key);
6481
+ const values = object.properties.filter((item) => ts7.isPropertyAssignment(item) && nameOf(item.name) === key);
6306
6482
  if (values.length > 1)
6307
6483
  throw new Error(`Duplicate '${key}' property`);
6308
6484
  return values[0];
6309
6485
  }
6310
6486
  function stringProperty(object, key) {
6311
6487
  const value = property(object, key)?.initializer;
6312
- return value && ts6.isStringLiteral(value) ? value.text : undefined;
6488
+ return value && ts7.isStringLiteral(value) ? value.text : undefined;
6313
6489
  }
6314
6490
  function replaceProperty(source, object, key, value) {
6315
6491
  const previous = property(object, key);
6316
- const replacement = ts6.factory.createPropertyAssignment(key, value);
6492
+ const replacement = ts7.factory.createPropertyAssignment(key, value);
6317
6493
  const properties = object.properties.map((item) => item === previous ? replacement : item);
6318
6494
  if (!previous)
6319
6495
  properties.push(replacement);
6320
- const updated = ts6.factory.updateObjectLiteralExpression(object, properties);
6321
- return source.text.slice(0, object.getStart(source)) + ts6.createPrinter().printNode(ts6.EmitHint.Expression, updated, source) + source.text.slice(object.end);
6496
+ const updated = ts7.factory.updateObjectLiteralExpression(object, properties);
6497
+ return source.text.slice(0, object.getStart(source)) + ts7.createPrinter().printNode(ts7.EmitHint.Expression, updated, source) + source.text.slice(object.end);
6322
6498
  }
6323
6499
  function findClass(source, name) {
6324
- return unique(source.statements.filter((statement) => ts6.isClassDeclaration(statement) && statement.name?.text === name), `class '${name}'`);
6500
+ return unique(source.statements.filter((statement) => ts7.isClassDeclaration(statement) && statement.name?.text === name), `class '${name}'`);
6325
6501
  }
6326
6502
  function decoratorObject(node, name) {
6327
- const decorator = unique((ts6.getDecorators(node) ?? []).filter((item) => ts6.isCallExpression(item.expression) && item.expression.expression.getText() === name), `@${name} decorator`);
6328
- const argument = ts6.isCallExpression(decorator.expression) ? decorator.expression.arguments[0] : undefined;
6329
- if (!argument || !ts6.isObjectLiteralExpression(argument))
6503
+ const decorator = unique((ts7.getDecorators(node) ?? []).filter((item) => ts7.isCallExpression(item.expression) && item.expression.expression.getText() === name), `@${name} decorator`);
6504
+ const argument = ts7.isCallExpression(decorator.expression) ? decorator.expression.arguments[0] : undefined;
6505
+ if (!argument || !ts7.isObjectLiteralExpression(argument))
6330
6506
  throw new Error(`@${name} requires a static object`);
6331
6507
  return argument;
6332
6508
  }
6333
6509
  function moduleObjects(source) {
6334
6510
  const result = [];
6335
6511
  for (const statement of source.statements) {
6336
- if (ts6.isClassDeclaration(statement) && (ts6.getDecorators(statement) ?? []).some((item) => ts6.isCallExpression(item.expression) && item.expression.expression.getText() === "Module")) {
6512
+ if (ts7.isClassDeclaration(statement) && (ts7.getDecorators(statement) ?? []).some((item) => ts7.isCallExpression(item.expression) && item.expression.expression.getText() === "Module")) {
6337
6513
  result.push(decoratorObject(statement, "Module"));
6338
6514
  }
6339
- if (ts6.isVariableStatement(statement)) {
6515
+ if (ts7.isVariableStatement(statement)) {
6340
6516
  for (const declaration of statement.declarationList.declarations) {
6341
6517
  const call = declaration.initializer;
6342
- if (call && ts6.isCallExpression(call) && ["defineModule", "defineFeatureSlice"].includes(call.expression.getText()) && call.arguments[0] && ts6.isObjectLiteralExpression(call.arguments[0])) {
6518
+ if (call && ts7.isCallExpression(call) && ["defineModule", "defineFeatureSlice"].includes(call.expression.getText()) && call.arguments[0] && ts7.isObjectLiteralExpression(call.arguments[0])) {
6343
6519
  result.push(call.arguments[0]);
6344
6520
  }
6345
6521
  }
@@ -6349,23 +6525,23 @@ function moduleObjects(source) {
6349
6525
  }
6350
6526
  function importSymbol(source, path, symbol) {
6351
6527
  identifier(symbol);
6352
- const current = resolve7(source.fileName).replace(/\.(tsx?|mts|cts)$/, "");
6353
- const target = resolve7(dirname6(source.fileName), path).replace(/\.(tsx?|mts|cts)$/, "");
6528
+ const current = resolve8(source.fileName).replace(/\.(tsx?|mts|cts)$/, "");
6529
+ const target = resolve8(dirname6(source.fileName), path).replace(/\.(tsx?|mts|cts)$/, "");
6354
6530
  if (current === target)
6355
6531
  return source.text;
6356
- const matches = source.statements.filter((item) => ts6.isImportDeclaration(item) && ts6.isStringLiteral(item.moduleSpecifier) && item.moduleSpecifier.text === path);
6532
+ const matches = source.statements.filter((item) => ts7.isImportDeclaration(item) && ts7.isStringLiteral(item.moduleSpecifier) && item.moduleSpecifier.text === path);
6357
6533
  if (matches.length > 1)
6358
6534
  throw new Error(`Ambiguous imports from '${path}'`);
6359
6535
  const match = matches[0];
6360
- if (match && ts6.isImportDeclaration(match) && match.importClause?.namedBindings && ts6.isNamedImports(match.importClause.namedBindings) && !match.importClause.isTypeOnly) {
6536
+ if (match && ts7.isImportDeclaration(match) && match.importClause?.namedBindings && ts7.isNamedImports(match.importClause.namedBindings) && !match.importClause.isTypeOnly) {
6361
6537
  if (match.importClause.namedBindings.elements.some((item) => item.name.text === symbol))
6362
6538
  return source.text;
6363
6539
  const bindings = match.importClause.namedBindings;
6364
- const updated = ts6.factory.updateNamedImports(bindings, [
6540
+ const updated = ts7.factory.updateNamedImports(bindings, [
6365
6541
  ...bindings.elements,
6366
- ts6.factory.createImportSpecifier(false, undefined, ts6.factory.createIdentifier(symbol))
6542
+ ts7.factory.createImportSpecifier(false, undefined, ts7.factory.createIdentifier(symbol))
6367
6543
  ]);
6368
- return source.text.slice(0, bindings.getStart(source)) + ts6.createPrinter().printNode(ts6.EmitHint.Unspecified, updated, source) + source.text.slice(bindings.end);
6544
+ return source.text.slice(0, bindings.getStart(source)) + ts7.createPrinter().printNode(ts7.EmitHint.Unspecified, updated, source) + source.text.slice(bindings.end);
6369
6545
  }
6370
6546
  if (match)
6371
6547
  throw new Error(`Import from '${path}' is not a named value import`);
@@ -6516,8 +6692,8 @@ async function run() {
6516
6692
  if (checkSchema && command !== "graphql-schema")
6517
6693
  throw new Error("--check is only supported by graphql-schema");
6518
6694
  const defaults = resolveSupacloudConfig(loadedConfig, process.cwd());
6519
- const resolvedRoot = rootDir ? resolve9(process.cwd(), rootDir) : defaults.rootDir;
6520
- const resolvedOut = outDir ? resolve9(process.cwd(), outDir) : defaults.outDir;
6695
+ const resolvedRoot = rootDir ? resolve10(process.cwd(), rootDir) : defaults.rootDir;
6696
+ const resolvedOut = outDir ? resolve10(process.cwd(), outDir) : defaults.outDir;
6521
6697
  const configured = compileOptionsFromConfig({
6522
6698
  ...loadedConfig,
6523
6699
  root: resolvedRoot,
@@ -6558,7 +6734,7 @@ async function run() {
6558
6734
  } else if (command === "fix") {
6559
6735
  if (!query)
6560
6736
  throw new Error("fix requires a JSON file containing one DiagnosticFix");
6561
- const fix = JSON.parse(await readFile5(resolve9(process.cwd(), query), "utf8"));
6737
+ const fix = JSON.parse(await readFile5(resolve10(process.cwd(), query), "utf8"));
6562
6738
  const result = await applyDiagnosticFix(fix, { rootDir: resolvedRoot, dryRun });
6563
6739
  console.log(JSON.stringify({ ok: true, ...result }, null, 2));
6564
6740
  } else if (command === "compile") {