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