Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,17 @@ require('@rushstack/eslint-patch/modern-module-resolution');
module.exports = {
root: true,

ignorePatterns: ['e2e/fixtures/', 'e2e/minimum/', 'e2e/latest/'],

parserOptions: {
// https://github.com/typescript-eslint/typescript-eslint/tree/main/packages/parser#parseroptionstsconfigrootdir
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
tsconfigRootDir: __dirname,
project: ['./tsconfig.eslint.json', './packages/*/tsconfig.json'],
project: [
'./tsconfig.eslint.json',
'./packages/*/tsconfig.json',
'./e2e/tsconfig.json',
],
},

env: {
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,29 @@ jobs:
- run: pnpm lint
- run: pnpm format:check
- run: pnpm build

e2e:
name: e2e (Node.js ${{ matrix.node-version }})
runs-on: ubuntu-26.04
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
# The oldest supported version and the active LTS version.
node-version: [22, 26]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

# Reads the pnpm version from the packageManager field in package.json.
- uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6.1.1
with:
cache: true

- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ matrix.node-version }}

- run: pnpm install --frozen-lockfile
- run: pnpm test
7 changes: 7 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
pnpm-lock.yaml

# Copied fixtures and generated files in the e2e projects.
e2e/.tarballs/
e2e/minimum/*
e2e/latest/*
!e2e/minimum/package.json
!e2e/latest/package.json
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ A collection of packages to help you generate code from GraphQL documents and us
- [@dreamonkey/graphql-codegen-vue-query-block](./packages/vue-query-block) - Custom `<query>` block to define GraphQL documents in Vue SFCs
- TODO: [@dreamonkey/app-extension-graphql-codegen](./packages/app-extension) - GraphQL Code Generator scaffolding and integration with all the packages in this repo for [Quasar Framework](https://quasar.dev/) as an App Extension

## Testing

Run `pnpm test`. The command runs the end-to-end tests in `e2e/` with Vitest.
The tests build and pack each package, then install the tarballs into two projects.
The `minimum` project uses the oldest supported versions, and the `latest` project uses the newest.
Each project runs GraphQL Code Generator, type-checks the output, and runs a Vite build.

To test one project, filter by its name: `pnpm test -t latest`.

## Donate

If you appreciate the work that went into these packages, please consider [donating](https://github.com/sponsors/dreamonkey).
6 changes: 6 additions & 0 deletions e2e/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.tarballs/
# Everything in a variant project except package.json comes from fixtures/ or the test run.
/minimum/*
/latest/*
!/minimum/package.json
!/latest/package.json
88 changes: 88 additions & 0 deletions e2e/e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { execa } from 'execa';
import { beforeAll, describe, expect, test } from 'vitest';

const fixturesDir = join(import.meta.dirname, 'fixtures');
const variants = ['minimum', 'latest'];
const moduleResolutionFlags = {
bundler: ['--module', 'preserve', '--moduleResolution', 'bundler'],
node10: ['--module', 'esnext', '--moduleResolution', 'node10'],
nodenext: ['--module', 'nodenext', '--moduleResolution', 'nodenext'],
};
// Vitest sets NODE_ENV to "test". GraphQL Code Generator prints nothing when NODE_ENV is "test".
const childEnv = Object.fromEntries(
Object.entries(process.env).filter(([name]) => name !== 'NODE_ENV'),
);
const BROKEN_SFC =
'<template><div>\n<query lang="graphql">\nquery broken { count }\n</query>\n';

describe.each(variants)('%s', (variant) => {
const variantDir = join(import.meta.dirname, variant);
const execaOptions = { cwd: variantDir, env: childEnv, extendEnv: false };
const pnpm = (args: string[]) => execa('pnpm', args, execaOptions);

async function runFailingCodegen(configFileName: string) {
const result = await execa(
'pnpm',
['exec', 'graphql-codegen', '--config', configFileName],
{ ...execaOptions, all: true, reject: false },
);
expect(result.failed).toBe(true);
return result.all;
}

async function writeCodegenConfig(
fileName: string,
transform: (config: string) => string,
) {
const config = await readFile(join(variantDir, 'codegen.ts'), 'utf-8');
await writeFile(join(variantDir, fileName), transform(config));
}

beforeAll(async () => {
// Removing node_modules makes pnpm resolve the newest versions in each range again.
for (const directory of ['src', 'node_modules']) {
await rm(join(variantDir, directory), { recursive: true, force: true });
}
await cp(fixturesDir, variantDir, { recursive: true });
await pnpm(['install']);
});

test('generates code', async () => {
await pnpm(['exec', 'graphql-codegen', '--config', 'codegen.ts']);
});

test.each(Object.entries(moduleResolutionFlags))(
'type-checks the generated code with %s module resolution',
async (_moduleResolution, flags) => {
await pnpm(['exec', 'tsc', '-p', 'tsconfig.json', ...flags]);
},
);

test('builds the app with Vite', async () => {
await pnpm(['exec', 'vite', 'build']);
});

test('rejects an output file that is not TypeScript', async () => {
await writeCodegenConfig('codegen.js-output.ts', (config) =>
config.replace("extension: '.graphql.ts'", "extension: '.graphql.js'"),
);

expect(await runFailingCodegen('codegen.js-output.ts')).toContain(
'requires the output file extension to be one of',
);
});

test('reports Vue SFC parse errors', async () => {
await mkdir(join(variantDir, 'src/broken'), { recursive: true });
await writeFile(join(variantDir, 'src/broken/Broken.vue'), BROKEN_SFC);
await writeCodegenConfig('codegen.broken-sfc.ts', (config) =>
config.replace('src/components/PostCount.vue', 'src/broken/Broken.vue'),
);

expect(await runFailingCodegen('codegen.broken-sfc.ts')).toContain(
'Failed to parse the Vue SFC at src/broken/Broken.vue',
);
});
});
26 changes: 26 additions & 0 deletions e2e/fixtures/codegen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
schema: 'schema.graphql',
documents: [
'src/**/*.graphql',
{
'src/components/PostCount.vue': {
loader: '@dreamonkey/graphql-codegen-vue-query-block',
},
},
],
generates: {
'src/': {
preset: 'near-operation-file',
presetConfig: { extension: '.graphql.ts' },
plugins: [
'typescript-operations',
'typed-document-node',
'@dreamonkey/graphql-codegen-vue-apollo-plugin',
],
},
},
};

