From aaa5aa93b04306e880cfd1565720f11c076fd887 Mon Sep 17 00:00:00 2001 From: Amartuvshin Surenjav Date: Wed, 7 Oct 2026 06:07:57 +0800 Subject: [PATCH] Add schema-driven GraphQL codegen to generated plugins The API prints its subgraph SDL (typeDefs via buildSubgraphSchema, or resolver classes via NestFactory + GraphQLSchemaFactory on the NestJS stack) to api/generated/schema.graphql, and the UI's graphql-codegen client preset validates every gql() document against it and generates typed documents into ui/src/gql/. check/build/dev:ui run codegen first; oxlint restricts graphql-tag and @apollo/client gql imports in ui/. --- README.md | 1 + package.json | 2 +- src/generated.ts | 14 +++++++-- src/scaffold.ts | 7 +++++ src/stacks.ts | 13 +++++--- templates/api/nestjs/src/app.module.ts | 4 +-- templates/api/nestjs/src/print-schema.ts | 19 ++++++++++++ templates/api/nestjs/src/resolvers.ts | 5 +++ templates/api/platform/package.json | 1 + .../api/schema-print/src/print-schema.ts | 12 +++++++ templates/base/AGENTS.md | 11 +++++++ templates/base/README.md | 20 +++++++----- templates/base/_dockerignore | 2 ++ templates/base/_gitignore | 2 ++ templates/base/_oxlintrc.json | 14 +++++++++ templates/base/docs/erxes-integration.md | 11 +++++++ templates/ui/react/codegen.ts | 25 +++++++++++++++ templates/ui/react/package.json | 7 ++++- templates/ui/react/rspack.config.ts | 6 +++- templates/ui/react/src/graphql.ts | 20 ++++-------- templates/ui/react/src/pages/IndexPage.tsx | 4 +-- templates/ui/react/tsconfig.json | 5 +-- test/e2e.test.ts | 9 ++++-- test/scaffold.test.ts | 31 +++++++++++++++++++ 24 files changed, 205 insertions(+), 40 deletions(-) create mode 100644 templates/api/nestjs/src/print-schema.ts create mode 100644 templates/api/nestjs/src/resolvers.ts create mode 100644 templates/api/schema-print/src/print-schema.ts create mode 100644 templates/base/_oxlintrc.json create mode 100644 templates/ui/react/codegen.ts diff --git a/README.md b/README.md index 03acb79..a467606 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ api/ platform stack: erxes-api-shared startPlugin + tenant Mongoose or NestJS code-first federation (Node.js) ui/ React 18 Rspack Module Federation remote, prefixed Tailwind CSS — overview/settings pages plus a notification inbox widget + GraphQL types generated from the printed API schema (graphql-codegen) Dockerfile API-only image for the chosen package manager README.md, AGENTS.md, docs/erxes-integration.md ``` diff --git a/package.json b/package.json index d2183f6..bce7751 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@erxes/create-plugin", - "version": "0.1.1", + "version": "0.2.0", "description": "Scaffold a standalone erxes plugin repository (API + React UI remote)", "license": "MIT", "repository": { diff --git a/src/generated.ts b/src/generated.ts index 89587bc..b81fe0c 100644 --- a/src/generated.ts +++ b/src/generated.ts @@ -59,12 +59,18 @@ export const runtimesFor = (o: Pick): BackendStack["r const LINT_DEPENDENCIES = { oxfmt: "^0.71.0", oxlint: "^1.86.0" }; +/** Runs a script at the workspace root (yarn drops the extra `run`). */ +const rootRun = (pm: PackageManager, script: string) => + pm === "yarn" ? `yarn ${script}` : `${pm} run ${script}`; + export const createRootPackageJson = (o: ProjectOptions) => { const { packageManager: pm, names } = o; const api = `${names.name}_api`; const ui = `${names.name}_ui`; const apiScripts = runtimesFor(o)[runtimeFor(pm)]?.scripts ?? {}; + const codegen = rootRun(pm, "codegen"); const build = [ + codegen, ...("build" in apiScripts ? [workspaceRun(pm, api, "build")] : []), workspaceRun(pm, ui, "build"), ]; @@ -77,9 +83,13 @@ export const createRootPackageJson = (o: ProjectOptions) => { ...(pm === "pnpm" ? {} : { workspaces: ["api", "ui"] }), scripts: { "dev:api": workspaceRun(pm, api, "dev"), - "dev:ui": workspaceRun(pm, ui, "dev"), + "dev:ui": `${codegen} && ${workspaceRun(pm, ui, "dev")}`, + codegen: [ + workspaceRun(pm, api, "schema:print"), + workspaceRun(pm, ui, "codegen"), + ].join(" && "), build: build.join(" && "), - check: [workspaceRun(pm, api, "check"), workspaceRun(pm, ui, "check")].join(" && "), + check: [codegen, workspaceRun(pm, api, "check"), workspaceRun(pm, ui, "check")].join(" && "), lint: "oxlint", fmt: "oxfmt", }, diff --git a/src/scaffold.ts b/src/scaffold.ts index 1ad4606..2b59aa7 100644 --- a/src/scaffold.ts +++ b/src/scaffold.ts @@ -17,6 +17,7 @@ const DOTFILES: Record = { _gitignore: ".gitignore", _dockerignore: ".dockerignore", "_env.example": ".env.example", + "_oxlintrc.json": ".oxlintrc.json", }; /** The erxes shared libraries on npm, installed under the names erxes code imports. */ @@ -73,6 +74,12 @@ const tokensFor = ({ ? "api/src/status/status.resolver.ts" : "api/src/graphql/schema.ts", ], + [ + "__schemaPrint__", + backend === "nestjs" + ? "`api/src/print-schema.ts` builds the schema from `RESOLVERS` in `api/src/resolvers.ts`; register every resolver class there (AppModule reads the same list)" + : "`api/src/print-schema.ts` prints `typeDefs` from `api/src/graphql/schema.ts` to `api/generated/schema.graphql`", + ], ); return tokens; diff --git a/src/stacks.ts b/src/stacks.ts index 42a2bf2..bdb1506 100644 --- a/src/stacks.ts +++ b/src/stacks.ts @@ -43,6 +43,7 @@ const NODE_SETUP: RuntimeSetup = { build: "tsc -p tsconfig.build.json", start: "node --env-file-if-exists=../.env dist/main.js", check: "tsc --noEmit", + "schema:print": "tsx src/print-schema.ts", }, devDependencies: { "@types/node": "^22.20.4", tsx: "^4.23.15" }, types: ["node"], @@ -53,6 +54,7 @@ const BUN_SETUP: RuntimeSetup = { dev: "bun --env-file=../.env --watch src/main.ts", start: "bun --env-file=../.env src/main.ts", check: "tsc --noEmit", + "schema:print": "bun src/print-schema.ts", }, devDependencies: { "@types/bun": "^1.4.2" }, types: ["bun"], @@ -63,7 +65,7 @@ export const BACKENDS = { integration: "platform", label: "Express via erxes-api-shared", hint: "erxes-api-shared startPlugin + tenant-scoped Mongoose models (Node.js)", - templates: ["api/commonjs", "api/platform"], + templates: ["api/commonjs", "api/platform", "api/schema-print"], runtimes: { node: NODE_SETUP, }, @@ -72,7 +74,7 @@ export const BACKENDS = { integration: "standalone", label: "Express", hint: "Express 5 + Apollo Server federated subgraph", - templates: ["api/standalone", "api/standalone-schema", "api/express"], + templates: ["api/standalone", "api/standalone-schema", "api/express", "api/schema-print"], runtimes: { node: NODE_SETUP, bun: BUN_SETUP, @@ -82,7 +84,7 @@ export const BACKENDS = { integration: "standalone", label: "Fastify", hint: "Fastify + Apollo Server federated subgraph", - templates: ["api/standalone", "api/standalone-schema", "api/fastify"], + templates: ["api/standalone", "api/standalone-schema", "api/fastify", "api/schema-print"], runtimes: { node: NODE_SETUP, }, @@ -91,7 +93,7 @@ export const BACKENDS = { integration: "standalone", label: "Hono", hint: "Hono + GraphQL Yoga federated subgraph (Node.js or Bun)", - templates: ["api/standalone", "api/standalone-schema", "api/hono"], + templates: ["api/standalone", "api/standalone-schema", "api/hono", "api/schema-print"], runtimes: { node: NODE_SETUP, bun: BUN_SETUP, @@ -101,7 +103,7 @@ export const BACKENDS = { integration: "standalone", label: "Elysia", hint: "Elysia + GraphQL Yoga federated subgraph (Bun)", - templates: ["api/standalone", "api/standalone-schema", "api/elysia"], + templates: ["api/standalone", "api/standalone-schema", "api/elysia", "api/schema-print"], runtimes: { bun: BUN_SETUP, }, @@ -118,6 +120,7 @@ export const BACKENDS = { build: "nest build", start: "node --env-file-if-exists=../.env dist/main.js", check: "tsc --noEmit", + "schema:print": "nest build && node dist/print-schema.js", }, devDependencies: { "@nestjs/cli": "^12.0.8", "@types/node": "^22.20.4" }, types: ["node"], diff --git a/templates/api/nestjs/src/app.module.ts b/templates/api/nestjs/src/app.module.ts index 47d0892..301de46 100644 --- a/templates/api/nestjs/src/app.module.ts +++ b/templates/api/nestjs/src/app.module.ts @@ -4,7 +4,7 @@ import { GraphQLModule } from "@nestjs/graphql"; import { manifest } from "./config.ts"; import { createContext, type HeaderSource } from "./context.ts"; import { HealthController } from "./health.controller.ts"; -import { StatusResolver } from "./status/status.resolver.ts"; +import { RESOLVERS } from "./resolvers.ts"; @Module({ imports: [ @@ -17,6 +17,6 @@ import { StatusResolver } from "./status/status.resolver.ts"; }), ], controllers: [HealthController], - providers: [StatusResolver], + providers: RESOLVERS, }) export class AppModule {} diff --git a/templates/api/nestjs/src/print-schema.ts b/templates/api/nestjs/src/print-schema.ts new file mode 100644 index 0000000..101b8c7 --- /dev/null +++ b/templates/api/nestjs/src/print-schema.ts @@ -0,0 +1,19 @@ +import "reflect-metadata"; +import { NestFactory } from "@nestjs/core"; +import { GraphQLSchemaBuilderModule, GraphQLSchemaFactory } from "@nestjs/graphql"; +import { printSchema } from "graphql"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { RESOLVERS } from "./resolvers.ts"; + +// Builds the schema from the resolver classes without starting the HTTP +// server or joining the gateway; AppModule registers the same RESOLVERS list. +const main = async () => { + const app = await NestFactory.create(GraphQLSchemaBuilderModule, { logger: false }); + await app.init(); + const schema = await app.get(GraphQLSchemaFactory).create(RESOLVERS); + mkdirSync("generated", { recursive: true }); + writeFileSync("generated/schema.graphql", printSchema(schema)); + await app.close(); +}; + +main().catch((error) => { console.error(error); process.exit(1); }); diff --git a/templates/api/nestjs/src/resolvers.ts b/templates/api/nestjs/src/resolvers.ts new file mode 100644 index 0000000..7574ee7 --- /dev/null +++ b/templates/api/nestjs/src/resolvers.ts @@ -0,0 +1,5 @@ +import { StatusResolver } from "./status/status.resolver.ts"; + +// AppModule and print-schema.ts both read this list; register every new +// resolver class here so the printed SDL matches the running API. +export const RESOLVERS = [StatusResolver]; diff --git a/templates/api/platform/package.json b/templates/api/platform/package.json index 64b5ce1..ccaefd2 100644 --- a/templates/api/platform/package.json +++ b/templates/api/platform/package.json @@ -10,6 +10,7 @@ "zod": "^4.6.5" }, "devDependencies": { + "@apollo/subgraph": "^2.15.1", "@types/node": "^22.18.0", "typescript": "^5.9.3" } diff --git a/templates/api/schema-print/src/print-schema.ts b/templates/api/schema-print/src/print-schema.ts new file mode 100644 index 0000000..8a3ed14 --- /dev/null +++ b/templates/api/schema-print/src/print-schema.ts @@ -0,0 +1,12 @@ +import { buildSubgraphSchema } from "@apollo/subgraph"; +import { printSchema } from "graphql"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { typeDefs } from "./graphql/schema.ts"; + +// Prints the subgraph SDL that ui/ codegen validates its documents against. +// Runs from api/ as a package script; needs no Redis, Mongo or running server. +mkdirSync("generated", { recursive: true }); +writeFileSync( + "generated/schema.graphql", + printSchema(buildSubgraphSchema([{ typeDefs }])), +); diff --git a/templates/base/AGENTS.md b/templates/base/AGENTS.md index 8e70a78..988ef7f 100644 --- a/templates/base/AGENTS.md +++ b/templates/base/AGENTS.md @@ -10,6 +10,7 @@ is the single source of the plugin's identity and ports. - `__pm__ install` - `__pmRun__ dev:api` / `__pmRun__ dev:ui` +- `__pmRun__ codegen` - `__pmRun__ check`, `__pmRun__ lint`, `__pmRun__ build`, `__pmRun__ fmt` - `docker build --build-arg UI_ENTRY_URL= -t .` @@ -35,6 +36,7 @@ __apiContracts__ `ui/src/widgets/NotificationWidget.tsx` exports `NotificationWidget` for the `./notificationWidget` inbox expose. Keep the expose names in `ui/rspack.config.ts` in sync with `erxes.json`. +- __schemaPrint__. - `erxes-ui`, `ui-modules` and `erxes-api-shared` are the npm packages `@erxes/ui`, `@erxes/ui-modules` and `@erxes/api-shared`, installed under those import names with npm aliases; keep them on the versions your erxes @@ -59,3 +61,12 @@ __apiContracts__ security boundary. - Every query has loading, error and empty states; refetch or update the Apollo cache after each mutation. +- GraphQL documents are `gql()` from `~/gql` — generated into `ui/src/gql/` + by `codegen` from `api/generated/schema.graphql` — written as a function + call on a static string with no `${}` interpolation. +- Apollo hooks infer their types from the document: no `useQuery` or + `useMutation` generics and no handwritten response interfaces; derive + types from the generated `*Query` types. +- `api/generated` and `ui/src/gql` are gitignored; `check`, `build` and + `dev:ui` run `codegen` first. Run `__pmRun__ codegen` after changing a + document or the schema. diff --git a/templates/base/README.md b/templates/base/README.md index 8ecd1a1..4d71fa5 100644 --- a/templates/base/README.md +++ b/templates/base/README.md @@ -26,6 +26,9 @@ __pmRun__ dev:api # registers with the erxes gateway through Redis __pmRun__ dev:ui # serves http://localhost:__uiPort__/remoteEntry.js ``` +`check`, `build` and `dev:ui` run `codegen` first, so the generated GraphQL +types always match the printed API schema. + In the erxes checkout `.env`, add the plugin and restart the gateway and core-api so they pick it up: @@ -44,14 +47,15 @@ curl localhost:__apiPort__/graphql -H 'content-type: application/json' \ ## Scripts -| Script | What it does | -| ------------------- | ------------------------------ | -| `__pmRun__ dev:api` | API in watch mode | -| `__pmRun__ dev:ui` | UI remote dev server | -| `__pmRun__ build` | Production build of every part | -| `__pmRun__ check` | Type-check every part | -| `__pmRun__ lint` | oxlint | -| `__pmRun__ fmt` | oxfmt | +| Script | What it does | +| ------------------- | ----------------------------------------------- | +| `__pmRun__ dev:api` | API in watch mode | +| `__pmRun__ dev:ui` | UI remote dev server | +| `__pmRun__ codegen` | Print the API schema, generate UI GraphQL types | +| `__pmRun__ build` | Production build of every part | +| `__pmRun__ check` | Type-check every part | +| `__pmRun__ lint` | oxlint | +| `__pmRun__ fmt` | oxfmt | ## Deploy diff --git a/templates/base/_dockerignore b/templates/base/_dockerignore index 84a5efd..1fd7391 100644 --- a/templates/base/_dockerignore +++ b/templates/base/_dockerignore @@ -1,5 +1,7 @@ node_modules **/node_modules **/dist +**/generated +ui/src/gql **/.env .git diff --git a/templates/base/_gitignore b/templates/base/_gitignore index 9c97bbd..4c0c584 100644 --- a/templates/base/_gitignore +++ b/templates/base/_gitignore @@ -1,3 +1,5 @@ node_modules dist +api/generated +ui/src/gql .env diff --git a/templates/base/_oxlintrc.json b/templates/base/_oxlintrc.json new file mode 100644 index 0000000..40d4aa9 --- /dev/null +++ b/templates/base/_oxlintrc.json @@ -0,0 +1,14 @@ +{ + "ignorePatterns": ["ui/src/gql", "api/generated"], + "overrides": [ + { + "files": ["ui/**/*.ts", "ui/**/*.tsx"], + "rules": { + "no-restricted-imports": ["error", { "paths": [ + { "name": "graphql-tag", "message": "Use `import { gql } from \"~/gql\"` so codegen picks the document up." }, + { "name": "@apollo/client", "importNames": ["gql"], "message": "Use `import { gql } from \"~/gql\"` so codegen picks the document up." } + ] }] + } + } + ] +} diff --git a/templates/base/docs/erxes-integration.md b/templates/base/docs/erxes-integration.md index 94f9f4a..d14ade0 100644 --- a/templates/base/docs/erxes-integration.md +++ b/templates/base/docs/erxes-integration.md @@ -84,6 +84,17 @@ Notes: compare an aliased peer range. The peer is still linked to the `erxes-ui` this repo installs, so the warning is harmless. +## GraphQL types + +`__pmRun__ codegen` prints the API's subgraph SDL to +`api/generated/schema.graphql`, then `ui/codegen.ts` (graphql-codegen client +preset) validates every `gql()` document under `ui/src` against it and +generates typed documents into `ui/src/gql/` (gitignored, like +`api/generated`). Import `gql` from `~/gql` and let Apollo hooks infer their +types — drift between the SDL and the UI fails `check` instead of surfacing +as a runtime Apollo error. The same pattern backs erxes's own `operation` +plugin. + ## Styling core-ui ships Tailwind preflight and unprefixed utilities for its own code. diff --git a/templates/ui/react/codegen.ts b/templates/ui/react/codegen.ts new file mode 100644 index 0000000..12992fd --- /dev/null +++ b/templates/ui/react/codegen.ts @@ -0,0 +1,25 @@ +import type { CodegenConfig } from "@graphql-codegen/cli"; + +// Validates every document in src/ against the SDL the API printed to +// api/generated/schema.graphql and generates the result/variable types into +// src/gql/ (gitignored; `codegen` runs before check/build/dev). +const config: CodegenConfig = { + schema: "../api/generated/schema.graphql", + documents: ["src/**/*.{ts,tsx}", "!src/gql/**"], + generates: { + "src/gql/": { + preset: "client", + presetConfig: { fragmentMasking: false, gqlTagName: "gql" }, + config: { + scalars: { + Date: { input: "string | Date", output: "string" }, + JSON: "unknown", + }, + enumsAsTypes: true, + useTypeImports: true, + }, + }, + }, +}; + +export default config; diff --git a/templates/ui/react/package.json b/templates/ui/react/package.json index 12d8cb7..dd7a381 100644 --- a/templates/ui/react/package.json +++ b/templates/ui/react/package.json @@ -6,18 +6,23 @@ "scripts": { "dev": "rspack serve", "build": "rspack build", - "check": "tsc --noEmit" + "check": "tsc --noEmit", + "codegen": "graphql-codegen --config codegen.ts" }, "dependencies": { "@tabler/icons-react": "^3.48.0" }, "devDependencies": { "@apollo/client": "^3.14.1", + "@graphql-codegen/cli": "^7.4.3", + "@graphql-codegen/client-preset": "^6.2.0", + "@graphql-typed-document-node/core": "^3.2.0", "@module-federation/enhanced": "^2.9.2", "@rspack/cli": "^2.2.8", "@rspack/core": "^2.2.8", "@rspack/dev-server": "^2.2.1", "@tailwindcss/postcss": "^4.3.3", + "@types/node": "^22.20.4", "@types/react": "^18.3.31", "@types/react-dom": "^18.3.7", "css-loader": "^7.1.5", diff --git a/templates/ui/react/rspack.config.ts b/templates/ui/react/rspack.config.ts index 4237326..ba48e43 100644 --- a/templates/ui/react/rspack.config.ts +++ b/templates/ui/react/rspack.config.ts @@ -1,5 +1,6 @@ import { ModuleFederationPlugin } from "@module-federation/enhanced/rspack"; import { defineConfig } from "@rspack/cli"; +import { fileURLToPath } from "node:url"; import manifest from "../erxes.json" with { type: "json" }; const { name, remote, port } = manifest.ui; @@ -22,7 +23,10 @@ const hostShared = [ export default defineConfig({ entry: {}, output: { publicPath: "auto", uniqueName: remote, clean: true }, - resolve: { extensions: [".tsx", ".ts", ".js"] }, + resolve: { + extensions: [".tsx", ".ts", ".js"], + alias: { "~": fileURLToPath(new URL("./src", import.meta.url)) }, + }, module: { rules: [ { diff --git a/templates/ui/react/src/graphql.ts b/templates/ui/react/src/graphql.ts index baaf94f..351fd91 100644 --- a/templates/ui/react/src/graphql.ts +++ b/templates/ui/react/src/graphql.ts @@ -1,7 +1,9 @@ -import { gql } from "@apollo/client"; +import { gql } from "~/gql"; -// Operation names are global across erxes; keep the plugin prefix. -export const STATUS_QUERY = gql` +// Operation names are global across erxes; keep the plugin prefix. gql() is +// generated by codegen — write documents as static strings (no `${}`), and +// let Apollo hooks infer their types from the document. +export const STATUS_QUERY = gql(` query __camel__Status { __camel__Status { plugin @@ -11,14 +13,4 @@ export const STATUS_QUERY = gql` userEmail } } -`; - -export type StatusQuery = { - __camel__Status: { - plugin: string; - version: string; - subdomain: string; - userId: string | null; - userEmail: string | null; - }; -}; +`); diff --git a/templates/ui/react/src/pages/IndexPage.tsx b/templates/ui/react/src/pages/IndexPage.tsx index 1f0ff96..2e9e410 100644 --- a/templates/ui/react/src/pages/IndexPage.tsx +++ b/templates/ui/react/src/pages/IndexPage.tsx @@ -24,7 +24,7 @@ import { import { PageHeader } from "ui-modules"; import { Link } from "react-router"; import manifest from "../../../erxes.json" with { type: "json" }; -import { STATUS_QUERY, type StatusQuery } from "../graphql"; +import { STATUS_QUERY } from "../graphql"; const { path } = manifest.ui; @@ -47,7 +47,7 @@ const StatusRow = ({ label, children }: { label: string; children: ReactNode }) ); const ConnectionCard = () => { - const { data, loading, error, refetch } = useQuery(STATUS_QUERY); + const { data, loading, error, refetch } = useQuery(STATUS_QUERY); const status = data?.__camel__Status; const pending = ( diff --git a/templates/ui/react/tsconfig.json b/templates/ui/react/tsconfig.json index c383a2d..3989df2 100644 --- a/templates/ui/react/tsconfig.json +++ b/templates/ui/react/tsconfig.json @@ -11,7 +11,8 @@ "jsx": "react-jsx", "lib": ["ES2022", "DOM", "DOM.Iterable"], "resolveJsonModule": true, - "types": [] + "types": ["node"], + "paths": { "~/*": ["./src/*"] } }, - "include": ["src", "rspack.config.ts"] + "include": ["src", "rspack.config.ts", "codegen.ts"] } diff --git a/test/e2e.test.ts b/test/e2e.test.ts index 2867314..01fbd75 100644 --- a/test/e2e.test.ts +++ b/test/e2e.test.ts @@ -1,5 +1,6 @@ import assert from "node:assert/strict"; -import { mkdtemp, rm } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import { mkdtemp, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { after, before, describe, it } from "node:test"; @@ -43,8 +44,9 @@ describe("generated projects install, type-check and build", { skip: !enabled }, for (const [backend, pm] of cases) { it(`${backend} with ${pm}`, { timeout: 15 * 60_000 }, async () => { const dir = join(root, `${backend}-${pm}`); + const names = derivePluginNames(`e2e-${backend}`); await scaffold(dir, { - names: derivePluginNames(`e2e-${backend}`), + names, description: "End-to-end test plugin", backend, frontend: "react", @@ -55,6 +57,9 @@ describe("generated projects install, type-check and build", { skip: !enabled }, await run(pm, ["install"], dir); await run(pm, ["run", "check"], dir); + const sdl = await readFile(join(dir, "api/generated/schema.graphql"), "utf8"); + assert.ok(sdl.includes(`${names.camel}Status`), "schema.graphql has the status field"); + assert.ok(existsSync(join(dir, "ui/src/gql/graphql.ts")), "ui/src/gql/graphql.ts"); await run(pm, ["run", "lint"], dir); await run(pm, ["run", "build"], dir); }); diff --git a/test/scaffold.test.ts b/test/scaffold.test.ts index 79f4a43..94129e7 100644 --- a/test/scaffold.test.ts +++ b/test/scaffold.test.ts @@ -107,6 +107,31 @@ for (const pm of PACKAGE_MANAGERS) { assert.equal(pkg.workspaces === undefined, pm === "pnpm"); }); + it("wires codegen into the root scripts", async () => { + const pkg = await readJson(join(dir, "package.json")); + const expectedCodegen = { + npm: "npm run schema:print -w erxes-agent-v2_api && npm run codegen -w erxes-agent-v2_ui", + pnpm: "pnpm --filter erxes-agent-v2_api schema:print && pnpm --filter erxes-agent-v2_ui codegen", + yarn: "yarn workspace erxes-agent-v2_api run schema:print && yarn workspace erxes-agent-v2_ui run codegen", + bun: "bun --filter erxes-agent-v2_api schema:print && bun --filter erxes-agent-v2_ui codegen", + }[pm]; + assert.equal(pkg.scripts.codegen, expectedCodegen); + assert.ok( + pkg.scripts.check?.startsWith( + pm === "yarn" ? "yarn codegen && " : `${pm} run codegen && `, + ), + pkg.scripts.check, + ); + assert.ok(existsSync(join(dir, "ui/codegen.ts")), "ui/codegen.ts"); + assert.ok(existsSync(join(dir, ".oxlintrc.json")), ".oxlintrc.json"); + const gitignore = await readFile(join(dir, ".gitignore"), "utf8"); + assert.match(gitignore, /api\/generated/); + assert.match(gitignore, /ui\/src\/gql/); + const graphql = await readFile(join(dir, "ui/src/graphql.ts"), "utf8"); + assert.match(graphql, /from "~\/gql"/); + assert.doesNotMatch(graphql, /@apollo\/client/); + }); + it("needs no install settings for the erxes shared libraries", async () => { assert.equal(existsSync(join(dir, ".npmrc")), false); if (pm === "pnpm") { @@ -167,6 +192,12 @@ describe("backend stacks", () => { const api = await readJson(join(dir, "api/package.json")); assert.deepEqual(api.scripts, runtimesFor({ backend })[runtimeFor(pm)]?.scripts); + assert.ok(api.scripts["schema:print"], "schema:print"); + assert.ok(existsSync(join(dir, "api/src/print-schema.ts"))); + assert.equal( + existsSync(join(dir, "api/src/resolvers.ts")), + backend === "nestjs", + ); // Scripts run in api/ and must read the single .env at the repo root. for (const script of [api.scripts.dev, api.scripts.start]) { assert.match(script ?? "", /--env-file(-if-exists)?=\.\.\/\.env\b/, script);