@supacloud/compiler 0.13.1 → 0.15.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 CHANGED
@@ -119,12 +119,49 @@ const result = await queries.ReviewList({ first: 20 });
119
119
 
120
120
  Method names and types come from named operations. The generated client is
121
121
  dependency-free and refreshes identity per request; `getSdk(requester)` integrates
122
- an existing transport. It rejects HTTP errors, GraphQL errors and malformed
123
- response envelopes, but does not runtime-decode selected field values.
122
+ an existing transport returning `Promise<unknown>`. It rejects HTTP errors,
123
+ GraphQL errors, malformed response envelopes and invalid selected field values.
124
+ Both clients run generated operation parsers before returning typed data. No
125
+ customer TypeScript-to-TypeBox postprocessor or extra runtime dependency is needed.
126
+ The same module exports `parseReviewListQuery(value: unknown)` and
127
+ `isReviewListQuery(value: unknown)` for other integration boundaries (names follow
128
+ your operations). Validation follows the generated selected JSON shape, including
129
+ aliases, fragments, enums, lists, nullability and optional conditional fields.
130
+ Unmapped scalars remain `unknown`; scalar domain formats and authorization still
131
+ need business validation. Non-JSON scalar mappings such as `Date` fail compilation;
132
+ map the wire value to `string` and convert it after validation instead.
133
+ Generic application adapters can use `GraphqlQueryResults[Name]`,
134
+ `parseGraphqlResult(name, value)` and `isGraphqlResult(name, value)` instead of
135
+ maintaining their own result-type registry. Registry keys are operation names
136
+ such as `"ReviewList"`, without the `Query` type suffix.
124
137
  `graphql.manifest.json` records query locations and the schema hash; context packs
125
138
  include colocated queries. RLS/grants, real database acceptance and query resource
126
139
  limits remain deployment responsibilities. Business writes stay in Commands.
127
140
 
141
+ Use project configuration instead of a custom compile wrapper for shared rules:
142
+
143
+ ```ts
144
+ export default defineSupacloudConfig({
145
+ root: "src",
146
+ graphql: { schema: "graphql/schema.graphql" },
147
+ moduleBoundaries: [{
148
+ sourceTag: "type:feature",
149
+ bannedDependenciesWithTags: ["type:feature"],
150
+ }],
151
+ typeSafety: { scanProductionSource: true, noAnyInGenerated: true },
152
+ allowRouteCommandBindings: false,
153
+ });
154
+ ```
155
+
156
+ `compile`, `check` and `dev` apply these options through the same compiler pipeline.
157
+ `check` also compares generated validators without temporary directories or writes.
158
+ `allowRouteCommandBindings: false` prevents duplicate governance when an application
159
+ executes Commands inside its own service boundary. `disallowControllerDirectDb`
160
+ and `detectOrphanModules` expose the existing optional architecture checks too.
161
+ Keep application-specific governance and business queries in the application.
162
+ See `docs/compiler-consumer-simplification.md` in the repository for the ownership
163
+ checklist and migration boundaries.
164
+
128
165
  ## 安装
129
166
 
130
167
  ```bash
package/dist/cli.js CHANGED
@@ -26,10 +26,11 @@ function camelName(token) {
26
26
  return token.charAt(0).toLowerCase() + token.slice(1);
27
27
  }
28
28
  function relativeImportPath(fromDir, toFile) {
29
- const fromParts = fromDir.split("/").filter(Boolean);
30
- const toParts = toFile.split("/").filter(Boolean);
29
+ const fromParts = fromDir.split(/[\\/]/).filter(Boolean);
30
+ const toParts = toFile.split(/[\\/]/).filter(Boolean);
31
+ const isWindows = process.platform === "win32" || /^[a-zA-Z]:/.test(fromParts[0] ?? "") || /^[a-zA-Z]:/.test(toParts[0] ?? "");
31
32
  let common = 0;
32
- while (common < fromParts.length && common < toParts.length && fromParts[common] === toParts[common]) {
33
+ while (common < fromParts.length && common < toParts.length && (fromParts[common] === toParts[common] || isWindows && fromParts[common].toLowerCase() === toParts[common].toLowerCase())) {
33
34
  common += 1;
34
35
  }
35
36
  const ups = fromParts.length - common;
@@ -1213,11 +1214,11 @@ export function createGraphqlClient(options: GraphqlClientOptions) {
1213
1214
  }
1214
1215
  endpoint.pathname = endpoint.pathname.replace(/\\/$/, "") + "/graphql/v1";
1215
1216
  const fetcher = options.fetch ?? globalThis.fetch.bind(globalThis);
1216
- return getSdk<GraphqlRequestOptions>(async <R, V>(
1217
+ return getSdk<GraphqlRequestOptions>(async (
1217
1218
  query: string,
1218
- variables?: V,
1219
+ variables?: unknown,
1219
1220
  request?: GraphqlRequestOptions,
1220
- ): Promise<R> => {
1221
+ ): Promise<unknown> => {
1221
1222
  const headers = new Headers({ "Content-Type": "application/json", Accept: "application/json" });
1222
1223
  if (options.publishableKey) headers.set("apikey", options.publishableKey);
1223
1224
  const token = await options.getAccessToken?.();
@@ -1252,8 +1253,8 @@ export function createGraphqlClient(options: GraphqlClientOptions) {
1252
1253
  if (!("data" in envelope) || !envelope.data || typeof envelope.data !== "object" || Array.isArray(envelope.data)) {
1253
1254
  throw new GraphqlRequestError("invalid-response", "GraphQL returned no result object.", response.status);
1254
1255
  }
1255
- // Operation types describe the schema snapshot, not runtime response validation.
1256
- return envelope.data as R;
1256
+ // The operation-specific parser in getSdk validates selected field values.
1257
+ return envelope.data;
1257
1258
  });
1258
1259
  }
1259
1260
  `;
@@ -1327,6 +1328,165 @@ var init_graphql_inputs = __esm(() => {
1327
1328
  init_graphql_options();
1328
1329
  });
1329
1330
 