export default config;
2 changes: 2 additions & 0 deletions e2e/fixtures/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<div id="app"></div>
<script type="module" src="/main.ts"></script>
6 changes: 6 additions & 0 deletions e2e/fixtures/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createApp } from 'vue';
import PostCount from './src/components/PostCount.vue';
import { GetPostsDocument } from './src/posts.graphql';

console.log(GetPostsDocument.definitions.length);
createApp(PostCount).mount('#app');
8 changes: 8 additions & 0 deletions e2e/fixtures/pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Makes each variant project its own workspace root, outside the repository workspace.
lockfile: false

allowBuilds:
# Checks that the platform binary is installed.
esbuild: true
# Selects the Vue 2 or Vue 3 build.
vue-demi: true
22 changes: 22 additions & 0 deletions e2e/fixtures/schema.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
type Post {
id: ID!
title: String!
body: String
}

input PostInput {
title: String!
body: String
}

type Query {
posts: [Post!]!
post(id: ID!): Post
maybePosts: [Post]
count: Int!
}

type Mutation {
createPost(input: PostInput!): Post!
deletePost(id: ID!): Boolean
}
9 changes: 9 additions & 0 deletions e2e/fixtures/src/components/PostCount.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<template><div /></template>

<script lang="ts" setup></script>

<query lang="graphql">
query getPostCount {
count
}
</query>
37 changes: 37 additions & 0 deletions e2e/fixtures/src/posts.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
fragment PostDetails on Post {
id
title
body
}

query getPosts {
posts {
...PostDetails
}
}

query getPost($id: ID!) {
post(id: $id) {
...PostDetails
}
}

query getMaybePosts {
maybePosts {
id
}
}

