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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,16 @@ codegen({
* Override the codegen config file path.
*/
configFilePathOverride: string,
/**
* Skip codegen for a given cycle when true.
*
* The callback receives the current trigger and the changed file path
* for watcher-driven runs.
*/
skip:
boolean |
((context: { trigger: "start" | "build" | "watch"; filePath?: string }) =>
boolean | Promise<boolean>),
/**
* Log various steps to aid in tracking down bugs.
*
Expand Down
44 changes: 36 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ import { createMatchCache } from "./utils/matchCache";
import { isBuildMode, isServeMode, type ViteMode } from "./utils/viteModes";
import type { Plugin } from "vite";

export interface SkipContext {
trigger: "start" | "build" | "watch";
filePath?: string;
}

export type SkipFn = (context: SkipContext) => boolean | Promise<boolean>;

export interface Options {
/**
* Run codegen on server start.
Expand Down Expand Up @@ -89,6 +96,12 @@ export interface Options {
* Override the codegen config file path.
*/
configFilePathOverride?: string;
/**
* Skip codegen for a given cycle.
*
* @default false
*/
skip?: boolean | SkipFn;
/**
* Log various steps to aid in tracking down bugs.
*
Expand Down Expand Up @@ -117,6 +130,7 @@ export function GraphQLCodegen(options?: Options): Plugin {
configOverrideOnBuild = {},
configOverrideWatcher = {},
configFilePathOverride,
skip = false,
debug = false,
} = options ?? {};

Expand All @@ -125,18 +139,28 @@ export function GraphQLCodegen(options?: Options): Plugin {
debugLog(...args);
};

const shouldSkipGeneration = async (context: SkipContext) =>
typeof skip === "function" ? await skip(context) : skip;

const generateWithOverride = async (
overrideConfig: Partial<CodegenConfig>,
skipContext: SkipContext,
) => {
const currentConfig = codegenContext.getConfig();
if (await shouldSkipGeneration(skipContext)) {
log("Generation skipped", skipContext);
return;
}

return await generate({
const currentConfig = codegenContext.getConfig();
await generate({
...currentConfig,
...configOverride,
...overrideConfig,
// Vite handles file watching
watch: false,
});

log(`Generation successful on ${skipContext.trigger}`);
};

if (options) log("Plugin initialized with options:", options);
Expand Down Expand Up @@ -167,8 +191,9 @@ export function GraphQLCodegen(options?: Options): Plugin {
if (!runOnStart) return;

try {
await generateWithOverride(configOverrideOnStart);
log("Generation successful on start");
await generateWithOverride(configOverrideOnStart, {
trigger: "start",
});
} catch (error) {
// GraphQL Codegen handles logging useful errors
log("Generation failed on start");
Expand All @@ -180,8 +205,9 @@ export function GraphQLCodegen(options?: Options): Plugin {
if (!runOnBuild) return;

try {
await generateWithOverride(configOverrideOnBuild);
log("Generation successful on build");
await generateWithOverride(configOverrideOnBuild, {
trigger: "build",
});
} catch (error) {
// GraphQL Codegen handles logging useful errors
log("Generation failed on build");
Expand All @@ -204,8 +230,10 @@ export function GraphQLCodegen(options?: Options): Plugin {
log("File is in match cache");

try {
await generateWithOverride(configOverrideWatcher);
log("Generation successful in file watcher");
await generateWithOverride(configOverrideWatcher, {
trigger: "watch",
filePath,
});
} catch {
// GraphQL Codegen handles logging useful errors
log("Generation failed in file watcher");
Expand Down
233 changes: 233 additions & 0 deletions test/skip/skip.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import { promises as fs } from "node:fs";
import { createServer, type UserConfig } from "vite";
import { afterEach, describe, expect, it, vi, type TestContext } from "vitest";
import codegen, { type Options, type SkipContext } from "../../src/index";

const codegenGenerateMock = vi.hoisted(() => vi.fn());
vi.mock("@graphql-codegen/cli", async (importOriginal) => {
const actual = await importOriginal<typeof import("@graphql-codegen/cli")>();

return {
...actual,
generate: codegenGenerateMock,
};
});

const TEST_PATH = "./test/skip" as const;
const DOCUMENT_PATH = `${TEST_PATH}/graphql` as const;
const SCHEMA_FILE = `${TEST_PATH}/schema.graphql` as const;
const QUERY_FILE = `${DOCUMENT_PATH}/Foo.graphql` as const;
const OUTPUT_PATH = `${TEST_PATH}/generated` as const;
const OUTPUT_FILE = `${OUTPUT_PATH}/graphql.ts` as const;

const setupFiles = async () => {
await fs.mkdir(DOCUMENT_PATH, { recursive: true });
await fs.writeFile(
SCHEMA_FILE,
`
type Query {
foo: String
bar: String
}
`,
);
await fs.writeFile(QUERY_FILE, "query Foo { foo }");
};

const updateQueryFile = async (content: string) => {
await fs.writeFile(QUERY_FILE, content);
await new Promise((resolve) => setTimeout(resolve, 200));
};

interface TestContextWithServer extends TestContext {
viteServer: Awaited<ReturnType<typeof createServer>> | null;
}

const startServer = async (
options: Options = {},
context: TestContextWithServer,
) => {
await setupFiles();

const config = {
root: import.meta.dirname,
logLevel: "silent",
server: {
host: "127.0.0.1",
port: 0,
strictPort: false,
},
plugins: [
codegen({
config: {
schema: SCHEMA_FILE,
documents: `${DOCUMENT_PATH}/**/*.graphql`,
generates: {
[OUTPUT_FILE]: {
plugins: ["typescript", "typescript-operations"],
},
},
},
...options,
}),
],
} satisfies UserConfig;

context.viteServer = await createServer(config).then((server) =>
server.listen(),
);

// ensure the watcher is ready before proceeding with tests
await vi.waitFor(() => {
expect(
context.viteServer?.watcher.listeners("change").length,
).toBeGreaterThan(1);
});
};

describe("skip", () => {
afterEach<TestContextWithServer>(async (context) => {
codegenGenerateMock.mockReset();
context.viteServer?.watcher.close();
await context.viteServer?.close();
context.viteServer = null;

await fs.rm(SCHEMA_FILE, { force: true });
await fs.rm(DOCUMENT_PATH, { recursive: true, force: true });
await fs.rm(OUTPUT_PATH, { recursive: true, force: true });
});

describe("on server start", () => {
it.for([
{
describe: "when skip is not set",
skip: undefined,
},
{
describe: "when skip is false",
skip: false,
},
{
describe: "when skip returns false by matching start trigger",
skip: ({ trigger }: SkipContext) => trigger !== "start",
},
{
describe: "when skip resolves false by matching start trigger",
skip: ({ trigger }: SkipContext) =>
Promise.resolve(trigger !== "start"),
},
])("it should run codegen $describe", async ({ skip }, context) => {
await startServer({ skip }, context as TestContextWithServer);

expect(codegenGenerateMock).toHaveBeenCalledWith({
pluginContext: {},
schema: SCHEMA_FILE,
documents: `${DOCUMENT_PATH}/**/*.graphql`,
watch: false,
generates: {
[OUTPUT_FILE]: {
plugins: ["typescript", "typescript-operations"],
},
},
});
});

it.for([
{
describe: "when skip is true",
skip: true,
},
{
describe: "when skip returns true by matching start trigger",
skip: ({ trigger }: SkipContext) => trigger === "start",
},
{
describe: "when skip resolves true by matching start trigger",
skip: ({ trigger }: SkipContext) =>
Promise.resolve(trigger === "start"),
},
])("it should skip codegen $describe", async ({ skip }, context) => {
await startServer({ skip }, context as TestContextWithServer);

expect(codegenGenerateMock).not.toHaveBeenCalled();
});
});

describe("on watch triggered", () => {
it.for([
{
describe: "when skip is not set",
skip: undefined,
},
{
describe: "when skip is false",
skip: false,
},
{
describe: "when skip returns false by matching watch trigger",
skip: ({ trigger }: SkipContext) => trigger !== "watch",
},
{
describe: "when skip resolves false by matching watch trigger",
skip: ({ trigger }: SkipContext) =>
Promise.resolve(trigger !== "watch"),
},
{
describe: "when skip resolves false by matching filePath",
skip: async ({ filePath }: SkipContext) =>
filePath !== (await fs.realpath(QUERY_FILE)),
},
])("it should run codegen $describe", async ({ skip }, context) => {
await startServer(
{ skip, enableWatcher: true },
context as TestContextWithServer,
);
codegenGenerateMock.mockReset();

await updateQueryFile("query Foo { foo bar }");

expect(codegenGenerateMock).toHaveBeenCalledWith({
pluginContext: {},
schema: SCHEMA_FILE,
documents: `${DOCUMENT_PATH}/**/*.graphql`,
watch: false,
generates: {
[OUTPUT_FILE]: {
plugins: ["typescript", "typescript-operations"],
},
},
});
});

it.for([
{
describe: "when skip is true",
skip: true,
},
{
describe: "when skip returns true by matching watch trigger",
skip: ({ trigger }: SkipContext) => trigger === "watch",
},
{
describe: "when skip resolves true by matching watch trigger",
skip: ({ trigger }: SkipContext) =>
Promise.resolve(trigger === "watch"),
},
{
describe: "when skip resolves true by matching filePath",
skip: async ({ filePath }: SkipContext) =>
filePath === (await fs.realpath(QUERY_FILE)),
},
])("it should skip codegen $describe", async ({ skip }, context) => {
await startServer(
{ skip, enableWatcher: true },
context as TestContextWithServer,
);
codegenGenerateMock.mockReset();

await updateQueryFile("query Foo { foo bar }");

expect(codegenGenerateMock).not.toHaveBeenCalled();
});
});
});