1331
+ // src/graphql-runtime.ts
1332
+ import * as ts6 from "@typescript/typescript6";
1333
+ import { resolve as resolve4 } from "node:path";
1334
+ function renderGraphqlValidators(source, operationNames) {
1335
+ const fileName = resolve4("/__supacloud_graphql__/contracts.ts");
1336
+ const options = {
1337
+ strict: true,
1338
+ noUncheckedIndexedAccess: true,
1339
+ exactOptionalPropertyTypes: true,
1340
+ noImplicitOverride: true,
1341
+ noPropertyAccessFromIndexSignature: true,
1342
+ noFallthroughCasesInSwitch: true,
1343
+ skipLibCheck: false,
1344
+ target: ts6.ScriptTarget.ES2022,
1345
+ lib: ["lib.es2022.d.ts"],
1346
+ types: [],
1347
+ noEmit: true
1348
+ };
1349
+ const host = ts6.createCompilerHost(options);
1350
+ const getSourceFile = host.getSourceFile.bind(host);
1351
+ host.getSourceFile = (path, languageVersion, onError, shouldCreateNewSourceFile) => path === fileName ? ts6.createSourceFile(path, source, languageVersion, true) : getSourceFile(path, languageVersion, onError, shouldCreateNewSourceFile);
1352
+ const program = ts6.createProgram([fileName], options, host);
1353
+ const diagnostics = ts6.getPreEmitDiagnostics(program);
1354
+ if (diagnostics.length) {
1355
+ throw new Error(`Generated GraphQL types cannot be validated: ${diagnostics.map((item) => ts6.flattenDiagnosticMessageText(item.messageText, `
1356
+ `)).join("; ")}`);
1357
+ }
1358
+ const checker = program.getTypeChecker();
1359
+ const entry = program.getSourceFile(fileName);
1360
+ const module = entry && checker.getSymbolAtLocation(entry);
1361
+ if (!module)
1362
+ throw new Error("Generated GraphQL types have no module");
1363
+ const exports = new Map(checker.getExportsOfModule(module).map((symbol) => [symbol.name, symbol]));
1364
+ if (exports.has("GraphqlQueryResults")) {
1365
+ throw new Error("GraphQL type GraphqlQueryResults conflicts with the generated result registry.");
1366
+ }
1367
+ const names = new Map;
1368
+ const definitions = new Map;
1369
+ function unsupported(type) {
1370
+ throw new Error(`Unsupported GraphQL result wire type: ${checker.typeToString(type)}. Map custom scalars to JSON wire types or unknown.`);
1371
+ }
1372
+ function reference(type) {
1373
+ const existing = names.get(type);
1374
+ if (existing)
1375
+ return existing;
1376
+ const name = `checkGraphqlValue${names.size}`;
1377
+ names.set(type, name);
1378
+ definitions.set(name, "");
1379
+ definitions.set(name, `function ${name}(value: unknown): boolean {
1380
+ return ${expression(type)};
1381
+ }`);
1382
+ return name;
1383
+ }
1384
+ function expression(type) {
1385
+ if (type.flags & ts6.TypeFlags.Any)
1386
+ return unsupported(type);
1387
+ if (type.flags & ts6.TypeFlags.Unknown)
1388
+ return "true";
1389
+ if (type.flags & ts6.TypeFlags.Never)
1390
+ return "false";
1391
+ if (type.flags & ts6.TypeFlags.Null)
1392
+ return "value === null";
1393
+ if (type.flags & ts6.TypeFlags.Undefined)
1394
+ return "value === undefined";
1395
+ if (type.isStringLiteral() || type.isNumberLiteral())
1396
+ return `value === ${JSON.stringify(type.value)}`;
1397
+ if (type.flags & ts6.TypeFlags.BooleanLiteral)
1398
+ return `value === ${checker.typeToString(type)}`;
1399
+ if (type.flags & ts6.TypeFlags.String)
1400
+ return 'typeof value === "string"';
1401
+ if (type.flags & ts6.TypeFlags.Number)
1402
+ return 'typeof value === "number" && Number.isFinite(value)';
1403
+ if (type.flags & ts6.TypeFlags.Boolean)
1404
+ return 'typeof value === "boolean"';
1405
+ if (type.isUnion())
1406
+ return type.types.map((part) => `${reference(part)}(value)`).join(" || ");
1407
+ if (type.isIntersection())
1408
+ return type.types.map((part) => `${reference(part)}(value)`).join(" && ");
1409
+ if (checker.isTupleType(type))
1410
+ return unsupported(type);
1411
+ if (checker.isArrayType(type)) {
1412
+ const item = checker.getIndexTypeOfType(type, ts6.IndexKind.Number);
1413
+ if (!item)
1414
+ return unsupported(type);
1415
+ return `isGraphqlArray(value) && Array.from(value).every(${reference(item)})`;
1416
+ }
1417
+ if (type.flags & ts6.TypeFlags.Object) {
1418
+ if (type.getCallSignatures().length || type.getConstructSignatures().length)
1419
+ return unsupported(type);
1420
+ const indexes = checker.getIndexInfosOfType(type);
1421
+ if (indexes.some((index) => !(index.keyType.flags & ts6.TypeFlags.String)))
1422
+ return unsupported(type);
1423
+ const properties = checker.getPropertiesOfType(type).map((property) => {
1424
+ const declaration = property.valueDeclaration ?? property.declarations?.[0];
1425
+ if (!declaration)
1426
+ return unsupported(type);
1427
+ const check = reference(checker.getTypeOfSymbolAtLocation(property, declaration));
1428
+ const key = JSON.stringify(property.name);
1429
+ const present = `Object.prototype.hasOwnProperty.call(value, ${key})`;
1430
+ return property.flags & ts6.SymbolFlags.Optional ? `(!${present} || ${check}(value[${key}]))` : `(${present} && ${check}(value[${key}]))`;
1431
+ });
1432
+ const indexedValues = indexes.map((index) => `Object.values(value).every(${reference(index.type)})`);
1433
+ return ["isGraphqlRecord(value)", ...properties, ...indexedValues].join(" && ");
1434
+ }
1435
+ return unsupported(type);
1436
+ }
1437
+ const operations = [...operationNames].sort();
1438
+ const parsers = operations.map((name) => {
1439
+ const typeName = `${name}Query`;
1440
+ const symbol = exports.get(typeName);
1441
+ if (!symbol)
1442
+ throw new Error(`Missing generated operation type: ${typeName}`);
1443
+ const check = reference(checker.getDeclaredTypeOfSymbol(symbol));
1444
+ return `export function is${typeName}(value: unknown): value is ${typeName} {
1445
+ return ${check}(value);
1446
+ }
1447
+ export function parse${typeName}(value: unknown): ${typeName} {
1448
+ if (!is${typeName}(value)) {
1449
+ throw new GraphqlRequestError("invalid-response", ${JSON.stringify(`GraphQL result does not match ${name}.`)});
1450
+ }
1451
+ return value;
1452
+ }`;
1453
+ });
1454
+ return `
1455
+ function isGraphqlRecord(value: unknown): value is Record<string, unknown> {
1456
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1457
+ }
1458
+ function isGraphqlArray(value: unknown): value is unknown[] {
1459
+ return Array.isArray(value);
1460
+ }
1461
+ ${[...definitions.values()].join(`
1462
+ `)}
1463
+ ${parsers.join(`
1464
+ `)}
1465
+ export interface GraphqlQueryResults {
1466
+ ${operations.map((name) => ` ${JSON.stringify(name)}: ${name}Query;`).join(`
1467
+ `)}
1468
+ }
1469
+ export function isGraphqlResult<Name extends keyof GraphqlQueryResults>(
1470
+ name: Name, value: unknown,
1471
+ ): value is GraphqlQueryResults[Name] {
1472
+ switch (name) {
1473
+ ${operations.map((name) => ` case ${JSON.stringify(name)}: return is${name}Query(value);`).join(`
1474
+ `)}
1475
+ default: return false;
1476
+ }
1477
+ }
1478
+ export function parseGraphqlResult<Name extends keyof GraphqlQueryResults>(
1479
+ name: Name, value: unknown,
1480
+ ): GraphqlQueryResults[Name] {
1481
+ if (!isGraphqlResult(name, value)) {
1482
+ throw new GraphqlRequestError("invalid-response", "GraphQL result does not match operation " + name + ".");
1483
+ }
1484
+ return value;
1485
+ }
1486
+ `;
1487
+ }
1488
+ var init_graphql_runtime = () => {};
1489
+
1330
1490
  // src/graphql.ts
1331
1491
  var exports_graphql = {};