query getCount {
count
}

mutation createPost($input: PostInput!) {
createPost(input: $input) {
...PostDetails
}
}

mutation deletePost($id: ID!) {
deletePost(id: $id)
}
25 changes: 25 additions & 0 deletions e2e/fixtures/src/usage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { ComputedRef } from 'vue';
import {
GetPostsDocument,
useGetPostsQuery,
useGetPostQuery,
useGetCountQuery,
useCreatePostMutation,
useDeletePostMutation,
} from './posts.graphql';

const { posts } = useGetPostsQuery();
const postsCheck: ComputedRef<{ id: string; title: string }[]> = posts;
const { post } = useGetPostQuery({ id: '1' });
const { count } = useGetCountQuery();
const countCheck: ComputedRef<number | undefined> = count;
const { createPost } = useCreatePostMutation();
const { deletePost } = useDeletePostMutation();

export async function run() {
const { data: newPost, cache } = await createPost({ input: { title: 'T' } });
const title: string = newPost.title;
cache.updateQuery({ query: GetPostsDocument }, (data) => data);
await deletePost({ id: '1' });
return [postsCheck, post, countCheck, title];
}
11 changes: 11 additions & 0 deletions e2e/fixtures/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "preserve",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"skipLibCheck": true
},
"include": ["src"]
}
9 changes: 9 additions & 0 deletions e2e/fixtures/vite.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import vue from '@vitejs/plugin-vue';
import nearOperationFile from '@dreamonkey/graphql-codegen-near-operation-file';
import vueQueryBlock from '@dreamonkey/graphql-codegen-vue-query-block/vite';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [vue(), nearOperationFile(), vueQueryBlock()],
build: { minify: false },
});
32 changes: 32 additions & 0 deletions e2e/global-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { mkdir, rename, rm } from 'node:fs/promises';
import { join } from 'node:path';
import { execa } from 'execa';

const tarballDir = join(import.meta.dirname, '.tarballs');

const repoDir = join(import.meta.dirname, '..');
const packagesToPack = [
'codegen-plugin',
'near-operation-file',
'vue-query-block',
];

// The variant projects install the packed tarballs. A workspace link would resolve peers from the package's own dev dependencies.
export default async function setup() {
await execa('pnpm', ['run', 'build'], { cwd: repoDir });

await rm(tarballDir, { recursive: true, force: true });
await mkdir(tarballDir);
for (const packageName of packagesToPack) {
const { stdout } = await execa(
'pnpm',
['pack', '--pack-destination', tarballDir],
{ cwd: join(repoDir, 'packages', packageName) },
);
const tarballPath = stdout.trim().split('\n').at(-1);
if (!tarballPath) {
throw new Error(`pnpm pack printed no tarball path for ${packageName}`);
}
await rename(tarballPath, join(tarballDir, `${packageName}.tgz`));
}
}
23 changes: 23 additions & 0 deletions e2e/latest/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "e2e-latest",
"private": true,
"description": "The newest dependency versions the packages support.",
"dependencies": {
"@apollo/client": "^3.6.0",
"@dreamonkey/graphql-codegen-near-operation-file": "file:../.tarballs/near-operation-file.tgz",
"@dreamonkey/graphql-codegen-vue-apollo-plugin": "file:../.tarballs/codegen-plugin.tgz",
"@dreamonkey/graphql-codegen-vue-query-block": "file:../.tarballs/vue-query-block.tgz",
"@graphql-codegen/cli": "^8.0.0",
"@graphql-codegen/near-operation-file-preset": "^5.0.0",
"@graphql-codegen/typed-document-node": "^8.0.0",
"@graphql-codegen/typescript-operations": "^7.0.0",
"@graphql-typed-document-node/core": "^3.2.0",
"@vitejs/plugin-vue": "^6.0.0",
"@vue/apollo-composable": "^4.0.0",
"@vue/compiler-sfc": "^3.3.0",
"graphql": "^16.0.0",
"typescript": "~5.9.0",
"vite": "^8.0.0",
"vue": "^3.3.0"
}
}
Loading
Loading