From 67577201d30d49cd1495641fce5e012539645575 Mon Sep 17 00:00:00 2001 From: Michael van Tellingen Date: Mon, 2 Sep 2024 22:47:31 +0200 Subject: [PATCH] format with biome --- .editorconfig | 12 +++ .github/workflows/release.yml | 94 ++++++++++++++++++++++ .vscode/extensions.json | 10 +++ .vscode/settings.json | 33 ++++++++ biome.json | 21 +++++ package.json | 48 +++++------ pnpm-lock.yaml | 91 +++++++++++++++++++++ src/bruno.ts | 19 ++--- src/index.ts | 40 +++++----- src/operations.ts | 145 ++++++++++++++++++---------------- src/vars.ts | 93 ++++++++++++++++++++++ tsconfig.json | 20 ++--- tsup.config.js | 18 ++--- 13 files changed, 505 insertions(+), 139 deletions(-) create mode 100644 .editorconfig create mode 100644 .github/workflows/release.yml create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 biome.json create mode 100644 src/vars.ts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e1a35af --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +# Set up tabs as indent style for a11y reasons +# This will be the default for prettier in >=v3 +[*] +indent_style = tab +indent_size = 2 + +# Yaml does not support tabs +[*.{yml,yaml}] +indent_style = space +indent_size = 2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..80804a9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,94 @@ +name: Release +on: [push] + +env: + CI: true + +jobs: + lint: + name: Lint codebase + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - uses: pnpm/action-setup@v4 + name: Install pnpm + id: pnpm-install + with: + run_install: true + + - name: Lint + run: pnpm lint + + build: + name: Build, and test on Node ${{ matrix.node }} and ${{ matrix.os }} + runs-on: ${{ matrix.os }} + needs: lint + strategy: + matrix: + node: ["18.x", "20.x", "22.x"] + os: [ubuntu-latest] + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + name: Install pnpm + id: pnpm-install + with: + version: 8 + run_install: true + + - name: Use Node ${{ matrix.node }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: "pnpm" + + - name: Test + run: pnpm run test:ci + + - name: Build + run: pnpm build + + release: + timeout-minutes: 15 + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + needs: build + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: pnpm/action-setup@v4 + name: Install pnpm + id: pnpm-install + with: + version: 8 + run_install: true + + - name: Install node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "pnpm" + + - name: Create and publish versions + uses: changesets/action@v1 + with: + title: "Release new version" + commit: "update version" + publish: pnpm publish:ci + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..ae9d2df --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + "recommendations": [ + "biomejs.biome", + "bradlc.vscode-tailwindcss", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "GraphQL.vscode-graphql-syntax", + "GraphQL.vscode-graphql" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..cccbb39 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,33 @@ +{ + "eslint.enable": true, + "prettier.enable": false, + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true, + "eslint.format.enable": false, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.fixAll.eslint": "never", + "source.organizeImports": "never", + "source.organizeImports.biome": "explicit" + }, + "files.watcherExclude": { + "**/.git/objects/**": true, + "**/.git/subtree-cache/**": true, + "**/node_modules/**": true, + "**/build": true, + "**/dist": true + }, + "search.exclude": { + "**/node_modules": true, + "**/dist": true + }, + "[typescript]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[jsonc]": { + "editor.defaultFormatter": "biomejs.biome" + }, + "[json]": { + "editor.defaultFormatter": "biomejs.biome" + } +} diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..d85ae0e --- /dev/null +++ b/biome.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", + "organizeImports": { + "enabled": true + }, + "files": { + "ignore": ["dist/*"] + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "noExplicitAny": "warn" + }, + "style": { + "noUselessElse": "off" + } + } + } +} diff --git a/package.json b/package.json index 4b81d04..aacce6b 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,28 @@ { - "name": "@labdigital/graphql-codegen-bruno", - "version": "1.0.0", - "main": "dist/index.js", - "scripts": { - "build": "tsup", - "clean": "rm -rf dist" - }, - "dependencies": { - "@graphql-codegen/cli": "^2.0.0", - "@graphql-codegen/plugin-helpers": "^2.0.0", - "fs-extra": "^10.0.0", - "graphql": "^16.9.0", - "prettier": "^3.3.3" - }, - "devDependencies": { - "@types/fs-extra": "^11.0.4", - "@types/graphql": "^14.5.0", - "@types/node": "^20.0.0", - "tsup": "^8.2.4", - "typescript": "^5.0.0" - }, - "packageManager": "pnpm@8.15.8" + "name": "@labdigital/graphql-codegen-bruno", + "version": "1.0.0", + "main": "dist/index.js", + "scripts": { + "build": "tsup", + "lint": "biome ci", + "clean": "rm -rf dist" + }, + "dependencies": { + "@biomejs/biome": "^1.8.3", + "@graphql-codegen/cli": "^2.0.0", + "@graphql-codegen/plugin-helpers": "^2.0.0", + "fs-extra": "^10.0.0", + "prettier": "^3.3.3" + }, + "devDependencies": { + "@types/fs-extra": "^11.0.4", + "@types/graphql": "^14.5.0", + "@types/node": "^20.0.0", + "tsup": "^8.2.4", + "typescript": "^5.0.0" + }, + "peerDependencies": { + "graphql": "^16.9.0" + }, + "packageManager": "pnpm@8.15.8" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6726332..2374e4a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@biomejs/biome': + specifier: ^1.8.3 + version: 1.8.3 '@graphql-codegen/cli': specifier: ^2.0.0 version: 2.16.5(@babel/core@7.25.2)(@types/node@20.16.3)(graphql@16.9.0)(typescript@5.5.4) @@ -238,6 +241,94 @@ packages: to-fast-properties: 2.0.0 dev: false + /@biomejs/biome@1.8.3: + resolution: {integrity: sha512-/uUV3MV+vyAczO+vKrPdOW0Iaet7UnJMU4bNMinggGJTAnBPjCoLEYcyYtYHNnUNYlv4xZMH6hVIQCAozq8d5w==} + engines: {node: '>=14.21.3'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@biomejs/cli-darwin-arm64': 1.8.3 + '@biomejs/cli-darwin-x64': 1.8.3 + '@biomejs/cli-linux-arm64': 1.8.3 + '@biomejs/cli-linux-arm64-musl': 1.8.3 + '@biomejs/cli-linux-x64': 1.8.3 + '@biomejs/cli-linux-x64-musl': 1.8.3 + '@biomejs/cli-win32-arm64': 1.8.3 + '@biomejs/cli-win32-x64': 1.8.3 + dev: false + + /@biomejs/cli-darwin-arm64@1.8.3: + resolution: {integrity: sha512-9DYOjclFpKrH/m1Oz75SSExR8VKvNSSsLnVIqdnKexj6NwmiMlKk94Wa1kZEdv6MCOHGHgyyoV57Cw8WzL5n3A==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@biomejs/cli-darwin-x64@1.8.3: + resolution: {integrity: sha512-UeW44L/AtbmOF7KXLCoM+9PSgPo0IDcyEUfIoOXYeANaNXXf9mLUwV1GeF2OWjyic5zj6CnAJ9uzk2LT3v/wAw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@biomejs/cli-linux-arm64-musl@1.8.3: + resolution: {integrity: sha512-9yjUfOFN7wrYsXt/T/gEWfvVxKlnh3yBpnScw98IF+oOeCYb5/b/+K7YNqKROV2i1DlMjg9g/EcN9wvj+NkMuQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@biomejs/cli-linux-arm64@1.8.3: + resolution: {integrity: sha512-fed2ji8s+I/m8upWpTJGanqiJ0rnlHOK3DdxsyVLZQ8ClY6qLuPc9uehCREBifRJLl/iJyQpHIRufLDeotsPtw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@biomejs/cli-linux-x64-musl@1.8.3: + resolution: {integrity: sha512-UHrGJX7PrKMKzPGoEsooKC9jXJMa28TUSMjcIlbDnIO4EAavCoVmNQaIuUSH0Ls2mpGMwUIf+aZJv657zfWWjA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@biomejs/cli-linux-x64@1.8.3: + resolution: {integrity: sha512-I8G2QmuE1teISyT8ie1HXsjFRz9L1m5n83U1O6m30Kw+kPMPSKjag6QGUn+sXT8V+XWIZxFFBoTDEDZW2KPDDw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@biomejs/cli-win32-arm64@1.8.3: + resolution: {integrity: sha512-J+Hu9WvrBevfy06eU1Na0lpc7uR9tibm9maHynLIoAjLZpQU3IW+OKHUtyL8p6/3pT2Ju5t5emReeIS2SAxhkQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@biomejs/cli-win32-x64@1.8.3: + resolution: {integrity: sha512-/PJ59vA1pnQeKahemaQf4Nyj7IKUvGQSc3Ze1uIGi+Wvr1xF7rGobSrAAG01T/gUDG21vkDsZYM03NAmPiVkqg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@cspotcode/source-map-support@0.8.1: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} diff --git a/src/bruno.ts b/src/bruno.ts index b17aae0..13d1ead 100644 --- a/src/bruno.ts +++ b/src/bruno.ts @@ -1,15 +1,12 @@ -import { FileContent } from "./operations"; import prettier from "prettier"; -import fs from "fs-extra"; +import type { FileContent } from "./operations"; export const asBruno = async (operation: FileContent, sequence: number) => { + const formattedContent = await prettier.format(operation.content, { + parser: "graphql", + }); - const formattedContent = await prettier.format(operation.content, { - parser: "graphql", - }); - - - return ` + return ` meta { name: ${operation.name} type: graphql @@ -27,7 +24,7 @@ body:graphql { } body:graphql:vars { + ${JSON.stringify(operation.vars, null, 2).split("\n").join("\n ")} } - ` -} - + `; +}; diff --git a/src/index.ts b/src/index.ts index fed5d03..30e6ba9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,34 +1,32 @@ -import { PluginFunction, Types } from "@graphql-codegen/plugin-helpers"; -import { GraphQLSchema } from "graphql"; +import path from "node:path"; +import type { PluginFunction, Types } from "@graphql-codegen/plugin-helpers"; import fs from "fs-extra"; -import path from "path"; -import prettier from "prettier"; -import { extractOperations } from "./operations"; +import type { GraphQLSchema } from "graphql"; import { asBruno } from "./bruno"; +import { extractOperations } from "./operations"; export interface BruPluginConfig { - outputDir?: string; + outputDir?: string; } export const plugin: PluginFunction = async ( - schema: GraphQLSchema, - documents: Types.DocumentFile[], - config: BruPluginConfig + schema: GraphQLSchema, + documents: Types.DocumentFile[], + config: BruPluginConfig, ): Promise => { - const outputDir = config.outputDir || "bruno-queries"; + const outputDir = config.outputDir || "bruno-queries"; - const operations = extractOperations(documents); - let i = 0; - for (const operation of operations) { - const fileName = `${operation.name}.bru`; - const outputPath = path.join(outputDir, fileName); + const operations = extractOperations(schema, documents); + let i = 0; + for (const operation of operations) { + const fileName = `${operation.name}.bru`; + const outputPath = path.join(outputDir, fileName); - const formattedContent = await asBruno(operation, i); + const formattedContent = await asBruno(operation, i); - fs.outputFileSync(outputPath, formattedContent); - console.log(`Generated .bru file: ${outputPath}`); - i++; - } + fs.outputFileSync(outputPath, formattedContent); + i++; + } - return ""; + return ""; }; diff --git a/src/operations.ts b/src/operations.ts index 64333a1..b977f38 100644 --- a/src/operations.ts +++ b/src/operations.ts @@ -1,86 +1,99 @@ -import { Types } from "@graphql-codegen/plugin-helpers"; -import { Kind, FragmentDefinitionNode, OperationDefinitionNode } from "graphql"; +import type { Types } from "@graphql-codegen/plugin-helpers"; +import { + type FragmentDefinitionNode, + type GraphQLSchema, + Kind, + type OperationDefinitionNode, +} from "graphql"; +import { generateExampleVariables } from "./vars"; export interface BruPluginConfig { - outputDir?: string; + outputDir?: string; } export type FileContent = { - name: string; - content: string; + name: string; + content: string; + vars: Record; }; export const extractOperations = ( - documents: Types.DocumentFile[] + schema: GraphQLSchema, + documents: Types.DocumentFile[], ): FileContent[] => { - const results: FileContent[] = []; + const results: FileContent[] = []; - // Step 1: Collect all fragments - const allFragments: Record = {}; + // Step 1: Collect all fragments + const allFragments: Record = {}; - for (const doc of documents) { - if (doc.document) { - for (const definition of doc.document.definitions) { - if (definition.kind === Kind.FRAGMENT_DEFINITION) { - const fragmentName = definition.name.value; - allFragments[fragmentName] = definition as FragmentDefinitionNode; - } - } - } - } + for (const doc of documents) { + if (doc.document) { + for (const definition of doc.document.definitions) { + if (definition.kind === Kind.FRAGMENT_DEFINITION) { + const fragmentName = definition.name.value; + allFragments[fragmentName] = definition as FragmentDefinitionNode; + } + } + } + } - // Step 2: Process operations and append only used fragments - for (const doc of documents) { - if (doc.document) { - const operations = doc.document.definitions - .filter((def) => def.kind === Kind.OPERATION_DEFINITION) - .map((operation) => { - let operationString = doc.rawSDL || ""; + // Step 2: Process operations and append only used fragments + for (const doc of documents) { + if (doc.document) { + const operations = doc.document.definitions + .filter((def) => def.kind === Kind.OPERATION_DEFINITION) + .map((operation) => { + const operationString = doc.rawSDL || ""; - // Collect the names of all fragments used in this operation - const usedFragments = new Set(); - const collectFragments = ( - node: OperationDefinitionNode | FragmentDefinitionNode - ) => { - if (node.selectionSet) { - for (const selection of node.selectionSet.selections) { - if (selection.kind === Kind.FRAGMENT_SPREAD) { - usedFragments.add(selection.name.value); - // Recursively collect fragments within fragments - if (allFragments[selection.name.value]) { - collectFragments(allFragments[selection.name.value]); - } - } else if (selection.selectionSet) { - collectFragments(selection as OperationDefinitionNode); - } - } - } - }; + // Collect the names of all fragments used in this operation + const usedFragments = new Set(); + const collectFragments = ( + node: OperationDefinitionNode | FragmentDefinitionNode, + ) => { + if (node.selectionSet) { + for (const selection of node.selectionSet.selections) { + if (selection.kind === Kind.FRAGMENT_SPREAD) { + usedFragments.add(selection.name.value); + // Recursively collect fragments within fragments + if (allFragments[selection.name.value]) { + collectFragments(allFragments[selection.name.value]); + } + } else if (selection.selectionSet) { + // @ts-expect-error + collectFragments(selection as OperationDefinitionNode); + } + } + } + }; - collectFragments(operation as OperationDefinitionNode); + collectFragments(operation as OperationDefinitionNode); - // Append only the used fragments - let fileContent = operationString; + // Append only the used fragments + let fileContent = operationString; - for (const fragmentName of usedFragments) { - const fragment = allFragments[fragmentName]; - if (fragment) { - const fragmentString = fragment.loc?.source.body || ""; - fileContent += `\n${fragmentString}`; - } - } + for (const fragmentName of usedFragments) { + const fragment = allFragments[fragmentName]; + if (fragment) { + const fragmentString = fragment.loc?.source.body || ""; + fileContent += `\n${fragmentString}`; + } + } - return { - name: - (operation as any).name?.value || - `Unnamed_${Math.random().toString(36).substring(7)}`, - content: fileContent, - } satisfies FileContent; - }); + return { + name: + (operation as any).name?.value || + `Unnamed_${Math.random().toString(36).substring(7)}`, + content: fileContent, + vars: generateExampleVariables( + schema, + operation as OperationDefinitionNode, + ), + } satisfies FileContent; + }); - results.push(...operations); - } - } + results.push(...operations); + } + } - return results; + return results; }; diff --git a/src/vars.ts b/src/vars.ts new file mode 100644 index 0000000..3fac07a --- /dev/null +++ b/src/vars.ts @@ -0,0 +1,93 @@ +import { + GraphQLInputObjectType, + GraphQLList, + GraphQLNonNull, + GraphQLObjectType, + GraphQLScalarType, + type GraphQLSchema, + type GraphQLType, + Kind, + type NamedTypeNode, + type OperationDefinitionNode, +} from "graphql"; + +const generateExampleValue = ( + schema: GraphQLSchema, + type: GraphQLType, +): any => { + if (type instanceof GraphQLNonNull) { + return generateExampleValue(schema, type.ofType); + } else if (type instanceof GraphQLList) { + return [generateExampleValue(schema, type.ofType)]; + } else if (type instanceof GraphQLScalarType) { + switch (type.name) { + case "Int": + return 123; + case "Float": + return 123.45; + case "String": + return "example"; + case "Boolean": + return true; + case "ID": + return "example-id"; + default: + return "example"; + } + } else if (type instanceof GraphQLInputObjectType) { + const fields = type.getFields(); + const exampleObject: Record = {}; + + for (const key in fields) { + exampleObject[key] = generateExampleValue(schema, fields[key].type); + } + + return exampleObject; + } else if (type instanceof GraphQLObjectType) { + // Similar logic as above for handling output types, if needed. + return {}; + } else { + return ""; + } +}; + +export const generateExampleVariables = ( + schema: GraphQLSchema, + operation: OperationDefinitionNode, +): Record => { + const exampleVariables: Record = {}; + + for (const variable of operation.variableDefinitions || []) { + if (variable.kind === Kind.VARIABLE_DEFINITION) { + let variableType: GraphQLType | undefined; + + // Narrow the type to NamedTypeNode + if (variable.type.kind === Kind.NAMED_TYPE) { + variableType = schema.getType( + (variable.type as NamedTypeNode).name.value, + ); + } else if (variable.type.kind === Kind.NON_NULL_TYPE) { + const nonNullType = variable.type.type; + if (nonNullType.kind === Kind.NAMED_TYPE) { + variableType = schema.getType( + (nonNullType as NamedTypeNode).name.value, + ); + } + } else if (variable.type.kind === Kind.LIST_TYPE) { + const listType = variable.type.type; + if (listType.kind === Kind.NAMED_TYPE) { + variableType = schema.getType((listType as NamedTypeNode).name.value); + } + } + + if (variableType) { + exampleVariables[variable.variable.name.value] = generateExampleValue( + schema, + variableType, + ); + } + } + } + + return exampleVariables; +}; diff --git a/tsconfig.json b/tsconfig.json index b53697a..876f1c0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,12 +1,12 @@ { - "compilerOptions": { - "target": "ES2022", - "module": "ES2022", - "moduleResolution": "bundler", - "outDir": "./dist", - "esModuleInterop": true, - "skipLibCheck": true, - "strict": true - }, - "include": ["src/**/*.ts"] + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "outDir": "./dist", + "esModuleInterop": true, + "skipLibCheck": true, + "strict": true + }, + "include": ["src/**/*.ts"] } diff --git a/tsup.config.js b/tsup.config.js index ce15469..02f44a2 100644 --- a/tsup.config.js +++ b/tsup.config.js @@ -1,12 +1,12 @@ -import { defineConfig } from 'tsup'; +import { defineConfig } from "tsup"; export default defineConfig({ - entry: ['src/index.ts'], - format: ['cjs'], // Support both CommonJS and ESM - dts: false, // Generate .d.ts files - sourcemap: false, // Generate sourcemaps - clean: true, // Clean output directory before each build - minify: false, // Minify the output - target: 'es2022', // Target the latest version of ECMAScript - outDir: 'dist', // Output directory + entry: ["src/index.ts"], + format: ["cjs"], // Support both CommonJS and ESM + dts: false, // Generate .d.ts files + sourcemap: false, // Generate sourcemaps + clean: true, // Clean output directory before each build + minify: false, // Minify the output + target: "es2022", // Target the latest version of ECMAScript + outDir: "dist", // Output directory });