@supacloud/compiler 0.13.1 → 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/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
@@ -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?.();
@@ -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, {
@@ -1475,15 +1634,15 @@ async function renderGraphql(options) {
1475
1634
  const separated = separateOperations(combined);
1476
1635
  const methods = Object.entries(separated).sort(([a], [b]) => a.localeCompare(b)).map(([name, document]) => {
1477
1636
  const operation = document.definitions.find((node) => node.kind === Kind.OPERATION_DEFINITION);
1478
- if (operation.kind !== Kind.OPERATION_DEFINITION)
1637
+ if (!operation || operation.kind !== Kind.OPERATION_DEFINITION)
1479
1638
  throw new Error("Missing query operation");
1480
1639
  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);
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));
1483
1642
  }`;
1484
1643
  });
1485
1644
  const facade = `
1486
- 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>;
1487
1646
  export function getSdk<C>(requester: Requester<C>) {
1488
1647
  return {
1489
1648
  ${methods.join(`,
@@ -1491,8 +1650,9 @@ ${methods.join(`,
1491
1650
  };
1492
1651
  }
1493
1652
  `;
1653
+ const validators = renderGraphqlValidators(generated, queryEntries.map((entry) => entry.name));
1494
1654
  result.files["graphql.ts"] = `// GENERATED BY @supacloud/compiler. DO NOT EDIT.
1495
- ` + generated + facade + GRAPHQL_CLIENT_SOURCE;
1655
+ ` + generated + validators + facade + GRAPHQL_CLIENT_SOURCE;
1496
1656
  if (options.graphql.typedDocuments) {
1497
1657
  const typedDocuments = await import("@graphql-codegen/typed-document-node");
1498
1658
  result.files["graphql.documents.ts"] = `// GENERATED BY @supacloud/compiler. DO NOT EDIT.
@@ -1521,6 +1681,7 @@ ${methods.join(`,
1521
1681
  var init_graphql = __esm(() => {
1522
1682
  init_graphql_inputs();
1523
1683
  init_graphql_options();
1684
+ init_graphql_runtime();
1524
1685
  });
1525
1686
 
1526
1687
  // src/graphql-schema.ts
@@ -1530,7 +1691,7 @@ __export(exports_graphql_schema, {
1530
1691
  });
1531
1692
  import { mkdir as mkdir2, readFile as readFile4 } from "node:fs/promises";
1532
1693
  import { createHash as createHash7 } from "node:crypto";
1533
- import { dirname as dirname7, resolve as resolve8 } from "node:path";
1694
+ import { dirname as dirname7, resolve as resolve9 } from "node:path";
1534
1695
  async function pullGraphqlSchema(options) {
1535
1696
  assertGraphqlOptions({ schema: options.output });
1536
1697
  const endpoint = new URL(options.url);
@@ -1570,11 +1731,10 @@ async function pullGraphqlSchema(options) {
1570
1731
  throw new Error("GraphQL schema export failed. Verify caller grants and enable introspection only in the intended development environment.");
1571
1732
  }
1572
1733
  const schema = lexicographicSortSchema(buildClientSchema2(data));
1573
- const path = resolve8(options.output);
1734
+ const path = resolve9(options.output);
1574
1735
  const content = path.endsWith(".json") ? JSON.stringify(introspectionFromSchema(schema), null, 2) + `
1575
1736
  ` : `# GENERATED BY supacloud-compiler graphql-schema. DO NOT EDIT.
1576
1737
  # 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
1738
  ` + printSchema2(schema) + `
1579
1739
  `;
1580
1740
  let previous;
@@ -1598,7 +1758,7 @@ var init_graphql_schema = __esm(() => {
1598
1758
  });
1599
1759
 
1600
1760
  // src/cli.ts
1601
- import { resolve as resolve9 } from "node:path";
1761
+ import { resolve as resolve10 } from "node:path";
1602
1762
  import { readFile as readFile5 } from "node:fs/promises";
1603
1763
 
1604
1764
  // src/analyze.ts
@@ -5701,12 +5861,12 @@ function exportGraphDot(graph) {
5701
5861
 
5702
5862
  // src/watch.ts
5703
5863
  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";
5864
+ import { dirname as dirname5, relative as relative7, resolve as resolve6, sep as sep7 } from "node:path";
5705
5865
 
5706
5866
  // src/incremental.ts
5707
5867
  import { createHash as createHash6 } from "node:crypto";
5708
5868
  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";
5869
+ import { isAbsolute, relative as relative6, resolve as resolve5, sep as sep6 } from "node:path";
5710
5870
  init_graphql_inputs();
5711
5871
  function createDependencyGraphCache() {
5712
5872
  return {
@@ -5781,11 +5941,11 @@ function createIncrementalCompiler() {
5781
5941
  };
5782
5942
  }
5783
5943
  async function updateSnapshot(previous, options, changedPaths) {
5784
- const rootDir = resolve4(options.rootDir);
5785
- const outDir = resolve4(options.outDir);
5944
+ const rootDir = resolve5(options.rootDir);
5945
+ const outDir = resolve5(options.outDir);
5786
5946
  const files = { ...previous.files };
5787
5947
  for (const changedPath of changedPaths) {
5788
- const absolutePath = isAbsolute(changedPath) ? resolve4(changedPath) : resolve4(rootDir, changedPath);
5948
+ const absolutePath = isAbsolute(changedPath) ? resolve5(changedPath) : resolve5(rootDir, changedPath);
5789
5949
  const relativeChangedPath = relative6(rootDir, absolutePath);
5790
5950
  if (relativeChangedPath === ".." || relativeChangedPath.startsWith(`..${sep6}`))
5791
5951
  continue;
@@ -5803,8 +5963,8 @@ async function updateSnapshot(previous, options, changedPaths) {
5803
5963
  return { files, optionsKey: optionsKeyOf(options) };
5804
5964
  }
5805
5965
  async function createSnapshot(options) {
5806
- const rootDir = resolve4(options.rootDir);
5807
- const outDir = resolve4(options.outDir);
5966
+ const rootDir = resolve5(options.rootDir);
5967
+ const outDir = resolve5(options.outDir);
5808
5968
  const paths = await listSourceFiles(rootDir, outDir);
5809
5969
  const files = {};
5810
5970
  for (const path of paths) {
@@ -5823,8 +5983,8 @@ async function createSnapshot(options) {
5823
5983
  }
5824
5984
  function optionsKeyOf(options) {
5825
5985
  return JSON.stringify({
5826
- rootDir: resolve4(options.rootDir),
5827
- outDir: resolve4(options.outDir),
5986
+ rootDir: resolve5(options.rootDir),
5987
+ outDir: resolve5(options.outDir),
5828
5988
  include: options.include,
5829
5989
  strict: options.strict,
5830
5990
  writeOnError: options.writeOnError,
@@ -5846,7 +6006,7 @@ async function listSourceFiles(rootDir, outDir) {
5846
6006
  const result = [];
5847
6007
  const visit = async (directory) => {
5848
6008
  for (const entry of await readdir(directory, { withFileTypes: true })) {
5849
- const path = resolve4(directory, entry.name);
6009
+ const path = resolve5(directory, entry.name);
5850
6010
  if (entry.isDirectory()) {
5851
6011
  if (entry.name === "node_modules" || entry.name === ".git" || path === outDir)
5852
6012
  continue;
@@ -5970,8 +6130,8 @@ function findAffectedModules(previous, current, changedFiles) {
5970
6130
  // src/watch.ts
5971
6131
  var DEFAULT_DEBOUNCE_MS = 100;
5972
6132
  function watchProject(options) {
5973
- const rootDir = resolve5(options.rootDir);
5974
- const outDir = resolve5(options.outDir);
6133
+ const rootDir = resolve6(options.rootDir);
6134
+ const outDir = resolve6(options.outDir);
5975
6135
  const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
5976
6136
  let timer;
5977
6137
  let closed = false;
@@ -5980,7 +6140,7 @@ function watchProject(options) {
5980
6140
  const pendingPaths = new Set;
5981
6141
  let watcher;
5982
6142
  let schemaWatcher;
5983
- const schemaPath = options.graphql ? resolve5(rootDir, options.graphql.schema) : undefined;
6143
+ const schemaPath = options.graphql ? resolve6(rootDir, options.graphql.schema) : undefined;
5984
6144
  const incremental = createIncrementalCompiler();
5985
6145
  let initialEvent;
5986
6146
  let resolveReady = () => {
@@ -6057,7 +6217,7 @@ function watchProject(options) {
6057
6217
  watcher = watch(rootDir, { recursive: true }, (_eventType, filename) => {
6058
6218
  if (!filename)
6059
6219
  return schedule();
6060
- const changedPath = resolve5(rootDir, filename.toString());
6220
+ const changedPath = resolve6(rootDir, filename.toString());
6061
6221
  const relativePath = relative7(outDir, changedPath);
6062
6222
  if (!relativePath.startsWith("..") && relativePath !== "")
6063
6223
  return;
@@ -6070,7 +6230,7 @@ function watchProject(options) {
6070
6230
  while (!existsSync5(directory) && dirname5(directory) !== directory)
6071
6231
  directory = dirname5(directory);
6072
6232
  schemaWatcher = watch(directory, { recursive: true }, (_eventType, filename) => {
6073
- if (!filename || resolve5(directory, filename.toString()) === schemaPath)
6233
+ if (!filename || resolve6(directory, filename.toString()) === schemaPath)
6074
6234
  schedule(schemaPath);
6075
6235
  });
6076
6236
  }
@@ -6097,7 +6257,7 @@ function watchProject(options) {
6097
6257
  // src/config.ts
6098
6258
  init_graphql_options();
6099
6259
  import { existsSync as existsSync6 } from "node:fs";
6100
- import { join as join6, resolve as resolve6 } from "node:path";
6260
+ import { join as join6, resolve as resolve7 } from "node:path";
6101
6261
  import { pathToFileURL } from "node:url";
6102
6262
  var DEFAULT_SUPACLOUD_CONFIG = {
6103
6263
  graphql: false,
@@ -6114,6 +6274,7 @@ var DEFAULT_SUPACLOUD_CONFIG = {
6114
6274
  function defineSupacloudConfig(config = {}) {
6115
6275
  if (config.graphql !== undefined && config.graphql !== false)
6116
6276
  assertGraphqlOptions(config.graphql);
6277
+ validateGovernanceConfig(config);
6117
6278
  return {
6118
6279
  ...DEFAULT_SUPACLOUD_CONFIG,
6119
6280
  ...config,
@@ -6121,11 +6282,35 @@ function defineSupacloudConfig(config = {}) {
6121
6282
  graphql: config.graphql ?? DEFAULT_SUPACLOUD_CONFIG.graphql
6122
6283
  };
6123
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
+ }
6124
6309
  function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
6125
6310
  const resolved = defineSupacloudConfig(config);
6126
6311
  return {
6127
- rootDir: resolve6(cwd, resolved.root ?? DEFAULT_SUPACLOUD_CONFIG.root),
6128
- 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),
6129
6314
  include: resolved.include ?? [...DEFAULT_SUPACLOUD_CONFIG.include],
6130
6315
  strict: resolved.strict ?? DEFAULT_SUPACLOUD_CONFIG.strict,
6131
6316
  requireRouteContracts: resolved.requireRouteContracts ?? DEFAULT_SUPACLOUD_CONFIG.requireRouteContracts,
@@ -6133,10 +6318,15 @@ function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
6133
6318
  generatePermissions: resolved.generatePermissions ?? DEFAULT_SUPACLOUD_CONFIG.generatePermissions,
6134
6319
  moduleBoundaryPreset: resolved.moduleBoundaryPreset ?? DEFAULT_SUPACLOUD_CONFIG.moduleBoundaryPreset,
6135
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 },
6136
6326
  treeShakeUnusedProviders: resolved.treeShakeUnusedProviders ?? DEFAULT_SUPACLOUD_CONFIG.treeShakeUnusedProviders,
6137
6327
  graphql: resolved.graphql ? {
6138
6328
  ...resolved.graphql,
6139
- schema: resolve6(cwd, resolved.graphql.schema)
6329
+ schema: resolve7(cwd, resolved.graphql.schema)
6140
6330
  } : undefined
6141
6331
  };
6142
6332
  }
@@ -6154,32 +6344,19 @@ async function loadSupacloudConfig(cwd = process.cwd()) {
6154
6344
  return defineSupacloudConfig(imported.default ?? {});
6155
6345
  }
6156
6346
  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
- };
6347
+ return resolveSupacloudConfig(config, cwd);
6171
6348
  }
6172
6349
 
6173
6350
  // src/fixes.ts
6174
6351
  import { randomUUID } from "node:crypto";
6175
6352
  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";
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";
6178
6355
  async function applyDiagnosticFix(fix, options = {}) {
6179
6356
  if (!fix || typeof fix.targetFile !== "string")
6180
6357
  throw new Error("Invalid DiagnosticFix");
6181
6358
  const root = await realpath(options.rootDir ?? process.cwd());
6182
- const file = resolve7(root, fix.targetFile);
6359
+ const file = resolve8(root, fix.targetFile);
6183
6360
  const stat = await lstat(file);
6184
6361
  const resolved = await realpath(file);
6185
6362
  const relativePath = relative8(root, resolved);
@@ -6199,7 +6376,7 @@ async function applyDiagnosticFix(fix, options = {}) {
6199
6376
  if (!current || current.initializer.getText(source) !== fix.expectedExpression) {
6200
6377
  throw new Error("Command mode changed since diagnosis; analyze the project again");
6201
6378
  }
6202
- content = replaceProperty(source, object, fix.property, ts6.factory.createStringLiteral(fix.value));
6379
+ content = replaceProperty(source, object, fix.property, ts7.factory.createStringLiteral(fix.value));
6203
6380
  break;
6204
6381
  }
6205
6382
  case "add_module_import": {
@@ -6210,15 +6387,15 @@ async function applyDiagnosticFix(fix, options = {}) {
6210
6387
  source = parse2(file, withImport);
6211
6388
  const object = unique(moduleObjects(source).filter((candidate) => !fix.targetModule || stringProperty(candidate, "name") === fix.targetModule), "target module");
6212
6389
  const imports = property(object, "imports");
6213
- if (imports && !ts6.isArrayLiteralExpression(imports.initializer)) {
6390
+ if (imports && !ts7.isArrayLiteralExpression(imports.initializer)) {
6214
6391
  throw new Error("Module imports must be a static array");
6215
6392
  }
6216
- const values = imports && ts6.isArrayLiteralExpression(imports.initializer) ? imports.initializer.elements : [];
6217
- if (values.some(ts6.isSpreadElement))
6393
+ const values = imports && ts7.isArrayLiteralExpression(imports.initializer) ? imports.initializer.elements : [];
6394
+ if (values.some(ts7.isSpreadElement))
6218
6395
  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([
6396
+ content = values.some((value) => ts7.isIdentifier(value) && value.text === fix.symbol) ? withImport : replaceProperty(source, object, "imports", ts7.factory.createArrayLiteralExpression([
6220
6397
  ...values,
6221
- ts6.factory.createIdentifier(fix.symbol)
6398
+ ts7.factory.createIdentifier(fix.symbol)
6222
6399
  ]));
6223
6400
  break;
6224
6401
  }
@@ -6230,24 +6407,24 @@ async function applyDiagnosticFix(fix, options = {}) {
6230
6407
  const command = findClass(source, fix.command);
6231
6408
  const object = decoratorObject(command, "Command");
6232
6409
  const current = property(object, "permission");
6233
- if (current && (!ts6.isStringLiteral(current.initializer) || current.initializer.text !== permission)) {
6410
+ if (current && (!ts7.isStringLiteral(current.initializer) || current.initializer.text !== permission)) {
6234
6411
  throw new Error("Command permission already exists with a different value");
6235
6412
  }
6236
- content = current ? original : replaceProperty(source, object, "permission", ts6.factory.createStringLiteral(permission));
6413
+ content = current ? original : replaceProperty(source, object, "permission", ts7.factory.createStringLiteral(permission));
6237
6414
  break;
6238
6415
  }
6239
6416
  case "add_route_parameter_binding": {
6240
6417
  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");
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");
6243
6420
  const binding = fix.binding === "param" ? "Param" : fix.binding === "query" ? "Query" : undefined;
6244
6421
  if (!binding)
6245
6422
  throw new Error("Invalid route binding");
6246
- const decorators = ts6.getDecorators(parameter) ?? [];
6423
+ const decorators = ts7.getDecorators(parameter) ?? [];
6247
6424
  if (decorators.length > 0)
6248
6425
  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)) {
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)) {
6251
6428
  throw new Error("Framework import must be static");
6252
6429
  }
6253
6430
  const edited = original.slice(0, parameter.getStart(source)) + `@${binding}(${JSON.stringify(fix.parameter)}) ` + original.slice(parameter.getStart(source));
@@ -6273,15 +6450,15 @@ async function applyDiagnosticFix(fix, options = {}) {
6273
6450
  return result;
6274
6451
  }
6275
6452
  function parse2(file, text) {
6276
- const result = ts6.transpileModule(text, {
6453
+ const result = ts7.transpileModule(text, {
6277
6454
  fileName: file,
6278
6455
  reportDiagnostics: true,
6279
- compilerOptions: { target: ts6.ScriptTarget.ESNext, experimentalDecorators: true }
6456
+ compilerOptions: { target: ts7.ScriptTarget.ESNext, experimentalDecorators: true }
6280
6457
  });
6281
- if (result.diagnostics?.some((item) => item.category === ts6.DiagnosticCategory.Error)) {
6458
+ if (result.diagnostics?.some((item) => item.category === ts7.DiagnosticCategory.Error)) {
6282
6459
  throw new Error("Cannot fix syntactically invalid TypeScript");
6283
6460
  }
6284
- 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);
6285
6462
  }
6286
6463
  function unique(items, description) {
6287
6464
  if (items.length !== 1)
@@ -6295,50 +6472,50 @@ function identifier(value) {
6295
6472
  function nameOf(name) {
6296
6473
  if (!name)
6297
6474
  return "";
6298
- 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 : "";
6299
6476
  }
6300
6477
  function property(object, key) {
6301
- 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))) {
6302
6479
  throw new Error("Fix requires explicit static object properties");
6303
6480
  }
6304
- 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);
6305
6482
  if (values.length > 1)
6306
6483
  throw new Error(`Duplicate '${key}' property`);
6307
6484
  return values[0];
6308
6485
  }
6309
6486
  function stringProperty(object, key) {
6310
6487
  const value = property(object, key)?.initializer;
6311
- return value && ts6.isStringLiteral(value) ? value.text : undefined;
6488
+ return value && ts7.isStringLiteral(value) ? value.text : undefined;
6312
6489
  }
6313
6490
  function replaceProperty(source, object, key, value) {
6314
6491
  const previous = property(object, key);
6315
- const replacement = ts6.factory.createPropertyAssignment(key, value);
6492
+ const replacement = ts7.factory.createPropertyAssignment(key, value);
6316
6493
  const properties = object.properties.map((item) => item === previous ? replacement : item);
6317
6494
  if (!previous)
6318
6495
  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);
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);
6321
6498
  }
6322
6499
  function findClass(source, name) {
6323
- 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}'`);
6324
6501
  }
6325
6502
  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))
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))
6329
6506
  throw new Error(`@${name} requires a static object`);
6330
6507
  return argument;
6331
6508
  }
6332
6509
  function moduleObjects(source) {
6333
6510
  const result = [];
6334
6511
  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")) {
6512
+ if (ts7.isClassDeclaration(statement) && (ts7.getDecorators(statement) ?? []).some((item) => ts7.isCallExpression(item.expression) && item.expression.expression.getText() === "Module")) {
6336
6513
  result.push(decoratorObject(statement, "Module"));
6337
6514
  }
6338
- if (ts6.isVariableStatement(statement)) {
6515
+ if (ts7.isVariableStatement(statement)) {
6339
6516
  for (const declaration of statement.declarationList.declarations) {
6340
6517
  const call = declaration.initializer;
6341
- 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])) {
6342
6519
  result.push(call.arguments[0]);
6343
6520
  }
6344
6521
  }
@@ -6348,23 +6525,23 @@ function moduleObjects(source) {
6348
6525
  }
6349
6526
  function importSymbol(source, path, symbol) {
6350
6527
  identifier(symbol);
6351
- const current = resolve7(source.fileName).replace(/\.(tsx?|mts|cts)$/, "");
6352
- 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)$/, "");
6353
6530
  if (current === target)
6354
6531
  return source.text;
6355
- 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);
6356
6533
  if (matches.length > 1)
6357
6534
  throw new Error(`Ambiguous imports from '${path}'`);
6358
6535
  const match = matches[0];
6359
- 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) {
6360
6537
  if (match.importClause.namedBindings.elements.some((item) => item.name.text === symbol))
6361
6538
  return source.text;
6362
6539
  const bindings = match.importClause.namedBindings;
6363
- const updated = ts6.factory.updateNamedImports(bindings, [
6540
+ const updated = ts7.factory.updateNamedImports(bindings, [
6364
6541
  ...bindings.elements,
6365
- ts6.factory.createImportSpecifier(false, undefined, ts6.factory.createIdentifier(symbol))
6542
+ ts7.factory.createImportSpecifier(false, undefined, ts7.factory.createIdentifier(symbol))
6366
6543
  ]);
6367
- 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);
6368
6545
  }
6369
6546
  if (match)
6370
6547
  throw new Error(`Import from '${path}' is not a named value import`);
@@ -6515,8 +6692,8 @@ async function run() {
6515
6692
  if (checkSchema && command !== "graphql-schema")
6516
6693
  throw new Error("--check is only supported by graphql-schema");
6517
6694
  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;
6695
+ const resolvedRoot = rootDir ? resolve10(process.cwd(), rootDir) : defaults.rootDir;
6696
+ const resolvedOut = outDir ? resolve10(process.cwd(), outDir) : defaults.outDir;
6520
6697
  const configured = compileOptionsFromConfig({
6521
6698
  ...loadedConfig,
6522
6699
  root: resolvedRoot,
@@ -6557,7 +6734,7 @@ async function run() {
6557
6734
  } else if (command === "fix") {
6558
6735
  if (!query)
6559
6736
  throw new Error("fix requires a JSON file containing one DiagnosticFix");
6560
- const fix = JSON.parse(await readFile5(resolve9(process.cwd(), query), "utf8"));
6737
+ const fix = JSON.parse(await readFile5(resolve10(process.cwd(), query), "utf8"));
6561
6738
  const result = await applyDiagnosticFix(fix, { rootDir: resolvedRoot, dryRun });
6562
6739
  console.log(JSON.stringify({ ok: true, ...result }, null, 2));
6563
6740
  } 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
@@ -1212,11 +1212,11 @@ export function createGraphqlClient(options: GraphqlClientOptions) {
1212
1212
  }
1213
1213
  endpoint.pathname = endpoint.pathname.replace(/\\/$/, "") + "/graphql/v1";
1214
1214
  const fetcher = options.fetch ?? globalThis.fetch.bind(globalThis);
1215
- return getSdk<GraphqlRequestOptions>(async <R, V>(
1215
+ return getSdk<GraphqlRequestOptions>(async (
1216
1216
  query: string,
1217
- variables?: V,
1217
+ variables?: unknown,
1218
1218
  request?: GraphqlRequestOptions,
1219
- ): Promise<R> => {
1219
+ ): Promise<unknown> => {
1220
1220
  const headers = new Headers({ "Content-Type": "application/json", Accept: "application/json" });
1221
1221
  if (options.publishableKey) headers.set("apikey", options.publishableKey);
1222
1222
  const token = await options.getAccessToken?.();
@@ -1251,8 +1251,8 @@ export function createGraphqlClient(options: GraphqlClientOptions) {
1251
1251
  if (!("data" in envelope) || !envelope.data || typeof envelope.data !== "object" || Array.isArray(envelope.data)) {
1252
1252
  throw new GraphqlRequestError("invalid-response", "GraphQL returned no result object.", response.status);
1253
1253
  }
1254
- // Operation types describe the schema snapshot, not runtime response validation.
1255
- return envelope.data as R;
1254
+ // The operation-specific parser in getSdk validates selected field values.
1255
+ return envelope.data;
1256
1256
  });
1257
1257
  }
1258
1258
  `;
@@ -1326,6 +1326,165 @@ var init_graphql_inputs = __esm(() => {
1326
1326
  init_graphql_options();
1327
1327
  });
1328
1328
 
1329
+ // src/graphql-runtime.ts
1330
+ import * as ts7 from "@typescript/typescript6";
1331
+ import { resolve as resolve5 } from "node:path";
1332
+ function renderGraphqlValidators(source, operationNames) {
1333
+ const fileName = resolve5("/__supacloud_graphql__/contracts.ts");
1334
+ const options = {
1335
+ strict: true,
1336
+ noUncheckedIndexedAccess: true,
1337
+ exactOptionalPropertyTypes: true,
1338
+ noImplicitOverride: true,
1339
+ noPropertyAccessFromIndexSignature: true,
1340
+ noFallthroughCasesInSwitch: true,
1341
+ skipLibCheck: false,
1342
+ target: ts7.ScriptTarget.ES2022,
1343
+ lib: ["lib.es2022.d.ts"],
1344
+ types: [],
1345
+ noEmit: true
1346
+ };
1347
+ const host = ts7.createCompilerHost(options);
1348
+ const getSourceFile = host.getSourceFile.bind(host);
1349
+ host.getSourceFile = (path, languageVersion, onError, shouldCreateNewSourceFile) => path === fileName ? ts7.createSourceFile(path, source, languageVersion, true) : getSourceFile(path, languageVersion, onError, shouldCreateNewSourceFile);
1350
+ const program = ts7.createProgram([fileName], options, host);
1351
+ const diagnostics = ts7.getPreEmitDiagnostics(program);
1352
+ if (diagnostics.length) {
1353
+ throw new Error(`Generated GraphQL types cannot be validated: ${diagnostics.map((item) => ts7.flattenDiagnosticMessageText(item.messageText, `
1354
+ `)).join("; ")}`);
1355
+ }
1356
+ const checker = program.getTypeChecker();
1357
+ const entry = program.getSourceFile(fileName);
1358
+ const module = entry && checker.getSymbolAtLocation(entry);
1359
+ if (!module)
1360
+ throw new Error("Generated GraphQL types have no module");
1361
+ const exports = new Map(checker.getExportsOfModule(module).map((symbol) => [symbol.name, symbol]));
1362
+ if (exports.has("GraphqlQueryResults")) {
1363
+ throw new Error("GraphQL type GraphqlQueryResults conflicts with the generated result registry.");
1364
+ }
1365
+ const names = new Map;
1366
+ const definitions = new Map;
1367
+ function unsupported(type) {
1368
+ throw new Error(`Unsupported GraphQL result wire type: ${checker.typeToString(type)}. Map custom scalars to JSON wire types or unknown.`);
1369
+ }
1370
+ function reference(type) {
1371
+ const existing = names.get(type);
1372
+ if (existing)
1373
+ return existing;
1374
+ const name = `checkGraphqlValue${names.size}`;
1375
+ names.set(type, name);
1376
+ definitions.set(name, "");
1377
+ definitions.set(name, `function ${name}(value: unknown): boolean {
1378
+ return ${expression(type)};
1379
+ }`);
1380
+ return name;
1381
+ }
1382
+ function expression(type) {
1383
+ if (type.flags & ts7.TypeFlags.Any)
1384
+ return unsupported(type);
1385
+ if (type.flags & ts7.TypeFlags.Unknown)
1386
+ return "true";
1387
+ if (type.flags & ts7.TypeFlags.Never)
1388
+ return "false";
1389
+ if (type.flags & ts7.TypeFlags.Null)
1390
+ return "value === null";
1391
+ if (type.flags & ts7.TypeFlags.Undefined)
1392
+ return "value === undefined";
1393
+ if (type.isStringLiteral() || type.isNumberLiteral())
1394
+ return `value === ${JSON.stringify(type.value)}`;
1395
+ if (type.flags & ts7.TypeFlags.BooleanLiteral)
1396
+ return `value === ${checker.typeToString(type)}`;
1397
+ if (type.flags & ts7.TypeFlags.String)
1398
+ return 'typeof value === "string"';
1399
+ if (type.flags & ts7.TypeFlags.Number)
1400
+ return 'typeof value === "number" && Number.isFinite(value)';
1401
+ if (type.flags & ts7.TypeFlags.Boolean)
1402
+ return 'typeof value === "boolean"';
1403
+ if (type.isUnion())
1404
+ return type.types.map((part) => `${reference(part)}(value)`).join(" || ");
1405
+ if (type.isIntersection())
1406
+ return type.types.map((part) => `${reference(part)}(value)`).join(" && ");
1407
+ if (checker.isTupleType(type))
1408
+ return unsupported(type);
1409
+ if (checker.isArrayType(type)) {
1410
+ const item = checker.getIndexTypeOfType(type, ts7.IndexKind.Number);
1411
+ if (!item)
1412
+ return unsupported(type);
1413
+ return `isGraphqlArray(value) && Array.from(value).every(${reference(item)})`;
1414
+ }
1415
+ if (type.flags & ts7.TypeFlags.Object) {
1416
+ if (type.getCallSignatures().length || type.getConstructSignatures().length)
1417
+ return unsupported(type);
1418
+ const indexes = checker.getIndexInfosOfType(type);
1419
+ if (indexes.some((index) => !(index.keyType.flags & ts7.TypeFlags.String)))
1420
+ return unsupported(type);
1421
+ const properties = checker.getPropertiesOfType(type).map((property2) => {
1422
+ const declaration = property2.valueDeclaration ?? property2.declarations?.[0];
1423
+ if (!declaration)
1424
+ return unsupported(type);
1425
+ const check = reference(checker.getTypeOfSymbolAtLocation(property2, declaration));
1426
+ const key = JSON.stringify(property2.name);
1427
+ const present = `Object.prototype.hasOwnProperty.call(value, ${key})`;
1428
+ return property2.flags & ts7.SymbolFlags.Optional ? `(!${present} || ${check}(value[${key}]))` : `(${present} && ${check}(value[${key}]))`;
1429
+ });
1430
+ const indexedValues = indexes.map((index) => `Object.values(value).every(${reference(index.type)})`);
1431
+ return ["isGraphqlRecord(value)", ...properties, ...indexedValues].join(" && ");
1432
+ }
1433
+ return unsupported(type);
1434
+ }
1435
+ const operations = [...operationNames].sort();
1436
+ const parsers = operations.map((name) => {
1437
+ const typeName = `${name}Query`;
1438
+ const symbol = exports.get(typeName);
1439
+ if (!symbol)
1440
+ throw new Error(`Missing generated operation type: ${typeName}`);
1441
+ const check = reference(checker.getDeclaredTypeOfSymbol(symbol));
1442
+ return `export function is${typeName}(value: unknown): value is ${typeName} {
1443
+ return ${check}(value);
1444
+ }
1445
+ export function parse${typeName}(value: unknown): ${typeName} {
1446
+ if (!is${typeName}(value)) {
1447
+ throw new GraphqlRequestError("invalid-response", ${JSON.stringify(`GraphQL result does not match ${name}.`)});
1448
+ }
1449
+ return value;
1450
+ }`;
1451
+ });
1452
+ return `
1453
+ function isGraphqlRecord(value: unknown): value is Record<string, unknown> {
1454
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1455
+ }
1456
+ function isGraphqlArray(value: unknown): value is unknown[] {
1457
+ return Array.isArray(value);
1458
+ }
1459
+ ${[...definitions.values()].join(`
1460
+ `)}
1461
+ ${parsers.join(`
1462
+ `)}
1463
+ export interface GraphqlQueryResults {
1464
+ ${operations.map((name) => ` ${JSON.stringify(name)}: ${name}Query;`).join(`
1465
+ `)}
1466
+ }
1467
+ export function isGraphqlResult<Name extends keyof GraphqlQueryResults>(
1468
+ name: Name, value: unknown,
1469
+ ): value is GraphqlQueryResults[Name] {
1470
+ switch (name) {
1471
+ ${operations.map((name) => ` case ${JSON.stringify(name)}: return is${name}Query(value);`).join(`
1472
+ `)}
1473
+ default: return false;
1474
+ }
1475
+ }
1476
+ export function parseGraphqlResult<Name extends keyof GraphqlQueryResults>(
1477
+ name: Name, value: unknown,
1478
+ ): GraphqlQueryResults[Name] {
1479
+ if (!isGraphqlResult(name, value)) {
1480
+ throw new GraphqlRequestError("invalid-response", "GraphQL result does not match operation " + name + ".");
1481
+ }
1482
+ return value;
1483
+ }
1484
+ `;
1485
+ }
1486
+ var init_graphql_runtime = () => {};
1487
+
1329
1488
  // src/graphql.ts
1330
1489
  var exports_graphql = {};
1331
1490
  __export(exports_graphql, {
@@ -1474,15 +1633,15 @@ async function renderGraphql(options) {
1474
1633
  const separated = separateOperations(combined);
1475
1634
  const methods = Object.entries(separated).sort(([a], [b]) => a.localeCompare(b)).map(([name, document]) => {
1476
1635
  const operation = document.definitions.find((node) => node.kind === Kind.OPERATION_DEFINITION);
1477
- if (operation.kind !== Kind.OPERATION_DEFINITION)
1636
+ if (!operation || operation.kind !== Kind.OPERATION_DEFINITION)
1478
1637
  throw new Error("Missing query operation");
1479
1638
  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);
1639
+ return ` async ${JSON.stringify(name)}(variables${required ? "" : "?"}: ${name}QueryVariables, options?: C): Promise<${name}Query> {
1640
+ return parse${name}Query(await requester(${JSON.stringify(print(document))}, variables, options));
1482
1641
  }`;
1483
1642
  });
1484
1643
  const facade = `
1485
- export type Requester<C> = <R, V>(query: string, variables?: V, options?: C) => Promise<R>;
1644
+ export type Requester<C> = (query: string, variables?: unknown, options?: C) => Promise<unknown>;
1486
1645
  export function getSdk<C>(requester: Requester<C>) {
1487
1646
  return {
1488
1647
  ${methods.join(`,
@@ -1490,8 +1649,9 @@ ${methods.join(`,
1490
1649
  };
1491
1650
  }
1492
1651
  `;
1652
+ const validators = renderGraphqlValidators(generated, queryEntries.map((entry) => entry.name));
1493
1653
  result.files["graphql.ts"] = `// GENERATED BY @supacloud/compiler. DO NOT EDIT.
1494
- ` + generated + facade + GRAPHQL_CLIENT_SOURCE;
1654
+ ` + generated + validators + facade + GRAPHQL_CLIENT_SOURCE;
1495
1655
  if (options.graphql.typedDocuments) {
1496
1656
  const typedDocuments = await import("@graphql-codegen/typed-document-node");
1497
1657
  result.files["graphql.documents.ts"] = `// GENERATED BY @supacloud/compiler. DO NOT EDIT.
@@ -1520,6 +1680,7 @@ ${methods.join(`,
1520
1680
  var init_graphql = __esm(() => {
1521
1681
  init_graphql_inputs();
1522
1682
  init_graphql_options();
1683
+ init_graphql_runtime();
1523
1684
  });
1524
1685
 
1525
1686
  // src/graphql-schema.ts
@@ -1529,7 +1690,7 @@ __export(exports_graphql_schema, {
1529
1690
  });
1530
1691
  import { mkdir as mkdir2, readFile as readFile4 } from "node:fs/promises";
1531
1692
  import { createHash as createHash7 } from "node:crypto";
1532
- import { dirname as dirname7, resolve as resolve8 } from "node:path";
1693
+ import { dirname as dirname7, resolve as resolve9 } from "node:path";
1533
1694
  async function pullGraphqlSchema(options) {
1534
1695
  assertGraphqlOptions({ schema: options.output });
1535
1696
  const endpoint = new URL(options.url);
@@ -1569,7 +1730,7 @@ async function pullGraphqlSchema(options) {
1569
1730
  throw new Error("GraphQL schema export failed. Verify caller grants and enable introspection only in the intended development environment.");
1570
1731
  }
1571
1732
  const schema = lexicographicSortSchema(buildClientSchema2(data));
1572
- const path = resolve8(options.output);
1733
+ const path = resolve9(options.output);
1573
1734
  const content = path.endsWith(".json") ? JSON.stringify(introspectionFromSchema(schema), null, 2) + `
1574
1735
  ` : `# GENERATED BY supacloud-compiler graphql-schema. DO NOT EDIT.
1575
1736
  # Database First: change database declarations, apply migrations, then re-export for the intended role.
@@ -5624,12 +5785,12 @@ function resolveTypeSafety(options) {
5624
5785
  }
5625
5786
  // src/watch.ts
5626
5787
  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";
5788
+ import { dirname as dirname5, relative as relative7, resolve as resolve7, sep as sep7 } from "node:path";
5628
5789
 
5629
5790
  // src/incremental.ts
5630
5791
  import { createHash as createHash6 } from "node:crypto";
5631
5792
  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";
5793
+ import { isAbsolute as isAbsolute2, relative as relative6, resolve as resolve6, sep as sep6 } from "node:path";
5633
5794
  init_graphql_inputs();
5634
5795
  function createDependencyGraphCache() {
5635
5796
  return {
@@ -5704,11 +5865,11 @@ function createIncrementalCompiler() {
5704
5865
  };
5705
5866
  }
5706
5867
  async function updateSnapshot(previous, options, changedPaths) {
5707
- const rootDir = resolve5(options.rootDir);
5708
- const outDir = resolve5(options.outDir);
5868
+ const rootDir = resolve6(options.rootDir);
5869
+ const outDir = resolve6(options.outDir);
5709
5870
  const files = { ...previous.files };
5710
5871
  for (const changedPath of changedPaths) {
5711
- const absolutePath = isAbsolute2(changedPath) ? resolve5(changedPath) : resolve5(rootDir, changedPath);
5872
+ const absolutePath = isAbsolute2(changedPath) ? resolve6(changedPath) : resolve6(rootDir, changedPath);
5712
5873
  const relativeChangedPath = relative6(rootDir, absolutePath);
5713
5874
  if (relativeChangedPath === ".." || relativeChangedPath.startsWith(`..${sep6}`))
5714
5875
  continue;
@@ -5726,8 +5887,8 @@ async function updateSnapshot(previous, options, changedPaths) {
5726
5887
  return { files, optionsKey: optionsKeyOf(options) };
5727
5888
  }
5728
5889
  async function createSnapshot(options) {
5729
- const rootDir = resolve5(options.rootDir);
5730
- const outDir = resolve5(options.outDir);
5890
+ const rootDir = resolve6(options.rootDir);
5891
+ const outDir = resolve6(options.outDir);
5731
5892
  const paths = await listSourceFiles(rootDir, outDir);
5732
5893
  const files = {};
5733
5894
  for (const path of paths) {
@@ -5746,8 +5907,8 @@ async function createSnapshot(options) {
5746
5907
  }
5747
5908
  function optionsKeyOf(options) {
5748
5909
  return JSON.stringify({
5749
- rootDir: resolve5(options.rootDir),
5750
- outDir: resolve5(options.outDir),
5910
+ rootDir: resolve6(options.rootDir),
5911
+ outDir: resolve6(options.outDir),
5751
5912
  include: options.include,
5752
5913
  strict: options.strict,
5753
5914
  writeOnError: options.writeOnError,
@@ -5769,7 +5930,7 @@ async function listSourceFiles(rootDir, outDir) {
5769
5930
  const result = [];
5770
5931
  const visit = async (directory) => {
5771
5932
  for (const entry of await readdir(directory, { withFileTypes: true })) {
5772
- const path = resolve5(directory, entry.name);
5933
+ const path = resolve6(directory, entry.name);
5773
5934
  if (entry.isDirectory()) {
5774
5935
  if (entry.name === "node_modules" || entry.name === ".git" || path === outDir)
5775
5936
  continue;
@@ -5893,8 +6054,8 @@ function findAffectedModules(previous, current, changedFiles) {
5893
6054
  // src/watch.ts
5894
6055
  var DEFAULT_DEBOUNCE_MS = 100;
5895
6056
  function watchProject(options) {
5896
- const rootDir = resolve6(options.rootDir);
5897
- const outDir = resolve6(options.outDir);
6057
+ const rootDir = resolve7(options.rootDir);
6058
+ const outDir = resolve7(options.outDir);
5898
6059
  const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
5899
6060
  let timer;
5900
6061
  let closed = false;
@@ -5903,7 +6064,7 @@ function watchProject(options) {
5903
6064
  const pendingPaths = new Set;
5904
6065
  let watcher;
5905
6066
  let schemaWatcher;
5906
- const schemaPath = options.graphql ? resolve6(rootDir, options.graphql.schema) : undefined;
6067
+ const schemaPath = options.graphql ? resolve7(rootDir, options.graphql.schema) : undefined;
5907
6068
  const incremental = createIncrementalCompiler();
5908
6069
  let initialEvent;
5909
6070
  let resolveReady = () => {
@@ -5980,7 +6141,7 @@ function watchProject(options) {
5980
6141
  watcher = watch(rootDir, { recursive: true }, (_eventType, filename) => {
5981
6142
  if (!filename)
5982
6143
  return schedule();
5983
- const changedPath = resolve6(rootDir, filename.toString());
6144
+ const changedPath = resolve7(rootDir, filename.toString());
5984
6145
  const relativePath = relative7(outDir, changedPath);
5985
6146
  if (!relativePath.startsWith("..") && relativePath !== "")
5986
6147
  return;
@@ -5993,7 +6154,7 @@ function watchProject(options) {
5993
6154
  while (!existsSync4(directory) && dirname5(directory) !== directory)
5994
6155
  directory = dirname5(directory);
5995
6156
  schemaWatcher = watch(directory, { recursive: true }, (_eventType, filename) => {
5996
- if (!filename || resolve6(directory, filename.toString()) === schemaPath)
6157
+ if (!filename || resolve7(directory, filename.toString()) === schemaPath)
5997
6158
  schedule(schemaPath);
5998
6159
  });
5999
6160
  }
@@ -6291,7 +6452,7 @@ init_generate();
6291
6452
  // src/config.ts
6292
6453
  init_graphql_options();
6293
6454
  import { existsSync as existsSync6 } from "node:fs";
6294
- import { join as join6, resolve as resolve7 } from "node:path";
6455
+ import { join as join6, resolve as resolve8 } from "node:path";
6295
6456
  import { pathToFileURL } from "node:url";
6296
6457
  var DEFAULT_SUPACLOUD_CONFIG = {
6297
6458
  graphql: false,
@@ -6308,6 +6469,7 @@ var DEFAULT_SUPACLOUD_CONFIG = {
6308
6469
  function defineSupacloudConfig(config = {}) {
6309
6470
  if (config.graphql !== undefined && config.graphql !== false)
6310
6471
  assertGraphqlOptions(config.graphql);
6472
+ validateGovernanceConfig(config);
6311
6473
  return {
6312
6474
  ...DEFAULT_SUPACLOUD_CONFIG,
6313
6475
  ...config,
@@ -6315,11 +6477,35 @@ function defineSupacloudConfig(config = {}) {
6315
6477
  graphql: config.graphql ?? DEFAULT_SUPACLOUD_CONFIG.graphql
6316
6478
  };
6317
6479
  }
6480
+ function validateGovernanceConfig(config) {
6481
+ const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
6482
+ const isStrings = (value) => Array.isArray(value) && Array.from(value).every((item) => typeof item === "string" && item.trim().length > 0);
6483
+ if (config.moduleBoundaries !== undefined) {
6484
+ if (!Array.isArray(config.moduleBoundaries))
6485
+ throw new Error("moduleBoundaries must be an array of module tag rules.");
6486
+ const rules = config.moduleBoundaries;
6487
+ for (const rule of rules) {
6488
+ 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"])) {
6489
+ throw new Error("moduleBoundaries rules require sourceTag and optional string arrays onlyDependOnLibsWithTags/bannedDependenciesWithTags.");
6490
+ }
6491
+ }
6492
+ }
6493
+ if (config.typeSafety !== undefined) {
6494
+ const rules = config.typeSafety;
6495
+ 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"])) {
6496
+ throw new Error("typeSafety accepts boolean scanProductionSource/noAnyInGenerated and a string array exclude.");
6497
+ }
6498
+ }
6499
+ for (const key of ["allowRouteCommandBindings", "disallowControllerDirectDb", "detectOrphanModules"]) {
6500
+ if (config[key] !== undefined && typeof config[key] !== "boolean")
6501
+ throw new Error(`${key} must be a boolean.`);
6502
+ }
6503
+ }
6318
6504
  function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
6319
6505
  const resolved = defineSupacloudConfig(config);
6320
6506
  return {
6321
- rootDir: resolve7(cwd, resolved.root ?? DEFAULT_SUPACLOUD_CONFIG.root),
6322
- outDir: resolve7(cwd, resolved.outDir ?? DEFAULT_SUPACLOUD_CONFIG.outDir),
6507
+ rootDir: resolve8(cwd, resolved.root ?? DEFAULT_SUPACLOUD_CONFIG.root),
6508
+ outDir: resolve8(cwd, resolved.outDir ?? DEFAULT_SUPACLOUD_CONFIG.outDir),
6323
6509
  include: resolved.include ?? [...DEFAULT_SUPACLOUD_CONFIG.include],
6324
6510
  strict: resolved.strict ?? DEFAULT_SUPACLOUD_CONFIG.strict,
6325
6511
  requireRouteContracts: resolved.requireRouteContracts ?? DEFAULT_SUPACLOUD_CONFIG.requireRouteContracts,
@@ -6327,10 +6513,15 @@ function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
6327
6513
  generatePermissions: resolved.generatePermissions ?? DEFAULT_SUPACLOUD_CONFIG.generatePermissions,
6328
6514
  moduleBoundaryPreset: resolved.moduleBoundaryPreset ?? DEFAULT_SUPACLOUD_CONFIG.moduleBoundaryPreset,
6329
6515
  commandCapabilities: resolved.commandCapabilities,
6516
+ ...resolved.moduleBoundaries ? { moduleBoundaries: resolved.moduleBoundaries } : {},
6517
+ ...resolved.typeSafety ? { typeSafety: resolved.typeSafety } : {},
6518
+ ...resolved.allowRouteCommandBindings === undefined ? {} : { allowRouteCommandBindings: resolved.allowRouteCommandBindings },
6519
+ ...resolved.disallowControllerDirectDb === undefined ? {} : { disallowControllerDirectDb: resolved.disallowControllerDirectDb },
6520
+ ...resolved.detectOrphanModules === undefined ? {} : { detectOrphanModules: resolved.detectOrphanModules },
6330
6521
  treeShakeUnusedProviders: resolved.treeShakeUnusedProviders ?? DEFAULT_SUPACLOUD_CONFIG.treeShakeUnusedProviders,
6331
6522
  graphql: resolved.graphql ? {
6332
6523
  ...resolved.graphql,
6333
- schema: resolve7(cwd, resolved.graphql.schema)
6524
+ schema: resolve8(cwd, resolved.graphql.schema)
6334
6525
  } : undefined
6335
6526
  };
6336
6527
  }
@@ -6348,20 +6539,7 @@ async function loadSupacloudConfig(cwd = process.cwd()) {
6348
6539
  return defineSupacloudConfig(imported.default ?? {});
6349
6540
  }
6350
6541
  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
- };
6542
+ return resolveSupacloudConfig(config, cwd);
6365
6543
  }
6366
6544
 
6367
6545
  // 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.14.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",