Skip to content

Commit 82ee2fb

Browse files
committed
feat: Add skip option
1 parent 6f59e77 commit 82ee2fb

3 files changed

Lines changed: 295 additions & 6 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,16 @@ codegen({
113113
* Override the codegen config file path.
114114
*/
115115
configFilePathOverride: string,
116+
/**
117+
* Skip codegen for a given cycle when true.
118+
*
119+
* The callback receives the current trigger and the changed file path
120+
* for watcher-driven runs.
121+
*/
122+
skip:
123+
boolean |
124+
((context: { trigger: "start" | "build" | "watch"; filePath?: string }) =>
125+
boolean | Promise<boolean>),
116126
/**
117127
* Log various steps to aid in tracking down bugs.
118128
*

src/index.ts

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ import { createMatchCache } from "./utils/matchCache";
1111
import { isBuildMode, isServeMode, type ViteMode } from "./utils/viteModes";
1212
import type { Plugin } from "vite";
1313

14+
export interface SkipContext {
15+
trigger: "start" | "build" | "watch";
16+
filePath?: string;
17+
}
18+
19+
export type SkipFn = (context: SkipContext) => boolean | Promise<boolean>;
20+
1421
export interface Options {
1522
/**
1623
* Run codegen on server start.
@@ -89,6 +96,12 @@ export interface Options {
8996
* Override the codegen config file path.
9097
*/
9198
configFilePathOverride?: string;
99+
/**
100+
* Skip codegen for a given cycle.
101+
*
102+
* @default false
103+
*/
104+
skip?: boolean | SkipFn;
92105
/**
93106
* Log various steps to aid in tracking down bugs.
94107
*
@@ -117,6 +130,7 @@ export function GraphQLCodegen(options?: Options): Plugin {
117130
configOverrideOnBuild = {},
118131
configOverrideWatcher = {},
119132
configFilePathOverride,
133+
skip = false,
120134
debug = false,
121135
} = options ?? {};
122136

@@ -139,6 +153,31 @@ export function GraphQLCodegen(options?: Options): Plugin {
139153
});
140154
};
141155

156+
const shouldSkipGeneration = async (context: SkipContext) => {
157+
const skipValue = typeof skip === "function" ? await skip(context) : skip;
158+
159+
log("Skip evaluated", {
160+
...context,
161+
skip: skipValue,
162+
});
163+
164+
return skipValue;
165+
};
166+
167+
const generateIfNotSkipped = async (
168+
context: SkipContext,
169+
overrideConfig: Partial<CodegenConfig>,
170+
) => {
171+
if (await shouldSkipGeneration(context)) {
172+
log("Generation skipped", context);
173+
return false;
174+
}
175+
176+
await generateWithOverride(overrideConfig);
177+
178+
return true;
179+
};
180+
142181
if (options) log("Plugin initialized with options:", options);
143182

144183
return {
@@ -167,8 +206,15 @@ export function GraphQLCodegen(options?: Options): Plugin {
167206
if (!runOnStart) return;
168207

169208
try {
170-
await generateWithOverride(configOverrideOnStart);
171-
log("Generation successful on start");
209+
const generated = await generateIfNotSkipped(
210+
{ trigger: "start" },
211+
configOverrideOnStart,
212+
);
213+
log(
214+
generated
215+
? "Generation successful on start"
216+
: "Generation skipped on start",
217+
);
172218
} catch (error) {
173219
// GraphQL Codegen handles logging useful errors
174220
log("Generation failed on start");
@@ -180,8 +226,15 @@ export function GraphQLCodegen(options?: Options): Plugin {
180226
if (!runOnBuild) return;
181227

182228
try {
183-
await generateWithOverride(configOverrideOnBuild);
184-
log("Generation successful on build");
229+
const generated = await generateIfNotSkipped(
230+
{ trigger: "build" },
231+
configOverrideOnBuild,
232+
);
233+
log(
234+
generated
235+
? "Generation successful on build"
236+
: "Generation skipped on build",
237+
);
185238
} catch (error) {
186239
// GraphQL Codegen handles logging useful errors
187240
log("Generation failed on build");
@@ -204,8 +257,15 @@ export function GraphQLCodegen(options?: Options): Plugin {
204257
log("File is in match cache");
205258

206259
try {
207-
await generateWithOverride(configOverrideWatcher);
208-
log("Generation successful in file watcher");
260+
const generated = await generateIfNotSkipped(
261+
{ trigger: "watch", filePath },
262+
configOverrideWatcher,
263+
);
264+
log(
265+
generated
266+
? "Generation successful in file watcher"
267+
: "Generation skipped in file watcher",
268+
);
209269
} catch {
210270
// GraphQL Codegen handles logging useful errors
211271
log("Generation failed in file watcher");

test/skip/skip.spec.ts

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import { promises as fs } from "node:fs";
2+
import { createServer, type UserConfig } from "vite";
3+
import { afterEach, describe, expect, it, vi, type TestContext } from "vitest";
4+
import codegen, { type Options } from "../../src/index";
5+
6+
const codegenGenerateMock = vi.hoisted(() => vi.fn());
7+
vi.mock("@graphql-codegen/cli", async (importOriginal) => {
8+
const actual = await importOriginal<typeof import("@graphql-codegen/cli")>();
9+
10+
return {
11+
...actual,
12+
generate: codegenGenerateMock,
13+
};
14+
});
15+
16+
const TEST_PATH = "./test/skip" as const;
17+
const DOCUMENT_PATH = `${TEST_PATH}/graphql` as const;
18+
const SCHEMA_FILE = `${TEST_PATH}/schema.graphql` as const;
19+
const QUERY_FILE = `${DOCUMENT_PATH}/Foo.graphql` as const;
20+
const OUTPUT_PATH = `${TEST_PATH}/generated` as const;
21+
const OUTPUT_FILE = `${OUTPUT_PATH}/graphql.ts` as const;
22+
23+
const setupFiles = async () => {
24+
await fs.mkdir(DOCUMENT_PATH, { recursive: true });
25+
await fs.writeFile(
26+
SCHEMA_FILE,
27+
`
28+
type Query {
29+
foo: String
30+
bar: String
31+
}
32+
`,
33+
);
34+
await fs.writeFile(QUERY_FILE, "query Foo { foo }");
35+
};
36+
37+
const updateQueryFile = async (content: string) => {
38+
await fs.writeFile(QUERY_FILE, content);
39+
await new Promise((resolve) => setTimeout(resolve, 200));
40+
};
41+
42+
interface TestContextWithServer extends TestContext {
43+
viteServer: Awaited<ReturnType<typeof createServer>> | null;
44+
}
45+
46+
const startServer = async (
47+
options: Options = {},
48+
context: TestContextWithServer,
49+
) => {
50+
await setupFiles();
51+
52+
const config = {
53+
root: import.meta.dirname,
54+
logLevel: "silent",
55+
server: {
56+
host: "127.0.0.1",
57+
port: 0,
58+
strictPort: false,
59+
},
60+
plugins: [
61+
codegen({
62+
config: {
63+
schema: SCHEMA_FILE,
64+
documents: `${DOCUMENT_PATH}/**/*.graphql`,
65+
generates: {
66+
[OUTPUT_FILE]: {
67+
plugins: ["typescript", "typescript-operations"],
68+
},
69+
},
70+
},
71+
...options,
72+
}),
73+
],
74+
} satisfies UserConfig;
75+
76+
context.viteServer = await createServer(config).then((server) =>
77+
server.listen(),
78+
);
79+
80+
// ensure the watcher is ready before proceeding with tests
81+
await vi.waitFor(() => {
82+
expect(
83+
context.viteServer?.watcher.listeners("change").length,
84+
).toBeGreaterThan(1);
85+
});
86+
};
87+
88+
describe("skip", () => {
89+
afterEach<TestContextWithServer>(async (context) => {
90+
codegenGenerateMock.mockReset();
91+
context.viteServer?.watcher.close();
92+
await context.viteServer?.close();
93+
context.viteServer = null;
94+
95+
await fs.rm(SCHEMA_FILE, { force: true });
96+
await fs.rm(DOCUMENT_PATH, { recursive: true, force: true });
97+
await fs.rm(OUTPUT_PATH, { recursive: true, force: true });
98+
});
99+
100+
describe("on server start", () => {
101+
it.for([
102+
{
103+
describe: "when skip is not set",
104+
skip: undefined,
105+
},
106+
{
107+
describe: "when skip is false",
108+
skip: false,
109+
},
110+
{
111+
describe: "when skip returns false",
112+
skip: () => false,
113+
},
114+
{
115+
describe: "when skip resolves false",
116+
skip: () => Promise.resolve(false),
117+
},
118+
])("it should run codegen $describe", async ({ skip }, context) => {
119+
await startServer({ skip }, context as TestContextWithServer);
120+
121+
expect(codegenGenerateMock).toHaveBeenCalledWith({
122+
pluginContext: {},
123+
schema: SCHEMA_FILE,
124+
documents: `${DOCUMENT_PATH}/**/*.graphql`,
125+
watch: false,
126+
generates: {
127+
[OUTPUT_FILE]: {
128+
plugins: ["typescript", "typescript-operations"],
129+
},
130+
},
131+
});
132+
});
133+
134+
it.for([
135+
{
136+
describe: "when skip is true",
137+
skip: true,
138+
},
139+
{
140+
describe: "when skip returns true",
141+
skip: () => true,
142+
},
143+
{
144+
describe: "when skip resolves true",
145+
skip: () => Promise.resolve(true),
146+
},
147+
])("it should skip codegen $describe", async ({ skip }, context) => {
148+
await startServer({ skip }, context as TestContextWithServer);
149+
150+
expect(codegenGenerateMock.mock.calls).toEqual([]);
151+
});
152+
});
153+
154+
describe("on watch triggered", () => {
155+
it.for([
156+
{
157+
describe: "when skip is not set",
158+
skip: undefined,
159+
},
160+
{
161+
describe: "when skip is false",
162+
skip: false,
163+
},
164+
{
165+
describe: "when skip returns false",
166+
skip: () => false,
167+
},
168+
{
169+
describe: "when skip resolves false",
170+
skip: () => Promise.resolve(false),
171+
},
172+
])("it should run codegen $describe", async ({ skip }, context) => {
173+
await startServer(
174+
{ skip, enableWatcher: true },
175+
context as TestContextWithServer,
176+
);
177+
codegenGenerateMock.mockReset();
178+
179+
await updateQueryFile("query Foo { foo bar }");
180+
181+
expect(codegenGenerateMock).toHaveBeenCalledWith({
182+
pluginContext: {},
183+
schema: SCHEMA_FILE,
184+
documents: `${DOCUMENT_PATH}/**/*.graphql`,
185+
watch: false,
186+
generates: {
187+
[OUTPUT_FILE]: {
188+
plugins: ["typescript", "typescript-operations"],
189+
},
190+
},
191+
});
192+
});
193+
194+
it.for([
195+
{
196+
describe: "when skip is true",
197+
skip: true,
198+
},
199+
{
200+
describe: "when skip returns true",
201+
skip: () => true,
202+
},
203+
{
204+
describe: "when skip resolves true",
205+
skip: () => Promise.resolve(true),
206+
},
207+
])("it should skip codegen $describe", async ({ skip }, context) => {
208+
await startServer(
209+
{ skip, enableWatcher: true },
210+
context as TestContextWithServer,
211+
);
212+
codegenGenerateMock.mockReset();
213+
214+
await updateQueryFile("query Foo { foo bar }");
215+
216+
expect(codegenGenerateMock.mock.calls).toEqual([]);
217+
});
218+
});
219+
});

0 commit comments

Comments
 (0)