@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 +39 -2
- package/dist/cli.js +268 -91
- package/dist/config.d.ts +11 -1
- package/dist/graphql-client.d.ts +1 -1
- package/dist/graphql-runtime.d.ts +5 -0
- package/dist/index.js +223 -45
- package/package.json +1 -1
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
|
|
123
|
-
|
|
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
|
|
1216
|
+
return getSdk<GraphqlRequestOptions>(async (
|
|
1217
1217
|
query: string,
|
|
1218
|
-
variables?:
|
|
1218
|
+
variables?: unknown,
|
|
1219
1219
|
request?: GraphqlRequestOptions,
|
|
1220
|
-
): Promise<
|
|
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
|
-
//
|
|
1256
|
-
return envelope.data
|
|
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
|
|
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> =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
5785
|
-
const 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) ?
|
|
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 =
|
|
5807
|
-
const 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:
|
|
5827
|
-
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 =
|
|
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 =
|
|
5974
|
-
const 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 ?
|
|
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 =
|
|
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 ||
|
|
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
|
|
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:
|
|
6128
|
-
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:
|
|
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
|
-
|
|
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
|
|
6177
|
-
import * as
|
|
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 =
|
|
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,
|
|
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 && !
|
|
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 &&
|
|
6217
|
-
if (values.some(
|
|
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) =>
|
|
6396
|
+
content = values.some((value) => ts7.isIdentifier(value) && value.text === fix.symbol) ? withImport : replaceProperty(source, object, "imports", ts7.factory.createArrayLiteralExpression([
|
|
6220
6397
|
...values,
|
|
6221
|
-
|
|
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 && (!
|
|
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",
|
|
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) =>
|
|
6242
|
-
const parameter = unique(method.parameters.filter((candidate) =>
|
|
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 =
|
|
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) =>
|
|
6250
|
-
if (!
|
|
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 =
|
|
6453
|
+
const result = ts7.transpileModule(text, {
|
|
6277
6454
|
fileName: file,
|
|
6278
6455
|
reportDiagnostics: true,
|
|
6279
|
-
compilerOptions: { target:
|
|
6456
|
+
compilerOptions: { target: ts7.ScriptTarget.ESNext, experimentalDecorators: true }
|
|
6280
6457
|
});
|
|
6281
|
-
if (result.diagnostics?.some((item) => item.category ===
|
|
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
|
|
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
|
|
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) => !
|
|
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) =>
|
|
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 &&
|
|
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 =
|
|
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 =
|
|
6320
|
-
return source.text.slice(0, object.getStart(source)) +
|
|
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) =>
|
|
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((
|
|
6327
|
-
const argument =
|
|
6328
|
-
if (!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 (
|
|
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 (
|
|
6515
|
+
if (ts7.isVariableStatement(statement)) {
|
|
6339
6516
|
for (const declaration of statement.declarationList.declarations) {
|
|
6340
6517
|
const call = declaration.initializer;
|
|
6341
|
-
if (call &&
|
|
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 =
|
|
6352
|
-
const target =
|
|
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) =>
|
|
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 &&
|
|
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 =
|
|
6540
|
+
const updated = ts7.factory.updateNamedImports(bindings, [
|
|
6364
6541
|
...bindings.elements,
|
|
6365
|
-
|
|
6542
|
+
ts7.factory.createImportSpecifier(false, undefined, ts7.factory.createIdentifier(symbol))
|
|
6366
6543
|
]);
|
|
6367
|
-
return source.text.slice(0, bindings.getStart(source)) +
|
|
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 ?
|
|
6519
|
-
const resolvedOut = 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(
|
|
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
|
};
|
package/dist/graphql-client.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
1215
|
+
return getSdk<GraphqlRequestOptions>(async (
|
|
1216
1216
|
query: string,
|
|
1217
|
-
variables?:
|
|
1217
|
+
variables?: unknown,
|
|
1218
1218
|
request?: GraphqlRequestOptions,
|
|
1219
|
-
): Promise<
|
|
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
|
-
//
|
|
1255
|
-
return envelope.data
|
|
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
|
|
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> =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
5708
|
-
const 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) ?
|
|
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 =
|
|
5730
|
-
const 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:
|
|
5750
|
-
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 =
|
|
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 =
|
|
5897
|
-
const 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 ?
|
|
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 =
|
|
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 ||
|
|
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
|
|
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:
|
|
6322
|
-
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:
|
|
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
|
-
|
|
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.
|
|
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",
|