1332
1492
  __export(exports_graphql, {
@@ -1475,15 +1635,15 @@ async function renderGraphql(options) {
1475
1635
  const separated = separateOperations(combined);
1476
1636
  const methods = Object.entries(separated).sort(([a], [b]) => a.localeCompare(b)).map(([name, document]) => {
1477
1637
  const operation = document.definitions.find((node) => node.kind === Kind.OPERATION_DEFINITION);
1478
- if (operation.kind !== Kind.OPERATION_DEFINITION)
1638
+ if (!operation || operation.kind !== Kind.OPERATION_DEFINITION)
1479
1639
  throw new Error("Missing query operation");
1480
1640
  const required = operation.variableDefinitions?.some((variable) => variable.type.kind === Kind.NON_NULL_TYPE && !variable.defaultValue);
1481
- return ` ${JSON.stringify(name)}(variables${required ? "" : "?"}: ${name}QueryVariables, options?: C): Promise<${name}Query> {
1482
- return requester<${name}Query, ${name}QueryVariables>(${JSON.stringify(print(document))}, variables, options);
1641
+ return ` async ${JSON.stringify(name)}(variables${required ? "" : "?"}: ${name}QueryVariables, options?: C): Promise<${name}Query> {
1642
+ return parse${name}Query(await requester(${JSON.stringify(print(document))}, variables, options));
1483
1643
  }`;
1484
1644
  });
1485
1645
  const facade = `
1486
- export type Requester<C> = <R, V>(query: string, variables?: V, options?: C) => Promise<R>;
1646
+ export type Requester<C> = (query: string, variables?: unknown, options?: C) => Promise<unknown>;
1487
1647
  export function getSdk<C>(requester: Requester<C>) {
1488
1648
  return {
1489
1649
  ${methods.join(`,
@@ -1491,8 +1651,9 @@ ${methods.join(`,
1491
1651
  };
1492
1652
  }
1493
1653
  `;
1654
+ const validators = renderGraphqlValidators(generated, queryEntries.map((entry) => entry.name));
1494
1655
  result.files["graphql.ts"] = `// GENERATED BY @supacloud/compiler. DO NOT EDIT.
1495
- ` + generated + facade + GRAPHQL_CLIENT_SOURCE;
1656
+ ` + generated + validators + facade + GRAPHQL_CLIENT_SOURCE;
1496
1657
  if (options.graphql.typedDocuments) {
1497
1658
  const typedDocuments = await import("@graphql-codegen/typed-document-node");
1498
1659
  result.files["graphql.documents.ts"] = `// GENERATED BY @supacloud/compiler. DO NOT EDIT.
@@ -1521,6 +1682,7 @@ ${methods.join(`,
1521
1682
  var init_graphql = __esm(() => {
1522
1683
  init_graphql_inputs();
1523
1684
  init_graphql_options();
1685
+ init_graphql_runtime();
1524
1686
  });
1525
1687
 
1526
1688
  // src/graphql-schema.ts
@@ -1530,7 +1692,7 @@ __export(exports_graphql_schema, {
1530
1692
  });
1531
1693
  import { mkdir as mkdir2, readFile as readFile4 } from "node:fs/promises";
1532
1694
  import { createHash as createHash7 } from "node:crypto";
1533
- import { dirname as dirname7, resolve as resolve8 } from "node:path";
1695
+ import { dirname as dirname7, resolve as resolve9 } from "node:path";
1534
1696
  async function pullGraphqlSchema(options) {
1535
1697
  assertGraphqlOptions({ schema: options.output });
1536
1698
  const endpoint = new URL(options.url);
@@ -1570,11 +1732,10 @@ async function pullGraphqlSchema(options) {
1570
1732
  throw new Error("GraphQL schema export failed. Verify caller grants and enable introspection only in the intended development environment.");
1571
1733
  }
1572
1734
  const schema = lexicographicSortSchema(buildClientSchema2(data));
1573
- const path = resolve8(options.output);
1735
+ const path = resolve9(options.output);
1574
1736
  const content = path.endsWith(".json") ? JSON.stringify(introspectionFromSchema(schema), null, 2) + `
1575
1737
  ` : `# GENERATED BY supacloud-compiler graphql-schema. DO NOT EDIT.
1576
1738
  # Database First: change database declarations, apply migrations, then re-export for the intended role.
1577
- # Database First: change database declarations, apply migrations, then re-export for the intended role.
1578
1739
  ` + printSchema2(schema) + `
1579
1740
  `;
1580
1741
  let previous;
@@ -1598,7 +1759,7 @@ var init_graphql_schema = __esm(() => {
1598
1759
  });
1599
1760
 
1600
1761
  // src/cli.ts
1601
- import { resolve as resolve9 } from "node:path";
1762
+ import { resolve as resolve10 } from "node:path";
1602
1763
  import { readFile as readFile5 } from "node:fs/promises";
1603
1764
 
1604
1765
  // src/analyze.ts
@@ -5701,12 +5862,12 @@ function exportGraphDot(graph) {
5701
5862
 
5702
5863
  // src/watch.ts
5703
5864
  import { existsSync as existsSync5, watch } from "node:fs";
5704
- import { dirname as dirname5, relative as relative7, resolve as resolve5, sep as sep7 } from "node:path";
5865
+ import { dirname as dirname5, relative as relative7, resolve as resolve6, sep as sep7 } from "node:path";
5705
5866
 
5706
5867
  // src/incremental.ts
5707
5868
  import { createHash as createHash6 } from "node:crypto";
5708
5869
  import { access as access2, readdir, readFile as readFile2 } from "node:fs/promises";
5709
- import { isAbsolute, relative as relative6, resolve as resolve4, sep as sep6 } from "node:path";
5870
+ import { isAbsolute, relative as relative6, resolve as resolve5, sep as sep6 } from "node:path";
5710
5871
  init_graphql_inputs();
5711
5872
  function createDependencyGraphCache() {
5712
5873
  return {
@@ -5781,11 +5942,11 @@ function createIncrementalCompiler() {
5781
5942
  };
5782
5943
  }
5783
5944
  async function updateSnapshot(previous, options, changedPaths) {
5784
- const rootDir = resolve4(options.rootDir);
5785
- const outDir = resolve4(options.outDir);
5945
+ const rootDir = resolve5(options.rootDir);
5946
+ const outDir = resolve5(options.outDir);
5786
5947
  const files = { ...previous.files };
5787
5948
  for (const changedPath of changedPaths) {
5788
- const absolutePath = isAbsolute(changedPath) ? resolve4(changedPath) : resolve4(rootDir, changedPath);
5949
+ const absolutePath = isAbsolute(changedPath) ? resolve5(changedPath) : resolve5(rootDir, changedPath);
5789
5950
  const relativeChangedPath = relative6(rootDir, absolutePath);
5790
5951
  if (relativeChangedPath === ".." || relativeChangedPath.startsWith(`..${sep6}`))
5791
5952
  continue;
@@ -5803,8 +5964,8 @@ async function updateSnapshot(previous, options, changedPaths) {
5803
5964
  return { files, optionsKey: optionsKeyOf(options) };
5804
5965
  }
5805
5966
  async function createSnapshot(options) {
5806
- const rootDir = resolve4(options.rootDir);
5807
- const outDir = resolve4(options.outDir);
5967
+ const rootDir = resolve5(options.rootDir);
5968
+ const outDir = resolve5(options.outDir);
5808
5969
  const paths = await listSourceFiles(rootDir, outDir);
5809
5970
  const files = {};
5810
5971
  for (const path of paths) {
@@ -5823,8 +5984,8 @@ async function createSnapshot(options) {
5823
5984
  }
5824
5985
  function optionsKeyOf(options) {
5825
5986
  return JSON.stringify({
5826
- rootDir: resolve4(options.rootDir),
5827
- outDir: resolve4(options.outDir),
5987
+ rootDir: resolve5(options.rootDir),
5988
+ outDir: resolve5(options.outDir),
5828
5989
  include: options.include,
5829
5990
  strict: options.strict,
5830
5991
  writeOnError: options.writeOnError,
@@ -5846,7 +6007,7 @@ async function listSourceFiles(rootDir, outDir) {
5846
6007
  const result = [];
5847
6008
  const visit = async (directory) => {
5848
6009
  for (const entry of await readdir(directory, { withFileTypes: true })) {
5849
- const path = resolve4(directory, entry.name);
6010
+ const path = resolve5(directory, entry.name);
5850
6011
  if (entry.isDirectory()) {
5851
6012
  if (entry.name === "node_modules" || entry.name === ".git" || path === outDir)
5852
6013
  continue;
@@ -5970,8 +6131,8 @@ function findAffectedModules(previous, current, changedFiles) {
5970
6131
  // src/watch.ts
5971
6132
  var DEFAULT_DEBOUNCE_MS = 100;
5972
6133
  function watchProject(options) {
5973
- const rootDir = resolve5(options.rootDir);
5974
- const outDir = resolve5(options.outDir);
6134
+ const rootDir = resolve6(options.rootDir);
6135
+ const outDir = resolve6(options.outDir);
5975
6136
  const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
5976
6137
  let timer;
5977
6138
  let closed = false;
@@ -5980,7 +6141,7 @@ function watchProject(options) {
5980
6141
  const pendingPaths = new Set;
5981
6142
  let watcher;
5982
6143
  let schemaWatcher;
5983
- const schemaPath = options.graphql ? resolve5(rootDir, options.graphql.schema) : undefined;
6144
+ const schemaPath = options.graphql ? resolve6(rootDir, options.graphql.schema) : undefined;
5984
6145
  const incremental = createIncrementalCompiler();
5985
6146
  let initialEvent;
5986
6147
  let resolveReady = () => {
@@ -6057,7 +6218,7 @@ function watchProject(options) {
6057
6218
  watcher = watch(rootDir, { recursive: true }, (_eventType, filename) => {
6058
6219
  if (!filename)
6059
6220
  return schedule();
6060
- const changedPath = resolve5(rootDir, filename.toString());
6221
+ const changedPath = resolve6(rootDir, filename.toString());
6061
6222
  const relativePath = relative7(outDir, changedPath);
6062
6223
  if (!relativePath.startsWith("..") && relativePath !== "")
6063
6224
  return;
@@ -6070,7 +6231,7 @@ function watchProject(options) {
6070
6231
  while (!existsSync5(directory) && dirname5(directory) !== directory)
6071
6232
  directory = dirname5(directory);
6072
6233
  schemaWatcher = watch(directory, { recursive: true }, (_eventType, filename) => {
6073
- if (!filename || resolve5(directory, filename.toString()) === schemaPath)
6234
+ if (!filename || resolve6(directory, filename.toString()) === schemaPath)
6074
6235
  schedule(schemaPath);
6075
6236
  });
6076
6237
  }
@@ -6097,7 +6258,7 @@ function watchProject(options) {
6097
6258
  // src/config.ts
6098
6259
  init_graphql_options();
6099
6260
  import { existsSync as existsSync6 } from "node:fs";
6100
- import { join as join6, resolve as resolve6 } from "node:path";
6261
+ import { join as join6, resolve as resolve7 } from "node:path";
6101
6262
  import { pathToFileURL } from "node:url";
6102
6263
  var DEFAULT_SUPACLOUD_CONFIG = {
6103
6264
  graphql: false,
@@ -6114,6 +6275,7 @@ var DEFAULT_SUPACLOUD_CONFIG = {
6114
6275
  function defineSupacloudConfig(config = {}) {
6115
6276
  if (config.graphql !== undefined && config.graphql !== false)
6116
6277
  assertGraphqlOptions(config.graphql);
6278
+ validateGovernanceConfig(config);
6117
6279
  return {
6118
6280
  ...DEFAULT_SUPACLOUD_CONFIG,
6119
6281
  ...config,
@@ -6121,11 +6283,35 @@ function defineSupacloudConfig(config = {}) {
6121
6283
  graphql: config.graphql ?? DEFAULT_SUPACLOUD_CONFIG.graphql
6122
6284
  };
6123
6285
  }
6286
+ function validateGovernanceConfig(config) {
6287
+ const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
6288
+ const isStrings = (value) => Array.isArray(value) && Array.from(value).every((item) => typeof item === "string" && item.trim().length > 0);
6289
+ if (config.moduleBoundaries !== undefined) {
6290
+ if (!Array.isArray(config.moduleBoundaries))
6291
+ throw new Error("moduleBoundaries must be an array of module tag rules.");
6292
+ const rules = config.moduleBoundaries;
6293
+ for (const rule of rules) {
6294
+ 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"])) {
6295
+ throw new Error("moduleBoundaries rules require sourceTag and optional string arrays onlyDependOnLibsWithTags/bannedDependenciesWithTags.");
6296
+ }
6297
+ }
6298
+ }
6299
+ if (config.typeSafety !== undefined) {
6300
+ const rules = config.typeSafety;
6301
+ 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"])) {
6302
+ throw new Error("typeSafety accepts boolean scanProductionSource/noAnyInGenerated and a string array exclude.");
6303
+ }
6304
+ }
6305
+ for (const key of ["allowRouteCommandBindings", "disallowControllerDirectDb", "detectOrphanModules"]) {
6306
+ if (config[key] !== undefined && typeof config[key] !== "boolean")
6307
+ throw new Error(`${key} must be a boolean.`);
6308
+ }
6309
+ }
6124
6310
  function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
6125
6311
  const resolved = defineSupacloudConfig(config);
6126
6312
  return {
6127
- rootDir: resolve6(cwd, resolved.root ?? DEFAULT_SUPACLOUD_CONFIG.root),
6128
- outDir: resolve6(cwd, resolved.outDir ?? DEFAULT_SUPACLOUD_CONFIG.outDir),
6313
+ rootDir: resolve7(cwd, resolved.root ?? DEFAULT_SUPACLOUD_CONFIG.root),
6314
+ outDir: resolve7(cwd, resolved.outDir ?? DEFAULT_SUPACLOUD_CONFIG.outDir),
6129
6315
  include: resolved.include ?? [...DEFAULT_SUPACLOUD_CONFIG.include],
6130
6316
  strict: resolved.strict ?? DEFAULT_SUPACLOUD_CONFIG.strict,
6131
6317
  requireRouteContracts: resolved.requireRouteContracts ?? DEFAULT_SUPACLOUD_CONFIG.requireRouteContracts,
@@ -6133,10 +6319,15 @@ function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
6133
6319
  generatePermissions: resolved.generatePermissions ?? DEFAULT_SUPACLOUD_CONFIG.generatePermissions,
6134
6320
  moduleBoundaryPreset: resolved.moduleBoundaryPreset ?? DEFAULT_SUPACLOUD_CONFIG.moduleBoundaryPreset,
6135
6321
  commandCapabilities: resolved.commandCapabilities,
6322
+ ...resolved.moduleBoundaries ? { moduleBoundaries: resolved.moduleBoundaries } : {},
6323
+ ...resolved.typeSafety ? { typeSafety: resolved.typeSafety } : {},
6324
+ ...resolved.allowRouteCommandBindings === undefined ? {} : { allowRouteCommandBindings: resolved.allowRouteCommandBindings },
6325
+ ...resolved.disallowControllerDirectDb === undefined ? {} : { disallowControllerDirectDb: resolved.disallowControllerDirectDb },
6326
+ ...resolved.detectOrphanModules === undefined ? {} : { detectOrphanModules: resolved.detectOrphanModules },
6136
6327
  treeShakeUnusedProviders: resolved.treeShakeUnusedProviders ?? DEFAULT_SUPACLOUD_CONFIG.treeShakeUnusedProviders,
6137
6328
  graphql: resolved.graphql ? {
6138
6329
  ...resolved.graphql,
6139
- schema: resolve6(cwd, resolved.graphql.schema)
6330
+ schema: resolve7(cwd, resolved.graphql.schema)
6140
6331
  } : undefined
6141
6332
  };
6142
6333
  }
@@ -6154,32 +6345,19 @@ async function loadSupacloudConfig(cwd = process.cwd()) {
6154
6345
  return defineSupacloudConfig(imported.default ?? {});
6155
6346
  }
6156
6347
  function compileOptionsFromConfig(config, cwd = process.cwd()) {
6157
- const resolved = resolveSupacloudConfig(config, cwd);
6158
- return {
6159
- rootDir: resolved.rootDir,
6160
- outDir: resolved.outDir,
6161
- include: resolved.include,
6162
- strict: resolved.strict,
6163
- requireRouteContracts: resolved.requireRouteContracts,
6164
- generateClient: resolved.generateClient,
6165
- generatePermissions: resolved.generatePermissions,
6166
- moduleBoundaryPreset: resolved.moduleBoundaryPreset,
6167
- commandCapabilities: resolved.commandCapabilities,
6168
- treeShakeUnusedProviders: resolved.treeShakeUnusedProviders,
6169
- graphql: resolved.graphql
6170
- };
6348
+ return resolveSupacloudConfig(config, cwd);
6171
6349
  }
6172
6350
 
6173
6351
  // src/fixes.ts
6174
6352
  import { randomUUID } from "node:crypto";
6175
6353
  import { lstat, readFile as readFile3, realpath, rename as rename2, unlink as unlink2, writeFile as writeFile2 } from "node:fs/promises";
6176
- import { dirname as dirname6, isAbsolute as isAbsolute2, relative as relative8, resolve as resolve7, sep as sep8 } from "node:path";
6177
- import * as ts6 from "@typescript/typescript6";
6354
+ import { dirname as dirname6, isAbsolute as isAbsolute2, relative as relative8, resolve as resolve8, sep as sep8 } from "node:path";
6355
+ import * as ts7 from "@typescript/typescript6";
6178
6356
  async function applyDiagnosticFix(fix, options = {}) {
6179
6357
  if (!fix || typeof fix.targetFile !== "string")
6180
6358
  throw new Error("Invalid DiagnosticFix");
6181
6359
  const root = await realpath(options.rootDir ?? process.cwd());
6182
- const file = resolve7(root, fix.targetFile);
6360
+ const file = resolve8(root, fix.targetFile);
6183
6361
  const stat = await lstat(file);
6184
6362
  const resolved = await realpath(file);
6185
6363
  const relativePath = relative8(root, resolved);
@@ -6199,7 +6377,7 @@ async function applyDiagnosticFix(fix, options = {}) {
6199
6377
  if (!current || current.initializer.getText(source) !== fix.expectedExpression) {
6200
6378
  throw new Error("Command mode changed since diagnosis; analyze the project again");
6201
6379
  }
6202
- content = replaceProperty(source, object, fix.property, ts6.factory.createStringLiteral(fix.value));
6380
+ content = replaceProperty(source, object, fix.property, ts7.factory.createStringLiteral(fix.value));
6203
6381
  break;
6204
6382
  }
6205
6383
  case "add_module_import": {
@@ -6210,15 +6388,15 @@ async function applyDiagnosticFix(fix, options = {}) {
6210
6388
  source = parse2(file, withImport);
6211
6389
  const object = unique(moduleObjects(source).filter((candidate) => !fix.targetModule || stringProperty(candidate, "name") === fix.targetModule), "target module");
6212
6390
  const imports = property(object, "imports");
6213
- if (imports && !ts6.isArrayLiteralExpression(imports.initializer)) {
6391
+ if (imports && !ts7.isArrayLiteralExpression(imports.initializer)) {
6214
6392
  throw new Error("Module imports must be a static array");
6215
6393
  }
6216
- const values = imports && ts6.isArrayLiteralExpression(imports.initializer) ? imports.initializer.elements : [];
6217
- if (values.some(ts6.isSpreadElement))
6394
+ const values = imports && ts7.isArrayLiteralExpression(imports.initializer) ? imports.initializer.elements : [];
6395
+ if (values.some(ts7.isSpreadElement))
6218
6396
  throw new Error("Module imports cannot contain spread elements");
6219
- content = values.some((value) => ts6.isIdentifier(value) && value.text === fix.symbol) ? withImport : replaceProperty(source, object, "imports", ts6.factory.createArrayLiteralExpression([
6397
+ content = values.some((value) => ts7.isIdentifier(value) && value.text === fix.symbol) ? withImport : replaceProperty(source, object, "imports", ts7.factory.createArrayLiteralExpression([
6220
6398
  ...values,
6221
- ts6.factory.createIdentifier(fix.symbol)
6399
+ ts7.factory.createIdentifier(fix.symbol)
6222
6400
  ]));
6223
6401
  break;
6224
6402
  }
@@ -6230,24 +6408,24 @@ async function applyDiagnosticFix(fix, options = {}) {
6230
6408
  const command = findClass(source, fix.command);
6231
6409
  const object = decoratorObject(command, "Command");
6232
6410
  const current = property(object, "permission");
6233
- if (current && (!ts6.isStringLiteral(current.initializer) || current.initializer.text !== permission)) {
6411
+ if (current && (!ts7.isStringLiteral(current.initializer) || current.initializer.text !== permission)) {
6234
6412
  throw new Error("Command permission already exists with a different value");
6235
6413
  }
6236
- content = current ? original : replaceProperty(source, object, "permission", ts6.factory.createStringLiteral(permission));
6414
+ content = current ? original : replaceProperty(source, object, "permission", ts7.factory.createStringLiteral(permission));
6237
6415
  break;
6238
6416
  }
6239
6417
  case "add_route_parameter_binding": {
6240
6418
  const controller = findClass(source, fix.controller);
6241
- const method = unique(controller.members.filter((member) => ts6.isMethodDeclaration(member) && nameOf(member.name) === fix.route), "route handler");
6242
- const parameter = unique(method.parameters.filter((candidate) => ts6.isIdentifier(candidate.name) && candidate.name.text === fix.parameter), "same-named route parameter");
6419
+ const method = unique(controller.members.filter((member) => ts7.isMethodDeclaration(member) && nameOf(member.name) === fix.route), "route handler");
6420
+ const parameter = unique(method.parameters.filter((candidate) => ts7.isIdentifier(candidate.name) && candidate.name.text === fix.parameter), "same-named route parameter");
6243
6421
  const binding = fix.binding === "param" ? "Param" : fix.binding === "query" ? "Query" : undefined;
6244
6422
  if (!binding)
6245
6423
  throw new Error("Invalid route binding");
6246
- const decorators = ts6.getDecorators(parameter) ?? [];
6424
+ const decorators = ts7.getDecorators(parameter) ?? [];
6247
6425
  if (decorators.length > 0)
6248
6426
  throw new Error("Parameter already has a decorator");
6249
- 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");
6250
- if (!ts6.isImportDeclaration(framework) || !ts6.isStringLiteral(framework.moduleSpecifier)) {
6427
+ 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");
6428
+ if (!ts7.isImportDeclaration(framework) || !ts7.isStringLiteral(framework.moduleSpecifier)) {
6251
6429
  throw new Error("Framework import must be static");
6252
6430
  }
6253
6431
  const edited = original.slice(0, parameter.getStart(source)) + `@${binding}(${JSON.stringify(fix.parameter)}) ` + original.slice(parameter.getStart(source));
@@ -6273,15 +6451,15 @@ async function applyDiagnosticFix(fix, options = {}) {
6273
6451
  return result;
6274
6452
  }
6275
6453
  function parse2(file, text) {
6276
- const result = ts6.transpileModule(text, {
6454
+ const result = ts7.transpileModule(text, {
6277
6455
  fileName: file,
6278
6456
  reportDiagnostics: true,
6279
- compilerOptions: { target: ts6.ScriptTarget.ESNext, experimentalDecorators: true }
6457
+ compilerOptions: { target: ts7.ScriptTarget.ESNext, experimentalDecorators: true }
6280
6458
  });
6281
- if (result.diagnostics?.some((item) => item.category === ts6.DiagnosticCategory.Error)) {
6459
+ if (result.diagnostics?.some((item) => item.category === ts7.DiagnosticCategory.Error)) {
6282
6460
  throw new Error("Cannot fix syntactically invalid TypeScript");
6283
6461
  }
6284
- return ts6.createSourceFile(file, text, ts6.ScriptTarget.Latest, true, ts6.ScriptKind.TS);
6462
+ return ts7.createSourceFile(file, text, ts7.ScriptTarget.Latest, true, ts7.ScriptKind.TS);
6285
6463
  }
6286
6464
  function unique(items, description) {
6287
6465
  if (items.length !== 1)
@@ -6295,50 +6473,50 @@ function identifier(value) {
6295
6473
  function nameOf(name) {
6296
6474
  if (!name)
6297
6475
  return "";
6298
- return ts6.isIdentifier(name) || ts6.isStringLiteral(name) || ts6.isNumericLiteral(name) ? name.text : "";
6476
+ return ts7.isIdentifier(name) || ts7.isStringLiteral(name) || ts7.isNumericLiteral(name) ? name.text : "";
6299
6477
  }
6300
6478
  function property(object, key) {
6301
- if (object.properties.some((item) => !ts6.isPropertyAssignment(item) || ts6.isComputedPropertyName(item.name))) {
6479
+ if (object.properties.some((item) => !ts7.isPropertyAssignment(item) || ts7.isComputedPropertyName(item.name))) {
6302
6480
  throw new Error("Fix requires explicit static object properties");
6303
6481
  }
6304
- const values = object.properties.filter((item) => ts6.isPropertyAssignment(item) && nameOf(item.name) === key);
6482
+ const values = object.properties.filter((item) => ts7.isPropertyAssignment(item) && nameOf(item.name) === key);
6305
6483
  if (values.length > 1)
6306
6484
  throw new Error(`Duplicate '${key}' property`);
6307
6485
  return values[0];
6308
6486
  }
6309
6487
  function stringProperty(object, key) {
6310
6488
  const value = property(object, key)?.initializer;
6311
- return value && ts6.isStringLiteral(value) ? value.text : undefined;
6489
+ return value && ts7.isStringLiteral(value) ? value.text : undefined;
6312
6490
  }
6313
6491
  function replaceProperty(source, object, key, value) {
6314
6492
  const previous = property(object, key);
6315
- const replacement = ts6.factory.createPropertyAssignment(key, value);
6493
+ const replacement = ts7.factory.createPropertyAssignment(key, value);
6316
6494
  const properties = object.properties.map((item) => item === previous ? replacement : item);
6317
6495
  if (!previous)
6318
6496
  properties.push(replacement);
6319
- const updated = ts6.factory.updateObjectLiteralExpression(object, properties);
6320
- return source.text.slice(0, object.getStart(source)) + ts6.createPrinter().printNode(ts6.EmitHint.Expression, updated, source) + source.text.slice(object.end);
6497
+ const updated = ts7.factory.updateObjectLiteralExpression(object, properties);
6498
+ return source.text.slice(0, object.getStart(source)) + ts7.createPrinter().printNode(ts7.EmitHint.Expression, updated, source) + source.text.slice(object.end);
6321
6499
  }
6322
6500
  function findClass(source, name) {
6323
- return unique(source.statements.filter((statement) => ts6.isClassDeclaration(statement) && statement.name?.text === name), `class '${name}'`);
6501
+ return unique(source.statements.filter((statement) => ts7.isClassDeclaration(statement) && statement.name?.text === name), `class '${name}'`);
6324
6502
  }
6325
6503
  function decoratorObject(node, name) {
6326
- const decorator = unique((ts6.getDecorators(node) ?? []).filter((item) => ts6.isCallExpression(item.expression) && item.expression.expression.getText() === name), `@${name} decorator`);
6327
- const argument = ts6.isCallExpression(decorator.expression) ? decorator.expression.arguments[0] : undefined;
6328
- if (!argument || !ts6.isObjectLiteralExpression(argument))
6504
+ const decorator = unique((ts7.getDecorators(node) ?? []).filter((item) => ts7.isCallExpression(item.expression) && item.expression.expression.getText() === name), `@${name} decorator`);
6505
+ const argument = ts7.isCallExpression(decorator.expression) ? decorator.expression.arguments[0] : undefined;
6506
+ if (!argument || !ts7.isObjectLiteralExpression(argument))
6329
6507
  throw new Error(`@${name} requires a static object`);
6330
6508
  return argument;
6331
6509
  }
6332
6510
  function moduleObjects(source) {
6333
6511
  const result = [];
6334
6512
  for (const statement of source.statements) {
6335
- if (ts6.isClassDeclaration(statement) && (ts6.getDecorators(statement) ?? []).some((item) => ts6.isCallExpression(item.expression) && item.expression.expression.getText() === "Module")) {
6513
+ if (ts7.isClassDeclaration(statement) && (ts7.getDecorators(statement) ?? []).some((item) => ts7.isCallExpression(item.expression) && item.expression.expression.getText() === "Module")) {
6336
6514
  result.push(decoratorObject(statement, "Module"));
6337
6515
  }
6338
- if (ts6.isVariableStatement(statement)) {
6516
+ if (ts7.isVariableStatement(statement)) {
6339
6517
  for (const declaration of statement.declarationList.declarations) {
6340
6518
  const call = declaration.initializer;
6341
- if (call && ts6.isCallExpression(call) && ["defineModule", "defineFeatureSlice"].includes(call.expression.getText()) && call.arguments[0] && ts6.isObjectLiteralExpression(call.arguments[0])) {
6519
+ if (call && ts7.isCallExpression(call) && ["defineModule", "defineFeatureSlice"].includes(call.expression.getText()) && call.arguments[0] && ts7.isObjectLiteralExpression(call.arguments[0])) {
6342
6520
  result.push(call.arguments[0]);
6343
6521
  }
6344
6522
  }
@@ -6348,23 +6526,23 @@ function moduleObjects(source) {
6348
6526
  }
6349
6527
  function importSymbol(source, path, symbol) {
6350
6528
  identifier(symbol);
6351
- const current = resolve7(source.fileName).replace(/\.(tsx?|mts|cts)$/, "");
6352
- const target = resolve7(dirname6(source.fileName), path).replace(/\.(tsx?|mts|cts)$/, "");
6529
+ const current = resolve8(source.fileName).replace(/\.(tsx?|mts|cts)$/, "");
6530
+ const target = resolve8(dirname6(source.fileName), path).replace(/\.(tsx?|mts|cts)$/, "");
6353
6531
  if (current === target)
6354
6532
  return source.text;
6355
- const matches = source.statements.filter((item) => ts6.isImportDeclaration(item) && ts6.isStringLiteral(item.moduleSpecifier) && item.moduleSpecifier.text === path);
6533
+ const matches = source.statements.filter((item) => ts7.isImportDeclaration(item) && ts7.isStringLiteral(item.moduleSpecifier) && item.moduleSpecifier.text === path);
6356
6534
  if (matches.length > 1)
6357
6535
  throw new Error(`Ambiguous imports from '${path}'`);
6358
6536
  const match = matches[0];
6359
- if (match && ts6.isImportDeclaration(match) && match.importClause?.namedBindings && ts6.isNamedImports(match.importClause.namedBindings) && !match.importClause.isTypeOnly) {
6537
+ if (match && ts7.isImportDeclaration(match) && match.importClause?.namedBindings && ts7.isNamedImports(match.importClause.namedBindings) && !match.importClause.isTypeOnly) {
6360
6538
  if (match.importClause.namedBindings.elements.some((item) => item.name.text === symbol))
6361
6539
  return source.text;
6362
6540
  const bindings = match.importClause.namedBindings;
6363
- const updated = ts6.factory.updateNamedImports(bindings, [
6541
+ const updated = ts7.factory.updateNamedImports(bindings, [
6364
6542
  ...bindings.elements,
6365
- ts6.factory.createImportSpecifier(false, undefined, ts6.factory.createIdentifier(symbol))
6543
+ ts7.factory.createImportSpecifier(false, undefined, ts7.factory.createIdentifier(symbol))
6366
6544
  ]);
6367
- return source.text.slice(0, bindings.getStart(source)) + ts6.createPrinter().printNode(ts6.EmitHint.Unspecified, updated, source) + source.text.slice(bindings.end);
6545
+ return source.text.slice(0, bindings.getStart(source)) + ts7.createPrinter().printNode(ts7.EmitHint.Unspecified, updated, source) + source.text.slice(bindings.end);
6368
6546
  }
6369
6547
  if (match)
6370
6548
  throw new Error(`Import from '${path}' is not a named value import`);
@@ -6515,8 +6693,8 @@ async function run() {
6515
6693
  if (checkSchema && command !== "graphql-schema")
6516
6694
  throw new Error("--check is only supported by graphql-schema");
6517
6695
  const defaults = resolveSupacloudConfig(loadedConfig, process.cwd());
6518
- const resolvedRoot = rootDir ? resolve9(process.cwd(), rootDir) : defaults.rootDir;
6519
- const resolvedOut = outDir ? resolve9(process.cwd(), outDir) : defaults.outDir;
6696
+ const resolvedRoot = rootDir ? resolve10(process.cwd(), rootDir) : defaults.rootDir;
6697
+ const resolvedOut = outDir ? resolve10(process.cwd(), outDir) : defaults.outDir;
6520
6698
  const configured = compileOptionsFromConfig({
6521
6699
  ...loadedConfig,
6522
6700
  root: resolvedRoot,
@@ -6557,7 +6735,7 @@ async function run() {
6557
6735
  } else if (command === "fix") {
6558
6736
  if (!query)
6559
6737
  throw new Error("fix requires a JSON file containing one DiagnosticFix");
6560
- const fix = JSON.parse(await readFile5(resolve9(process.cwd(), query), "utf8"));
6738
+ const fix = JSON.parse(await readFile5(resolve10(process.cwd(), query), "utf8"));
6561
6739
  const result = await applyDiagnosticFix(fix, { rootDir: resolvedRoot, dryRun });
6562
6740
  console.log(JSON.stringify({ ok: true, ...result }, null, 2));
6563
6741
  } else if (command === "compile") {
package/dist/config.d.ts CHANGED
@@ -10,10 +10,15 @@ export interface SupaCloudConfig {
10
10
  generateClient?: boolean;
11
11
  generatePermissions?: boolean;
12
12
  moduleBoundaryPreset?: ModuleBoundaryPresetName;
13
+ moduleBoundaries?: NonNullable<CompileOptions["moduleBoundaries"]>;
14
+ typeSafety?: NonNullable<CompileOptions["typeSafety"]>;
15
+ allowRouteCommandBindings?: boolean;
16
+ disallowControllerDirectDb?: boolean;
17
+ detectOrphanModules?: boolean;
13
18
  commandCapabilities?: CommandExecutionCapabilities;
14
19
  treeShakeUnusedProviders?: boolean;
15
20
  }
16
- export declare const DEFAULT_SUPACLOUD_CONFIG: Required<Omit<SupaCloudConfig, "include" | "moduleBoundaryPreset" | "commandCapabilities">> & {
21
+ export declare const DEFAULT_SUPACLOUD_CONFIG: Required<Omit<SupaCloudConfig, "include" | "moduleBoundaryPreset" | "commandCapabilities" | "moduleBoundaries" | "typeSafety" | "allowRouteCommandBindings" | "disallowControllerDirectDb" | "detectOrphanModules">> & {
17
22
  include: string[];
18
23
  moduleBoundaryPreset: ModuleBoundaryPresetName;
19
24
  };
@@ -28,6 +33,11 @@ export declare function resolveSupacloudConfig(config?: SupaCloudConfig, cwd?: s
28
33
  generatePermissions: boolean;
29
34
  moduleBoundaryPreset: ModuleBoundaryPresetName;
30
35
  commandCapabilities?: CommandExecutionCapabilities;
36
+ moduleBoundaries?: NonNullable<CompileOptions["moduleBoundaries"]>;
37
+ typeSafety?: NonNullable<CompileOptions["typeSafety"]>;
38
+ allowRouteCommandBindings?: boolean;
39
+ disallowControllerDirectDb?: boolean;
40
+ detectOrphanModules?: boolean;
31
41
  treeShakeUnusedProviders: boolean;
32
42
  graphql?: GraphqlOptions;
33
43
  };
@@ -1,2 +1,2 @@
1
1
  /** Appended to the generated SDK; browser consumers need only the platform fetch API. */
2
- export declare const GRAPHQL_CLIENT_SOURCE = "\nexport interface GraphqlClientOptions {\n /** Project base URL, not the management API URL. HTTPS is required outside loopback. */\n url: string;\n /** Public project key only. Never put a service-role or management key in a browser. */\n publishableKey?: string;\n /** Resolved on every request so session refresh and logout are observed. */\n getAccessToken?: () => string | null | undefined | Promise<string | null | undefined>;\n fetch?: (url: string, init: RequestInit) => Promise<Response>;\n}\n\nexport interface GraphqlRequestOptions {\n signal?: AbortSignal;\n}\n\nexport class GraphqlRequestError extends Error {\n constructor(\n public readonly code: \"http\" | \"graphql\" | \"invalid-response\",\n message: string,\n public readonly status?: number,\n public readonly errors?: readonly unknown[],\n ) {\n super(message);\n this.name = \"GraphqlRequestError\";\n }\n}\n\nexport function createGraphqlClient(options: GraphqlClientOptions) {\n const endpoint = new URL(options.url);\n const loopback = [\"localhost\", \"127.0.0.1\", \"[::1]\"].includes(endpoint.hostname);\n if (endpoint.protocol !== \"https:\" && !(endpoint.protocol === \"http:\" && loopback)) {\n throw new Error(\"GraphQL requires HTTPS outside loopback development.\");\n }\n if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {\n throw new Error(\"GraphQL project URLs must not contain credentials, query parameters or fragments.\");\n }\n endpoint.pathname = endpoint.pathname.replace(/\\/$/, \"\") + \"/graphql/v1\";\n const fetcher = options.fetch ?? globalThis.fetch.bind(globalThis);\n return getSdk<GraphqlRequestOptions>(async <R, V>(\n query: string,\n variables?: V,\n request?: GraphqlRequestOptions,\n ): Promise<R> => {\n const headers = new Headers({ \"Content-Type\": \"application/json\", Accept: \"application/json\" });\n if (options.publishableKey) headers.set(\"apikey\", options.publishableKey);\n const token = await options.getAccessToken?.();\n if (token) headers.set(\"Authorization\", \"Bearer \" + token);\n const response = await fetcher(endpoint.toString(), {\n method: \"POST\",\n headers,\n body: JSON.stringify({ query, variables }),\n ...(request?.signal ? { signal: request.signal } : {}),\n redirect: \"error\",\n });\n if (!response.ok) {\n throw new GraphqlRequestError(\"http\", \"GraphQL HTTP request failed (\" + response.status + \").\", response.status);\n }\n let envelope: unknown;\n try {\n envelope = await response.json();\n } catch {\n throw new GraphqlRequestError(\"invalid-response\", \"GraphQL returned invalid JSON.\", response.status);\n }\n if (!envelope || typeof envelope !== \"object\" || Array.isArray(envelope)) {\n throw new GraphqlRequestError(\"invalid-response\", \"GraphQL returned an invalid envelope.\", response.status);\n }\n if (\"errors\" in envelope) {\n if (!Array.isArray(envelope.errors)) {\n throw new GraphqlRequestError(\"invalid-response\", \"GraphQL returned invalid errors.\", response.status);\n }\n if (envelope.errors.length > 0) {\n throw new GraphqlRequestError(\"graphql\", \"GraphQL query failed.\", response.status, envelope.errors);\n }\n }\n if (!(\"data\" in envelope) || !envelope.data || typeof envelope.data !== \"object\" || Array.isArray(envelope.data)) {\n throw new GraphqlRequestError(\"invalid-response\", \"GraphQL returned no result object.\", response.status);\n }\n // Operation types describe the schema snapshot, not runtime response validation.\n return envelope.data as R;\n });\n}\n";
2
+ export declare const GRAPHQL_CLIENT_SOURCE = "\nexport interface GraphqlClientOptions {\n /** Project base URL, not the management API URL. HTTPS is required outside loopback. */\n url: string;\n /** Public project key only. Never put a service-role or management key in a browser. */\n publishableKey?: string;\n /** Resolved on every request so session refresh and logout are observed. */\n getAccessToken?: () => string | null | undefined | Promise<string | null | undefined>;\n fetch?: (url: string, init: RequestInit) => Promise<Response>;\n}\n\nexport interface GraphqlRequestOptions {\n signal?: AbortSignal;\n}\n\nexport class GraphqlRequestError extends Error {\n constructor(\n public readonly code: \"http\" | \"graphql\" | \"invalid-response\",\n message: string,\n public readonly status?: number,\n public readonly errors?: readonly unknown[],\n ) {\n super(message);\n this.name = \"GraphqlRequestError\";\n }\n}\n\nexport function createGraphqlClient(options: GraphqlClientOptions) {\n const endpoint = new URL(options.url);\n const loopback = [\"localhost\", \"127.0.0.1\", \"[::1]\"].includes(endpoint.hostname);\n if (endpoint.protocol !== \"https:\" && !(endpoint.protocol === \"http:\" && loopback)) {\n throw new Error(\"GraphQL requires HTTPS outside loopback development.\");\n }\n if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {\n throw new Error(\"GraphQL project URLs must not contain credentials, query parameters or fragments.\");\n }\n endpoint.pathname = endpoint.pathname.replace(/\\/$/, \"\") + \"/graphql/v1\";\n const fetcher = options.fetch ?? globalThis.fetch.bind(globalThis);\n return getSdk<GraphqlRequestOptions>(async (\n query: string,\n variables?: unknown,\n request?: GraphqlRequestOptions,\n ): Promise<unknown> => {\n const headers = new Headers({ \"Content-Type\": \"application/json\", Accept: \"application/json\" });\n if (options.publishableKey) headers.set(\"apikey\", options.publishableKey);\n const token = await options.getAccessToken?.();\n if (token) headers.set(\"Authorization\", \"Bearer \" + token);\n const response = await fetcher(endpoint.toString(), {\n method: \"POST\",\n headers,\n body: JSON.stringify({ query, variables }),\n ...(request?.signal ? { signal: request.signal } : {}),\n redirect: \"error\",\n });\n if (!response.ok) {\n throw new GraphqlRequestError(\"http\", \"GraphQL HTTP request failed (\" + response.status + \").\", response.status);\n }\n let envelope: unknown;\n try {\n envelope = await response.json();\n } catch {\n throw new GraphqlRequestError(\"invalid-response\", \"GraphQL returned invalid JSON.\", response.status);\n }\n if (!envelope || typeof envelope !== \"object\" || Array.isArray(envelope)) {\n throw new GraphqlRequestError(\"invalid-response\", \"GraphQL returned an invalid envelope.\", response.status);\n }\n if (\"errors\" in envelope) {\n if (!Array.isArray(envelope.errors)) {\n throw new GraphqlRequestError(\"invalid-response\", \"GraphQL returned invalid errors.\", response.status);\n }\n if (envelope.errors.length > 0) {\n throw new GraphqlRequestError(\"graphql\", \"GraphQL query failed.\", response.status, envelope.errors);\n }\n }\n if (!(\"data\" in envelope) || !envelope.data || typeof envelope.data !== \"object\" || Array.isArray(envelope.data)) {\n throw new GraphqlRequestError(\"invalid-response\", \"GraphQL returned no result object.\", response.status);\n }\n // The operation-specific parser in getSdk validates selected field values.\n return envelope.data;\n });\n}\n";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Project the standard Codegen result types, rather than independently interpreting
3
+ * selections, fragments and conditional fields. Unsupported wire types fail closed.
4
+ */
5
+ export declare function renderGraphqlValidators(source: string, operationNames: readonly string[]): string;
package/dist/index.js CHANGED
@@ -25,10 +25,11 @@ function camelName(token) {
25
25
  return token.charAt(0).toLowerCase() + token.slice(1);
26
26
  }
27
27
  function relativeImportPath(fromDir, toFile) {
28
- const fromParts = fromDir.split("/").filter(Boolean);
29
- const toParts = toFile.split("/").filter(Boolean);
28
+ const fromParts = fromDir.split(/[\\/]/).filter(Boolean);
29
+ const toParts = toFile.split(/[\\/]/).filter(Boolean);
30
+ const isWindows = process.platform === "win32" || /^[a-zA-Z]:/.test(fromParts[0] ?? "") || /^[a-zA-Z]:/.test(toParts[0] ?? "");
30
31
  let common = 0;
31
- while (common < fromParts.length && common < toParts.length && fromParts[common] === toParts[common]) {
32
+ while (common < fromParts.length && common < toParts.length && (fromParts[common] === toParts[common] || isWindows && fromParts[common].toLowerCase() === toParts[common].toLowerCase())) {
32
33
  common += 1;
33
34
  }
34
35
  const ups = fromParts.length - common;
@@ -1212,11 +1213,11 @@ export function createGraphqlClient(options: GraphqlClientOptions) {
1212
1213
  }
1213
1214
  endpoint.pathname = endpoint.pathname.replace(/\\/$/, "") + "/graphql/v1";
1214
1215
  const fetcher = options.fetch ?? globalThis.fetch.bind(globalThis);
1215
- return getSdk<GraphqlRequestOptions>(async <R, V>(
1216
+ return getSdk<GraphqlRequestOptions>(async (
1216
1217
  query: string,
1217
- variables?: V,
1218
+ variables?: unknown,
1218
1219
  request?: GraphqlRequestOptions,
1219
- ): Promise<R> => {
1220
+ ): Promise<unknown> => {
1220
1221
  const headers = new Headers({ "Content-Type": "application/json", Accept: "application/json" });
1221
1222
  if (options.publishableKey) headers.set("apikey", options.publishableKey);
1222
1223
  const token = await options.getAccessToken?.();
@@ -1251,8 +1252,8 @@ export function createGraphqlClient(options: GraphqlClientOptions) {
1251
1252
  if (!("data" in envelope) || !envelope.data || typeof envelope.data !== "object" || Array.isArray(envelope.data)) {
1252
1253
  throw new GraphqlRequestError("invalid-response", "GraphQL returned no result object.", response.status);
1253
1254
  }
1254
- // Operation types describe the schema snapshot, not runtime response validation.
1255
- return envelope.data as R;
1255
+ // The operation-specific parser in getSdk validates selected field values.
1256
+ return envelope.data;
1256
1257
  });
1257
1258
  }
1258
1259
  `;
@@ -1326,6 +1327,165 @@ var init_graphql_inputs = __esm(() => {
1326
1327
  init_graphql_options();
1327
1328
  });
1328
1329
 
1330
+ // src/graphql-runtime.ts
1331
+ import * as ts7 from "@typescript/typescript6";
1332
+ import { resolve as resolve5 } from "node:path";
1333
+ function renderGraphqlValidators(source, operationNames) {
1334
+ const fileName = resolve5("/__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: ts7.ScriptTarget.ES2022,
1344
+ lib: ["lib.es2022.d.ts"],
1345
+ types: [],
1346
+ noEmit: true
1347
+ };
1348
+ const host = ts7.createCompilerHost(options);
1349
+ const getSourceFile = host.getSourceFile.bind(host);
1350
+ host.getSourceFile = (path, languageVersion, onError, shouldCreateNewSourceFile) => path === fileName ? ts7.createSourceFile(path, source, languageVersion, true) : getSourceFile(path, languageVersion, onError, shouldCreateNewSourceFile);
1351
+ const program = ts7.createProgram([fileName], options, host);
1352
+ const diagnostics = ts7.getPreEmitDiagnostics(program);
1353
+ if (diagnostics.length) {
1354
+ throw new Error(`Generated GraphQL types cannot be validated: ${diagnostics.map((item) => ts7.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 & ts7.TypeFlags.Any)
1385
+ return unsupported(type);
1386
+ if (type.flags & ts7.TypeFlags.Unknown)
1387
+ return "true";
1388
+ if (type.flags & ts7.TypeFlags.Never)
1389
+ return "false";
1390
+ if (type.flags & ts7.TypeFlags.Null)
1391
+ return "value === null";
1392
+ if (type.flags & ts7.TypeFlags.Undefined)
1393
+ return "value === undefined";
1394
+ if (type.isStringLiteral() || type.isNumberLiteral())
1395
+ return `value === ${JSON.stringify(type.value)}`;
1396
+ if (type.flags & ts7.TypeFlags.BooleanLiteral)
1397
+ return `value === ${checker.typeToString(type)}`;
1398
+ if (type.flags & ts7.TypeFlags.String)
1399
+ return 'typeof value === "string"';
1400
+ if (type.flags & ts7.TypeFlags.Number)
1401
+ return 'typeof value === "number" && Number.isFinite(value)';
1402
+ if (type.flags & ts7.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, ts7.IndexKind.Number);
1412
+ if (!item)
1413
+ return unsupported(type);
1414
+ return `isGraphqlArray(value) && Array.from(value).every(${reference(item)})`;
1415
+ }
1416
+ if (type.flags & ts7.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 & ts7.TypeFlags.String)))
1421
+ return unsupported(type);
1422
+ const properties = checker.getPropertiesOfType(type).map((property2) => {
1423
+ const declaration = property2.valueDeclaration ?? property2.declarations?.[0];
1424
+ if (!declaration)
1425
+ return unsupported(type);
1426
+ const check = reference(checker.getTypeOfSymbolAtLocation(property2, declaration));
1427
+ const key = JSON.stringify(property2.name);
1428
+ const present = `Object.prototype.hasOwnProperty.call(value, ${key})`;
1429
+ return property2.flags & ts7.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
+
1329
1489
  // src/graphql.ts
1330
1490
  var exports_graphql = {};
1331
1491
  __export(exports_graphql, {
@@ -1474,15 +1634,15 @@ async function renderGraphql(options) {
1474
1634
  const separated = separateOperations(combined);
1475
1635
  const methods = Object.entries(separated).sort(([a], [b]) => a.localeCompare(b)).map(([name, document]) => {
1476
1636
  const operation = document.definitions.find((node) => node.kind === Kind.OPERATION_DEFINITION);
1477
- if (operation.kind !== Kind.OPERATION_DEFINITION)
1637
+ if (!operation || operation.kind !== Kind.OPERATION_DEFINITION)
1478
1638
  throw new Error("Missing query operation");
1479
1639
  const required = operation.variableDefinitions?.some((variable) => variable.type.kind === Kind.NON_NULL_TYPE && !variable.defaultValue);
1480
- return ` ${JSON.stringify(name)}(variables${required ? "" : "?"}: ${name}QueryVariables, options?: C): Promise<${name}Query> {
1481
- 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));
1482
1642
  }`;
1483
1643
  });
1484
1644
  const facade = `
1485
- 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>;
1486
1646
  export function getSdk<C>(requester: Requester<C>) {
1487
1647
  return {
1488
1648
  ${methods.join(`,
@@ -1490,8 +1650,9 @@ ${methods.join(`,
1490
1650
  };
1491
1651
  }
1492
1652
  `;
1653
+ const validators = renderGraphqlValidators(generated, queryEntries.map((entry) => entry.name));
1493
1654
  result.files["graphql.ts"] = `// GENERATED BY @supacloud/compiler. DO NOT EDIT.
1494
- ` + generated + facade + GRAPHQL_CLIENT_SOURCE;
1655
+ ` + generated + validators + facade + GRAPHQL_CLIENT_SOURCE;
1495
1656
  if (options.graphql.typedDocuments) {
1496
1657
  const typedDocuments = await import("@graphql-codegen/typed-document-node");
1497
1658
  result.files["graphql.documents.ts"] = `// GENERATED BY @supacloud/compiler. DO NOT EDIT.
@@ -1520,6 +1681,7 @@ ${methods.join(`,
1520
1681
  var init_graphql = __esm(() => {
1521
1682
  init_graphql_inputs();
1522
1683
  init_graphql_options();
1684
+ init_graphql_runtime();
1523
1685
  });
1524
1686
 
1525
1687
  // src/graphql-schema.ts
@@ -1529,7 +1691,7 @@ __export(exports_graphql_schema, {
1529
1691
  });
1530
1692
  import { mkdir as mkdir2, readFile as readFile4 } from "node:fs/promises";
1531
1693
  import { createHash as createHash7 } from "node:crypto";
1532
- import { dirname as dirname7, resolve as resolve8 } from "node:path";
1694
+ import { dirname as dirname7, resolve as resolve9 } from "node:path";
1533
1695
  async function pullGraphqlSchema(options) {
1534
1696
  assertGraphqlOptions({ schema: options.output });
1535
1697
  const endpoint = new URL(options.url);
@@ -1569,7 +1731,7 @@ async function pullGraphqlSchema(options) {
1569
1731
  throw new Error("GraphQL schema export failed. Verify caller grants and enable introspection only in the intended development environment.");
1570
1732
  }
1571
1733
  const schema = lexicographicSortSchema(buildClientSchema2(data));
1572
- const path = resolve8(options.output);
1734
+ const path = resolve9(options.output);
1573
1735
  const content = path.endsWith(".json") ? JSON.stringify(introspectionFromSchema(schema), null, 2) + `
1574
1736
  ` : `# GENERATED BY supacloud-compiler graphql-schema. DO NOT EDIT.
1575
1737
  # Database First: change database declarations, apply migrations, then re-export for the intended role.
@@ -5624,12 +5786,12 @@ function resolveTypeSafety(options) {
5624
5786
  }
5625
5787
  // src/watch.ts
5626
5788
  import { existsSync as existsSync4, watch } from "node:fs";
5627
- import { dirname as dirname5, relative as relative7, resolve as resolve6, sep as sep7 } from "node:path";
5789
+ import { dirname as dirname5, relative as relative7, resolve as resolve7, sep as sep7 } from "node:path";
5628
5790
 
5629
5791
  // src/incremental.ts
5630
5792
  import { createHash as createHash6 } from "node:crypto";
5631
5793
  import { access as access2, readdir, readFile as readFile3 } from "node:fs/promises";
5632
- import { isAbsolute as isAbsolute2, relative as relative6, resolve as resolve5, sep as sep6 } from "node:path";
5794
+ import { isAbsolute as isAbsolute2, relative as relative6, resolve as resolve6, sep as sep6 } from "node:path";
5633
5795
  init_graphql_inputs();
5634
5796
  function createDependencyGraphCache() {
5635
5797
  return {
@@ -5704,11 +5866,11 @@ function createIncrementalCompiler() {
5704
5866
  };
5705
5867
  }
5706
5868
  async function updateSnapshot(previous, options, changedPaths) {
5707
- const rootDir = resolve5(options.rootDir);
5708
- const outDir = resolve5(options.outDir);
5869
+ const rootDir = resolve6(options.rootDir);
5870
+ const outDir = resolve6(options.outDir);
5709
5871
  const files = { ...previous.files };
5710
5872
  for (const changedPath of changedPaths) {
5711
- const absolutePath = isAbsolute2(changedPath) ? resolve5(changedPath) : resolve5(rootDir, changedPath);
5873
+ const absolutePath = isAbsolute2(changedPath) ? resolve6(changedPath) : resolve6(rootDir, changedPath);
5712
5874
  const relativeChangedPath = relative6(rootDir, absolutePath);
5713
5875
  if (relativeChangedPath === ".." || relativeChangedPath.startsWith(`..${sep6}`))
5714
5876
  continue;
@@ -5726,8 +5888,8 @@ async function updateSnapshot(previous, options, changedPaths) {
5726
5888
  return { files, optionsKey: optionsKeyOf(options) };
5727
5889
  }
5728
5890
  async function createSnapshot(options) {
5729
- const rootDir = resolve5(options.rootDir);
5730
- const outDir = resolve5(options.outDir);
5891
+ const rootDir = resolve6(options.rootDir);
5892
+ const outDir = resolve6(options.outDir);
5731
5893
  const paths = await listSourceFiles(rootDir, outDir);
5732
5894
  const files = {};
5733
5895
  for (const path of paths) {
@@ -5746,8 +5908,8 @@ async function createSnapshot(options) {
5746
5908
  }
5747
5909
  function optionsKeyOf(options) {
5748
5910
  return JSON.stringify({
5749
- rootDir: resolve5(options.rootDir),
5750
- outDir: resolve5(options.outDir),
5911
+ rootDir: resolve6(options.rootDir),
5912
+ outDir: resolve6(options.outDir),
5751
5913
  include: options.include,
5752
5914
  strict: options.strict,
5753
5915
  writeOnError: options.writeOnError,
@@ -5769,7 +5931,7 @@ async function listSourceFiles(rootDir, outDir) {
5769
5931
  const result = [];
5770
5932
  const visit = async (directory) => {
5771
5933
  for (const entry of await readdir(directory, { withFileTypes: true })) {
5772
- const path = resolve5(directory, entry.name);
5934
+ const path = resolve6(directory, entry.name);
5773
5935
  if (entry.isDirectory()) {
5774
5936
  if (entry.name === "node_modules" || entry.name === ".git" || path === outDir)
5775
5937
  continue;
@@ -5893,8 +6055,8 @@ function findAffectedModules(previous, current, changedFiles) {
5893
6055
  // src/watch.ts
5894
6056
  var DEFAULT_DEBOUNCE_MS = 100;
5895
6057
  function watchProject(options) {
5896
- const rootDir = resolve6(options.rootDir);
5897
- const outDir = resolve6(options.outDir);
6058
+ const rootDir = resolve7(options.rootDir);
6059
+ const outDir = resolve7(options.outDir);
5898
6060
  const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
5899
6061
  let timer;
5900
6062
  let closed = false;
@@ -5903,7 +6065,7 @@ function watchProject(options) {
5903
6065
  const pendingPaths = new Set;
5904
6066
  let watcher;
5905
6067
  let schemaWatcher;
5906
- const schemaPath = options.graphql ? resolve6(rootDir, options.graphql.schema) : undefined;
6068
+ const schemaPath = options.graphql ? resolve7(rootDir, options.graphql.schema) : undefined;
5907
6069
  const incremental = createIncrementalCompiler();
5908
6070
  let initialEvent;
5909
6071
  let resolveReady = () => {
@@ -5980,7 +6142,7 @@ function watchProject(options) {
5980
6142
  watcher = watch(rootDir, { recursive: true }, (_eventType, filename) => {
5981
6143
  if (!filename)
5982
6144
  return schedule();
5983
- const changedPath = resolve6(rootDir, filename.toString());
6145
+ const changedPath = resolve7(rootDir, filename.toString());
5984
6146
  const relativePath = relative7(outDir, changedPath);
5985
6147
  if (!relativePath.startsWith("..") && relativePath !== "")
5986
6148
  return;
@@ -5993,7 +6155,7 @@ function watchProject(options) {
5993
6155
  while (!existsSync4(directory) && dirname5(directory) !== directory)
5994
6156
  directory = dirname5(directory);
5995
6157
  schemaWatcher = watch(directory, { recursive: true }, (_eventType, filename) => {
5996
- if (!filename || resolve6(directory, filename.toString()) === schemaPath)
6158
+ if (!filename || resolve7(directory, filename.toString()) === schemaPath)
5997
6159
  schedule(schemaPath);
5998
6160
  });
5999
6161
  }
@@ -6291,7 +6453,7 @@ init_generate();
6291
6453
  // src/config.ts
6292
6454
  init_graphql_options();
6293
6455
  import { existsSync as existsSync6 } from "node:fs";
6294
- import { join as join6, resolve as resolve7 } from "node:path";
6456
+ import { join as join6, resolve as resolve8 } from "node:path";
6295
6457
  import { pathToFileURL } from "node:url";
6296
6458
  var DEFAULT_SUPACLOUD_CONFIG = {
6297
6459
  graphql: false,
@@ -6308,6 +6470,7 @@ var DEFAULT_SUPACLOUD_CONFIG = {
6308
6470
  function defineSupacloudConfig(config = {}) {
6309
6471
  if (config.graphql !== undefined && config.graphql !== false)
6310
6472
  assertGraphqlOptions(config.graphql);
6473
+ validateGovernanceConfig(config);
6311
6474
  return {
6312
6475
  ...DEFAULT_SUPACLOUD_CONFIG,
6313
6476
  ...config,
@@ -6315,11 +6478,35 @@ function defineSupacloudConfig(config = {}) {
6315
6478
  graphql: config.graphql ?? DEFAULT_SUPACLOUD_CONFIG.graphql
6316
6479
  };
6317
6480
  }
6481
+ function validateGovernanceConfig(config) {
6482
+ const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
6483
+ const isStrings = (value) => Array.isArray(value) && Array.from(value).every((item) => typeof item === "string" && item.trim().length > 0);
6484
+ if (config.moduleBoundaries !== undefined) {
6485
+ if (!Array.isArray(config.moduleBoundaries))
6486
+ throw new Error("moduleBoundaries must be an array of module tag rules.");
6487
+ const rules = config.moduleBoundaries;
6488
+ for (const rule of rules) {
6489
+ 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"])) {
6490
+ throw new Error("moduleBoundaries rules require sourceTag and optional string arrays onlyDependOnLibsWithTags/bannedDependenciesWithTags.");
6491
+ }
6492
+ }
6493
+ }
6494
+ if (config.typeSafety !== undefined) {
6495
+ const rules = config.typeSafety;
6496
+ 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"])) {
6497
+ throw new Error("typeSafety accepts boolean scanProductionSource/noAnyInGenerated and a string array exclude.");
6498
+ }
6499
+ }
6500
+ for (const key of ["allowRouteCommandBindings", "disallowControllerDirectDb", "detectOrphanModules"]) {
6501
+ if (config[key] !== undefined && typeof config[key] !== "boolean")
6502
+ throw new Error(`${key} must be a boolean.`);
6503
+ }
6504
+ }
6318
6505
  function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
6319
6506
  const resolved = defineSupacloudConfig(config);
6320
6507
  return {
6321
- rootDir: resolve7(cwd, resolved.root ?? DEFAULT_SUPACLOUD_CONFIG.root),
6322
- outDir: resolve7(cwd, resolved.outDir ?? DEFAULT_SUPACLOUD_CONFIG.outDir),
6508
+ rootDir: resolve8(cwd, resolved.root ?? DEFAULT_SUPACLOUD_CONFIG.root),
6509
+ outDir: resolve8(cwd, resolved.outDir ?? DEFAULT_SUPACLOUD_CONFIG.outDir),
6323
6510
  include: resolved.include ?? [...DEFAULT_SUPACLOUD_CONFIG.include],
6324
6511
  strict: resolved.strict ?? DEFAULT_SUPACLOUD_CONFIG.strict,
6325
6512
  requireRouteContracts: resolved.requireRouteContracts ?? DEFAULT_SUPACLOUD_CONFIG.requireRouteContracts,
@@ -6327,10 +6514,15 @@ function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
6327
6514
  generatePermissions: resolved.generatePermissions ?? DEFAULT_SUPACLOUD_CONFIG.generatePermissions,
6328
6515
  moduleBoundaryPreset: resolved.moduleBoundaryPreset ?? DEFAULT_SUPACLOUD_CONFIG.moduleBoundaryPreset,
6329
6516
  commandCapabilities: resolved.commandCapabilities,
6517
+ ...resolved.moduleBoundaries ? { moduleBoundaries: resolved.moduleBoundaries } : {},
6518
+ ...resolved.typeSafety ? { typeSafety: resolved.typeSafety } : {},
6519
+ ...resolved.allowRouteCommandBindings === undefined ? {} : { allowRouteCommandBindings: resolved.allowRouteCommandBindings },
6520
+ ...resolved.disallowControllerDirectDb === undefined ? {} : { disallowControllerDirectDb: resolved.disallowControllerDirectDb },
6521
+ ...resolved.detectOrphanModules === undefined ? {} : { detectOrphanModules: resolved.detectOrphanModules },
6330
6522
  treeShakeUnusedProviders: resolved.treeShakeUnusedProviders ?? DEFAULT_SUPACLOUD_CONFIG.treeShakeUnusedProviders,
6331
6523
  graphql: resolved.graphql ? {
6332
6524
  ...resolved.graphql,
6333
- schema: resolve7(cwd, resolved.graphql.schema)
6525
+ schema: resolve8(cwd, resolved.graphql.schema)
6334
6526
  } : undefined
6335
6527
  };
6336
6528
  }
@@ -6348,20 +6540,7 @@ async function loadSupacloudConfig(cwd = process.cwd()) {
6348
6540
  return defineSupacloudConfig(imported.default ?? {});
6349
6541
  }
6350
6542
  function compileOptionsFromConfig(config, cwd = process.cwd()) {
6351
- const resolved = resolveSupacloudConfig(config, cwd);
6352
- return {
6353
- rootDir: resolved.rootDir,
6354
- outDir: resolved.outDir,
6355
- include: resolved.include,
6356
- strict: resolved.strict,
6357
- requireRouteContracts: resolved.requireRouteContracts,
6358
- generateClient: resolved.generateClient,
6359
- generatePermissions: resolved.generatePermissions,
6360
- moduleBoundaryPreset: resolved.moduleBoundaryPreset,
6361
- commandCapabilities: resolved.commandCapabilities,
6362
- treeShakeUnusedProviders: resolved.treeShakeUnusedProviders,
6363
- graphql: resolved.graphql
6364
- };
6543
+ return resolveSupacloudConfig(config, cwd);
6365
6544
  }
6366
6545
 
6367
6546
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/compiler",
3
- "version": "0.13.1",
3
+ "version": "0.15.0",
4
4
  "description": "Static compiler for @supacloud/app metadata: builds the application graph from AST, validates it, and generates reflection-free factory code",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",