@supacloud/compiler 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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 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?.();
@@ -1225,7 +1225,7 @@ export function createGraphqlClient(options: GraphqlClientOptions) {
1225
1225
  method: "POST",
1226
1226
  headers,
1227
1227
  body: JSON.stringify({ query, variables }),
1228
- signal: request?.signal,
1228
+ ...(request?.signal ? { signal: request.signal } : {}),
1229
1229
  redirect: "error",
1230
1230
  });
1231
1231
  if (!response.ok) {
@@ -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, {
@@ -1349,7 +1508,6 @@ import {
1349
1508
  validateSchema
1350
1509
  } from "graphql";
1351
1510
  import { codegen } from "@graphql-codegen/core";
1352
- import * as typescript from "@graphql-codegen/typescript";
1353
1511
  import * as operations from "@graphql-codegen/typescript-operations";
1354
1512
  async function renderGraphql(options) {
1355
1513
  const result = { diagnostics: [], files: {} };
@@ -1455,9 +1613,8 @@ async function renderGraphql(options) {
1455
1613
  try {
1456
1614
  const config = {
1457
1615
  useTypeImports: true,
1458
- skipTypename: true,
1459
- enumsAsTypes: true,
1460
- onlyOperationTypes: true,
1616
+ nonOptionalTypename: false,
1617
+ enumType: "string-literal",
1461
1618
  namingConvention: "keep",
1462
1619
  dedupeOperationSuffix: false,
1463
1620
  omitOperationSuffix: false,
@@ -1470,21 +1627,21 @@ async function renderGraphql(options) {
1470
1627
  schemaAst: schema,
1471
1628
  documents,
1472
1629
  config,
1473
- plugins: [{ typescript: {} }, { operations: {} }],
1474
- pluginMap: { typescript, operations }
1630
+ plugins: [{ operations: {} }],
1631
+ pluginMap: { operations }
1475
1632
  });
1476
1633
  const separated = separateOperations(combined);
1477
1634
  const methods = Object.entries(separated).sort(([a], [b]) => a.localeCompare(b)).map(([name, document]) => {
1478
1635
  const operation = document.definitions.find((node) => node.kind === Kind.OPERATION_DEFINITION);
1479
- if (operation.kind !== Kind.OPERATION_DEFINITION)
1636
+ if (!operation || operation.kind !== Kind.OPERATION_DEFINITION)
1480
1637
  throw new Error("Missing query operation");
1481
1638
  const required = operation.variableDefinitions?.some((variable) => variable.type.kind === Kind.NON_NULL_TYPE && !variable.defaultValue);
1482
- return ` ${JSON.stringify(name)}(variables${required ? "" : "?"}: ${name}QueryVariables, options?: C): Promise<${name}Query> {
1483
- 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));
1484
1641
  }`;
1485
1642
  });
1486
1643
  const facade = `
1487
- 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>;
1488
1645
  export function getSdk<C>(requester: Requester<C>) {
1489
1646
  return {
1490
1647
  ${methods.join(`,
@@ -1492,8 +1649,9 @@ ${methods.join(`,
1492
1649
  };
1493
1650
  }
1494
1651
  `;
1652
+ const validators = renderGraphqlValidators(generated, queryEntries.map((entry) => entry.name));
1495
1653
  result.files["graphql.ts"] = `// GENERATED BY @supacloud/compiler. DO NOT EDIT.
1496
- ` + generated + facade + GRAPHQL_CLIENT_SOURCE;
1654
+ ` + generated + validators + facade + GRAPHQL_CLIENT_SOURCE;
1497
1655
  if (options.graphql.typedDocuments) {
1498
1656
  const typedDocuments = await import("@graphql-codegen/typed-document-node");
1499
1657
  result.files["graphql.documents.ts"] = `// GENERATED BY @supacloud/compiler. DO NOT EDIT.
@@ -1503,8 +1661,8 @@ ${methods.join(`,
1503
1661
  schemaAst: schema,
1504
1662
  documents,
1505
1663
  config,
1506
- plugins: [{ typescript: {} }, { operations: {} }, { typedDocuments: {} }],
1507
- pluginMap: { typescript, operations, typedDocuments }
1664
+ plugins: [{ operations: {} }, { typedDocuments: {} }],
1665
+ pluginMap: { operations, typedDocuments }
1508
1666
  });
1509
1667
  }
1510
1668
  result.files["graphql.manifest.json"] = JSON.stringify({
@@ -1522,6 +1680,7 @@ ${methods.join(`,
1522
1680
  var init_graphql = __esm(() => {
1523
1681
  init_graphql_inputs();
1524
1682
  init_graphql_options();
1683
+ init_graphql_runtime();
1525
1684
  });
1526
1685
 
1527
1686
  // src/graphql-schema.ts
@@ -1531,7 +1690,7 @@ __export(exports_graphql_schema, {
1531
1690
  });
1532
1691
  import { mkdir as mkdir2, readFile as readFile4 } from "node:fs/promises";
1533
1692
  import { createHash as createHash7 } from "node:crypto";
1534
- import { dirname as dirname7, resolve as resolve8 } from "node:path";
1693
+ import { dirname as dirname7, resolve as resolve9 } from "node:path";
1535
1694
  async function pullGraphqlSchema(options) {
1536
1695
  assertGraphqlOptions({ schema: options.output });
1537
1696
  const endpoint = new URL(options.url);
@@ -1571,7 +1730,7 @@ async function pullGraphqlSchema(options) {
1571
1730
  throw new Error("GraphQL schema export failed. Verify caller grants and enable introspection only in the intended development environment.");
1572
1731
  }
1573
1732
  const schema = lexicographicSortSchema(buildClientSchema2(data));
1574
- const path = resolve8(options.output);
1733
+ const path = resolve9(options.output);
1575
1734
  const content = path.endsWith(".json") ? JSON.stringify(introspectionFromSchema(schema), null, 2) + `
1576
1735
  ` : `# GENERATED BY supacloud-compiler graphql-schema. DO NOT EDIT.
1577
1736
  # Database First: change database declarations, apply migrations, then re-export for the intended role.
@@ -5626,12 +5785,12 @@ function resolveTypeSafety(options) {
5626
5785
  }
5627
5786
  // src/watch.ts
5628
5787
  import { existsSync as existsSync4, watch } from "node:fs";
5629
- 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";
5630
5789
 
5631
5790
  // src/incremental.ts
5632
5791
  import { createHash as createHash6 } from "node:crypto";
5633
5792
  import { access as access2, readdir, readFile as readFile3 } from "node:fs/promises";
5634
- 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";
5635
5794
  init_graphql_inputs();
5636
5795
  function createDependencyGraphCache() {
5637
5796
  return {
@@ -5706,11 +5865,11 @@ function createIncrementalCompiler() {
5706
5865
  };
5707
5866
  }
5708
5867
  async function updateSnapshot(previous, options, changedPaths) {
5709
- const rootDir = resolve5(options.rootDir);
5710
- const outDir = resolve5(options.outDir);
5868
+ const rootDir = resolve6(options.rootDir);
5869
+ const outDir = resolve6(options.outDir);
5711
5870
  const files = { ...previous.files };
5712
5871
  for (const changedPath of changedPaths) {
5713
- const absolutePath = isAbsolute2(changedPath) ? resolve5(changedPath) : resolve5(rootDir, changedPath);
5872
+ const absolutePath = isAbsolute2(changedPath) ? resolve6(changedPath) : resolve6(rootDir, changedPath);
5714
5873
  const relativeChangedPath = relative6(rootDir, absolutePath);
5715
5874
  if (relativeChangedPath === ".." || relativeChangedPath.startsWith(`..${sep6}`))
5716
5875
  continue;
@@ -5728,8 +5887,8 @@ async function updateSnapshot(previous, options, changedPaths) {
5728
5887
  return { files, optionsKey: optionsKeyOf(options) };
5729
5888
  }
5730
5889
  async function createSnapshot(options) {
5731
- const rootDir = resolve5(options.rootDir);
5732
- const outDir = resolve5(options.outDir);
5890
+ const rootDir = resolve6(options.rootDir);
5891
+ const outDir = resolve6(options.outDir);
5733
5892
  const paths = await listSourceFiles(rootDir, outDir);
5734
5893
  const files = {};
5735
5894
  for (const path of paths) {
@@ -5748,8 +5907,8 @@ async function createSnapshot(options) {
5748
5907
  }
5749
5908
  function optionsKeyOf(options) {
5750
5909
  return JSON.stringify({
5751
- rootDir: resolve5(options.rootDir),
5752
- outDir: resolve5(options.outDir),
5910
+ rootDir: resolve6(options.rootDir),
5911
+ outDir: resolve6(options.outDir),
5753
5912
  include: options.include,
5754
5913
  strict: options.strict,
5755
5914
  writeOnError: options.writeOnError,
@@ -5771,7 +5930,7 @@ async function listSourceFiles(rootDir, outDir) {
5771
5930
  const result = [];
5772
5931
  const visit = async (directory) => {
5773
5932
  for (const entry of await readdir(directory, { withFileTypes: true })) {
5774
- const path = resolve5(directory, entry.name);
5933
+ const path = resolve6(directory, entry.name);
5775
5934
  if (entry.isDirectory()) {
5776
5935
  if (entry.name === "node_modules" || entry.name === ".git" || path === outDir)
5777
5936
  continue;
@@ -5895,8 +6054,8 @@ function findAffectedModules(previous, current, changedFiles) {
5895
6054
  // src/watch.ts
5896
6055
  var DEFAULT_DEBOUNCE_MS = 100;
5897
6056
  function watchProject(options) {
5898
- const rootDir = resolve6(options.rootDir);
5899
- const outDir = resolve6(options.outDir);
6057
+ const rootDir = resolve7(options.rootDir);
6058
+ const outDir = resolve7(options.outDir);
5900
6059
  const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
5901
6060
  let timer;
5902
6061
  let closed = false;
@@ -5905,7 +6064,7 @@ function watchProject(options) {
5905
6064
  const pendingPaths = new Set;
5906
6065
  let watcher;
5907
6066
  let schemaWatcher;
5908
- const schemaPath = options.graphql ? resolve6(rootDir, options.graphql.schema) : undefined;
6067
+ const schemaPath = options.graphql ? resolve7(rootDir, options.graphql.schema) : undefined;
5909
6068
  const incremental = createIncrementalCompiler();
5910
6069
  let initialEvent;
5911
6070
  let resolveReady = () => {
@@ -5982,7 +6141,7 @@ function watchProject(options) {
5982
6141
  watcher = watch(rootDir, { recursive: true }, (_eventType, filename) => {
5983
6142
  if (!filename)
5984
6143
  return schedule();
5985
- const changedPath = resolve6(rootDir, filename.toString());
6144
+ const changedPath = resolve7(rootDir, filename.toString());
5986
6145
  const relativePath = relative7(outDir, changedPath);
5987
6146
  if (!relativePath.startsWith("..") && relativePath !== "")
5988
6147
  return;
@@ -5995,7 +6154,7 @@ function watchProject(options) {
5995
6154
  while (!existsSync4(directory) && dirname5(directory) !== directory)
5996
6155
  directory = dirname5(directory);
5997
6156
  schemaWatcher = watch(directory, { recursive: true }, (_eventType, filename) => {
5998
- if (!filename || resolve6(directory, filename.toString()) === schemaPath)
6157
+ if (!filename || resolve7(directory, filename.toString()) === schemaPath)
5999
6158
  schedule(schemaPath);
6000
6159
  });
6001
6160
  }
@@ -6293,7 +6452,7 @@ init_generate();
6293
6452
  // src/config.ts
6294
6453
  init_graphql_options();
6295
6454
  import { existsSync as existsSync6 } from "node:fs";
6296
- import { join as join6, resolve as resolve7 } from "node:path";
6455
+ import { join as join6, resolve as resolve8 } from "node:path";
6297
6456
  import { pathToFileURL } from "node:url";
6298
6457
  var DEFAULT_SUPACLOUD_CONFIG = {
6299
6458
  graphql: false,
@@ -6310,6 +6469,7 @@ var DEFAULT_SUPACLOUD_CONFIG = {
6310
6469
  function defineSupacloudConfig(config = {}) {
6311
6470
  if (config.graphql !== undefined && config.graphql !== false)
6312
6471
  assertGraphqlOptions(config.graphql);
6472
+ validateGovernanceConfig(config);
6313
6473
  return {
6314
6474
  ...DEFAULT_SUPACLOUD_CONFIG,
6315
6475
  ...config,
@@ -6317,11 +6477,35 @@ function defineSupacloudConfig(config = {}) {
6317
6477
  graphql: config.graphql ?? DEFAULT_SUPACLOUD_CONFIG.graphql
6318
6478
  };
6319
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
+ }
6320
6504
  function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
6321
6505
  const resolved = defineSupacloudConfig(config);
6322
6506
  return {
6323
- rootDir: resolve7(cwd, resolved.root ?? DEFAULT_SUPACLOUD_CONFIG.root),
6324
- 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),
6325
6509
  include: resolved.include ?? [...DEFAULT_SUPACLOUD_CONFIG.include],
6326
6510
  strict: resolved.strict ?? DEFAULT_SUPACLOUD_CONFIG.strict,
6327
6511
  requireRouteContracts: resolved.requireRouteContracts ?? DEFAULT_SUPACLOUD_CONFIG.requireRouteContracts,
@@ -6329,10 +6513,15 @@ function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
6329
6513
  generatePermissions: resolved.generatePermissions ?? DEFAULT_SUPACLOUD_CONFIG.generatePermissions,
6330
6514
  moduleBoundaryPreset: resolved.moduleBoundaryPreset ?? DEFAULT_SUPACLOUD_CONFIG.moduleBoundaryPreset,
6331
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 },
6332
6521
  treeShakeUnusedProviders: resolved.treeShakeUnusedProviders ?? DEFAULT_SUPACLOUD_CONFIG.treeShakeUnusedProviders,
6333
6522
  graphql: resolved.graphql ? {
6334
6523
  ...resolved.graphql,
6335
- schema: resolve7(cwd, resolved.graphql.schema)
6524
+ schema: resolve8(cwd, resolved.graphql.schema)
6336
6525
  } : undefined
6337
6526
  };
6338
6527
  }
@@ -6350,20 +6539,7 @@ async function loadSupacloudConfig(cwd = process.cwd()) {
6350
6539
  return defineSupacloudConfig(imported.default ?? {});
6351
6540
  }
6352
6541
  function compileOptionsFromConfig(config, cwd = process.cwd()) {
6353
- const resolved = resolveSupacloudConfig(config, cwd);
6354
- return {
6355
- rootDir: resolved.rootDir,
6356
- outDir: resolved.outDir,
6357
- include: resolved.include,
6358
- strict: resolved.strict,
6359
- requireRouteContracts: resolved.requireRouteContracts,
6360
- generateClient: resolved.generateClient,
6361
- generatePermissions: resolved.generatePermissions,
6362
- moduleBoundaryPreset: resolved.moduleBoundaryPreset,
6363
- commandCapabilities: resolved.commandCapabilities,
6364
- treeShakeUnusedProviders: resolved.treeShakeUnusedProviders,
6365
- graphql: resolved.graphql
6366
- };
6542
+ return resolveSupacloudConfig(config, cwd);
6367
6543
  }
6368
6544
 
6369
6545
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/compiler",
3
- "version": "0.13.0",
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",
@@ -25,8 +25,9 @@
25
25
  "build:js": "bun build src/index.ts src/cli.ts --outdir dist --target node --external @typescript/typescript6 --external graphql --external '@graphql-codegen/*'",
26
26
  "build:types": "tsc -p tsconfig.json --emitDeclarationOnly",
27
27
  "clean": "rm -rf dist",
28
- "prepublishOnly": "bun run build",
28
+ "prepublishOnly": "bun run build && bun run test:package",
29
29
  "test": "bun test",
30
+ "test:package": "SUPACLOUD_COMPILER_TEST_CLI=\"$PWD/dist/cli.js\" bun test src/graphql-package.test.ts",
30
31
  "benchmark": "bun run src/benchmark.ts",
31
32
  "typecheck": "tsc -p tsconfig.json --noEmit",
32
33
  "typecheck:test": "tsc -p tsconfig.test.json --noEmit"
@@ -47,7 +48,6 @@
47
48
  "dependencies": {
48
49
  "@graphql-codegen/core": "^6.2.0",
49
50
  "@graphql-codegen/typed-document-node": "^7.1.0",
50
- "@graphql-codegen/typescript": "^6.1.0",
51
51
  "@graphql-codegen/typescript-operations": "^6.1.6",
52
52
  "@graphql-typed-document-node/core": "^3.2.0",
53
53
  "@typescript/typescript6": "^6.0.2",