From 7c234947e17361204f6e7561bf73e352ba94a032 Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Fri, 17 Jul 2026 12:12:37 +0800 Subject: [PATCH] feat(portal-shell): add graphql-codegen configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 文件后启用 --- apps/portal-shell/.gitignore | 2 + apps/portal-shell/codegen.yml | 30 ++++ apps/portal-shell/package.json | 9 +- apps/portal-shell/scripts/normalize-schema.ts | 153 ++++++++++++++++++ .../src/lib/api/operations/.gitkeep | 0 pnpm-lock.yaml | 57 ++++++- 6 files changed, 248 insertions(+), 3 deletions(-) create mode 100644 apps/portal-shell/.gitignore create mode 100644 apps/portal-shell/codegen.yml create mode 100644 apps/portal-shell/scripts/normalize-schema.ts create mode 100644 apps/portal-shell/src/lib/api/operations/.gitkeep diff --git a/apps/portal-shell/.gitignore b/apps/portal-shell/.gitignore new file mode 100644 index 0000000..f1485fa --- /dev/null +++ b/apps/portal-shell/.gitignore @@ -0,0 +1,2 @@ +# graphql-codegen 产物(构建时生成) +src/lib/api/__generated__/ diff --git a/apps/portal-shell/codegen.yml b/apps/portal-shell/codegen.yml new file mode 100644 index 0000000..45db720 --- /dev/null +++ b/apps/portal-shell/codegen.yml @@ -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 diff --git a/apps/portal-shell/package.json b/apps/portal-shell/package.json index 32ab8b0..6802428 100644 --- a/apps/portal-shell/package.json +++ b/apps/portal-shell/package.json @@ -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" } diff --git a/apps/portal-shell/scripts/normalize-schema.ts b/apps/portal-shell/scripts/normalize-schema.ts new file mode 100644 index 0000000..03136fd --- /dev/null +++ b/apps/portal-shell/scripts/normalize-schema.ts @@ -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(); diff --git a/apps/portal-shell/src/lib/api/operations/.gitkeep b/apps/portal-shell/src/lib/api/operations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 606f191..11a9a3d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -336,6 +336,18 @@ importers: specifier: ^4.5.0 version: 4.5.0(@types/react@18.3.31)(react@18.3.0) devDependencies: + '@graphql-codegen/cli': + specifier: ^5.0.0 + version: 5.0.0(@types/node@22.20.1)(encoding@0.1.13)(graphql@16.14.2) + '@graphql-codegen/typescript': + specifier: ^4.0.0 + version: 4.1.1(graphql@16.14.2) + '@graphql-codegen/typescript-document-nodes': + specifier: ^4.0.0 + version: 4.0.0(graphql@16.14.2) + '@graphql-codegen/typescript-operations': + specifier: ^4.0.0 + version: 4.3.1(graphql@16.14.2) '@testing-library/jest-dom': specifier: ^6.4.0 version: 6.9.1 @@ -372,6 +384,9 @@ importers: tailwindcss: specifier: ^3.4.0 version: 3.4.19(tsx@4.23.1)(yaml@2.9.0) + tsx: + specifier: ^4.0.0 + version: 4.23.1 typescript: specifier: ^5.6.0 version: 5.6.2 @@ -401,7 +416,7 @@ importers: version: 8.8.70(@rspack/core@2.1.3(@module-federation/runtime-tools@2.7.0)(@swc/helpers@0.5.23))(next@14.2.35(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0))(react-dom@18.3.0(react@18.3.0))(react@18.3.0)(styled-jsx@5.1.1(react@18.3.0))(typescript@5.6.2)(webpack@5.92.1(postcss@8.5.18)) '@sentry/nextjs': specifier: ^8.30.0 - version: 8.55.2(@opentelemetry/api-logs@0.46.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.57.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(encoding@0.1.13)(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0))(react@18.3.0)(webpack@5.92.1(postcss@8.5.18)) + version: 8.55.2(@opentelemetry/api-logs@0.46.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.57.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(encoding@0.1.13)(next@14.2.35(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0))(react@18.3.0)(webpack@5.92.1(postcss@8.5.18)) '@tanstack/react-query': specifier: ^5.59.0 version: 5.101.2(react@18.3.0) @@ -629,6 +644,9 @@ importers: packages/hooks: dependencies: + '@edu/shared-ts': + specifier: workspace:* + version: link:../shared-ts '@edu/ui-components': specifier: workspace:* version: link:../ui-components @@ -754,6 +772,9 @@ importers: services/classes: dependencies: + '@edu/shared-ts': + specifier: workspace:* + version: link:../../packages/shared-ts '@nestjs/common': specifier: ^10.4.0 version: 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -2771,6 +2792,11 @@ packages: peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + '@graphql-codegen/typescript-document-nodes@4.0.0': + resolution: {integrity: sha512-n9F10ScG2ZErjUk2GHTmBOL7OG+QPnPxLwzquQDGA5SuOo7ksyAvyEbKO+Ndk1bdK8+WwUe6IywMJM/zhOhtOA==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + '@graphql-codegen/typescript-operations@4.3.1': resolution: {integrity: sha512-yW5Iia6IK1VKiPm3oeukYMQN5pEBLwRlG8ZzQA9beeLQ8PskKyz6mjar6U7dJ2hc8pv/qT4R8kcJOQ2RloniAQ==} engines: {node: '>=16'} @@ -2783,6 +2809,11 @@ packages: peerDependencies: graphql: ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + '@graphql-codegen/visitor-plugin-common@4.0.0': + resolution: {integrity: sha512-OFWr5tkrG4nCcE7AI9BSAwuA0VLP16uNCLssbmXpBa1rKR6b4mX+rJTQCoz47TFV5hii8yp8xaWfXVUcsNY39w==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + '@graphql-codegen/visitor-plugin-common@5.5.0': resolution: {integrity: sha512-FSkxe/o4qKbpK+ipIT/jxZLYH0+3+XdIrJWsKlCW9wwJMF9mEJLJtzZNcxHSjz7+Eny6SUElAT2dqZ5XByxkog==} engines: {node: '>=16'} @@ -12088,6 +12119,14 @@ snapshots: graphql: 16.14.2 tslib: 2.6.3 + '@graphql-codegen/typescript-document-nodes@4.0.0(graphql@16.14.2)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.0.1(graphql@16.14.2) + '@graphql-codegen/visitor-plugin-common': 4.0.0(graphql@16.14.2) + auto-bind: 4.0.0 + graphql: 16.14.2 + tslib: 2.5.3 + '@graphql-codegen/typescript-operations@4.3.1(graphql@16.14.2)': dependencies: '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.14.2) @@ -12106,6 +12145,20 @@ snapshots: graphql: 16.14.2 tslib: 2.6.3 + '@graphql-codegen/visitor-plugin-common@4.0.0(graphql@16.14.2)': + dependencies: + '@graphql-codegen/plugin-helpers': 5.0.1(graphql@16.14.2) + '@graphql-tools/optimize': 2.0.0(graphql@16.14.2) + '@graphql-tools/relay-operation-optimizer': 7.1.6(graphql@16.14.2) + '@graphql-tools/utils': 10.6.0(graphql@16.14.2) + auto-bind: 4.0.0 + change-case-all: 1.0.15 + dependency-graph: 0.11.0 + graphql: 16.14.2 + graphql-tag: 2.12.7(graphql@16.14.2) + parse-filepath: 1.0.2 + tslib: 2.5.3 + '@graphql-codegen/visitor-plugin-common@5.5.0(graphql@16.14.2)': dependencies: '@graphql-codegen/plugin-helpers': 5.1.0(graphql@16.14.2) @@ -15520,7 +15573,7 @@ snapshots: - supports-color - webpack - '@sentry/nextjs@8.55.2(@opentelemetry/api-logs@0.46.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.57.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(encoding@0.1.13)(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0))(react@18.3.0)(webpack@5.92.1(postcss@8.5.18))': + '@sentry/nextjs@8.55.2(@opentelemetry/api-logs@0.46.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.57.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(encoding@0.1.13)(next@14.2.35(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.0(react@18.3.0))(react@18.3.0))(react@18.3.0)(webpack@5.92.1(postcss@8.5.18))': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/semantic-conventions': 1.43.0