feat(portal-shell): add graphql-codegen configuration

M1 Task 2: 配置 graphql-codegen 与 federation schema 预处理

- codegen.yml: schema 从 combined-schema.graphql 读取(federation 已剥离)

- scripts/normalize-schema.ts: 把 7 个子图的 extend type Query 合并为 type Query

- package.json: 新增 codegen/codegen:watch scripts + 4 个 codegen deps + tsx

- .gitignore: 忽略 src/lib/api/__generated__/

- documents 配置暂注释,Task 3 创建 operations 文件后启用
This commit is contained in:
SpecialX
2026-07-17 12:12:37 +08:00
parent 989603e318
commit 7c234947e1
6 changed files with 248 additions and 3 deletions

2
apps/portal-shell/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
# graphql-codegen 产物(构建时生成)
src/lib/api/__generated__/

View File

@@ -0,0 +1,30 @@
# graphql-codegen configuration
#
# Schema source: combined-schema.graphql (generated by scripts/normalize-schema.ts
# from services subgraph SDL files, with federation `extend type Query` normalized).
#
# Subgraph list (7 with GraphQL): iam / config-service / core-edu / content /
# msg / data-ana / ai (classes has no GraphQL subgraph yet).
#
# Outputs:
# - __generated__/types.ts: all GraphQL schema types (always generated)
# - __generated__/operations.ts: TypedDocumentNode (enabled after Task 3 creates
# operations/*.graphql.ts files)
#
# Related: spec section 2.4
schema: src/lib/api/__generated__/combined-schema.graphql
# documents: src/lib/api/operations/**/*.graphql.ts
# (re-enabled in Task 3 after operations files exist)
generates:
src/lib/api/__generated__/types.ts:
plugins:
- typescript
config:
preResolveTypes: true
skipTypename: true
exportTypeKeyOnly: true
useTypeImports: true

View File

@@ -10,7 +10,9 @@
"lint:tokens": "eslint -c .eslintrc.tokens.js src",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest"
"test:watch": "vitest",
"codegen": "tsx scripts/normalize-schema.ts && graphql-codegen --config codegen.yml",
"codegen:watch": "graphql-codegen --config codegen.yml --watch"
},
"dependencies": {
"@apollo/client": "^3.11.0",
@@ -25,6 +27,10 @@
"zustand": "^4.5.0"
},
"devDependencies": {
"@graphql-codegen/cli": "^5.0.0",
"@graphql-codegen/typescript": "^4.0.0",
"@graphql-codegen/typescript-document-nodes": "^4.0.0",
"@graphql-codegen/typescript-operations": "^4.0.0",
"@testing-library/jest-dom": "^6.4.0",
"@testing-library/react": "^16.0.0",
"@types/node": "^22.0.0",
@@ -37,6 +43,7 @@
"jsdom": "^25.0.0",
"postcss": "^8.4.0",
"tailwindcss": "^3.4.0",
"tsx": "^4.0.0",
"typescript": "^5.6.0",
"vitest": "^2.0.0"
}

View File

@@ -0,0 +1,153 @@
// Normalize federation subgraph schemas for graphql-codegen.
//
// Problem: services schema.graphql files are federation subgraphs that use
// `extend type Query` (with no body) and multiple `extend type Query { ... }`
// blocks. Standard GraphQL parser used by codegen cannot parse these.
//
// Solution: read all 7 subgraph schema files, merge all `extend type Query`
// blocks into a single `type Query { ... }`, output to a combined SDL file.
//
// Usage: tsx scripts/normalize-schema.ts
// Output: src/lib/api/__generated__/combined-schema.graphql
//
// Related: spec section 2.4
import * as fs from "node:fs";
import * as path from "node:path";
interface SubgraphSchema {
name: string;
content: string;
}
const SCHEMA_FILES = [
{
name: "iam",
path: "../../services/iam/src/graphql/generated/schema.graphql",
},
{
name: "config-service",
path: "../../services/config-service/src/graphql/generated/schema.graphql",
},
{
name: "core-edu",
path: "../../services/core-edu/src/graphql/generated/schema.graphql",
},
{
name: "content",
path: "../../services/content/src/graphql/generated/schema.graphql",
},
{
name: "msg",
path: "../../services/msg/src/graphql/generated/schema.graphql",
},
{
name: "data-ana",
path: "../../services/data-ana/src/graphql/generated/schema.graphql",
},
{
name: "ai",
path: "../../services/ai/src/graphql/generated/schema.graphql",
},
];
function loadSchemas(): SubgraphSchema[] {
const baseDir = process.cwd();
return SCHEMA_FILES.map((f) => ({
name: f.name,
content: fs.readFileSync(path.resolve(baseDir, f.path), "utf8"),
}));
}
// Strip federation `extend type Query` syntax:
// - Remove standalone `extend type Query` (no body)
// - Convert `extend type Query { ... }` blocks to `type Query { ... }`
// (we collect fields and merge later)
function normalizeSchema(content: string): {
staticDefs: string;
queryFields: string[];
} {
const lines = content.split("\n");
const staticDefs: string[] = [];
const queryFields: string[] = [];
let i = 0;
while (i < lines.length) {
const line = lines[i];
// Match `extend type Query` (no body, single line)
if (/^\s*extend\s+type\s+Query\s*$/.test(line)) {
i++;
continue;
}
// Match `extend type Query {` block start
const blockStart = line.match(/^\s*extend\s+type\s+Query\s*\{/);
if (blockStart) {
// Collect fields until matching `}`
i++;
while (i < lines.length && !/^\s*\}\s*$/.test(lines[i])) {
const fieldLine = lines[i];
if (fieldLine.trim()) {
queryFields.push(fieldLine);
}
i++;
}
// Skip closing `}`
i++;
continue;
}
// Match `type Query {` block (already standard, but merge fields)
const stdBlockStart = line.match(/^\s*type\s+Query\s*\{/);
if (stdBlockStart) {
i++;
while (i < lines.length && !/^\s*\}\s*$/.test(lines[i])) {
const fieldLine = lines[i];
if (fieldLine.trim()) {
queryFields.push(fieldLine);
}
i++;
}
i++;
continue;
}
staticDefs.push(line);
i++;
}
return { staticDefs: staticDefs.join("\n"), queryFields };
}
function main(): void {
const schemas = loadSchemas();
const allStaticDefs: string[] = [];
const allQueryFields: string[] = [];
for (const s of schemas) {
const { staticDefs, queryFields } = normalizeSchema(s.content);
allStaticDefs.push(`# === ${s.name} subgraph ===`);
allStaticDefs.push(staticDefs.trim());
allQueryFields.push(...queryFields);
}
// Deduplicate query fields (in case multiple subgraphs define same field)
const uniqueQueryFields = Array.from(
new Set(allQueryFields.map((f) => f.trim())),
);
const combined = [
"# Combined normalized schema for graphql-codegen (federation stripped)",
"# DO NOT EDIT - generated by scripts/normalize-schema.ts",
"",
...allStaticDefs,
"",
"type Query {",
...uniqueQueryFields.map((f) => ` ${f}`),
"}",
"",
].join("\n");
const outDir = path.resolve(process.cwd(), "src/lib/api/__generated__");
fs.mkdirSync(outDir, { recursive: true });
const outPath = path.join(outDir, "combined-schema.graphql");
fs.writeFileSync(outPath, combined);
console.log(`Combined schema written to ${outPath}`);
console.log(` Query fields: ${uniqueQueryFields.length}`);
}
main();