Skip to content

Commit a730612

Browse files
authored
feat: Add runtime skip property to conditionally skip codegen (#41)
* feat: Add skip option * test: use not.toHaveBeenCalled assertion
1 parent 6f59e77 commit a730612

3 files changed

Lines changed: 279 additions & 8 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: 36 additions & 8 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

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

142+
const shouldSkipGeneration = async (context: SkipContext) =>
143+
typeof skip === "function" ? await skip(context) : skip;
144+
128145
const generateWithOverride = async (
129146
overrideConfig: Partial<CodegenConfig>,
147+
skipContext: SkipContext,
130148
) => {
131-
const currentConfig = codegenContext.getConfig();
149+
if (await shouldSkipGeneration(skipContext)) {
150+
log("Generation skipped", skipContext);
151+
return;
152+
}
132153

133-
return await generate({
154+
const currentConfig = codegenContext.getConfig();
155+
await generate({
134156
...currentConfig,
135157
...configOverride,
136158
...overrideConfig,
137159
// Vite handles file watching
138160
watch: false,
139161
});
162+
163+
log(`Generation successful on ${skipContext.trigger}`);
140164
};
141165

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

169193
try {
170-
await generateWithOverride(configOverrideOnStart);
171-
log("Generation successful on start");
194+
await generateWithOverride(configOverrideOnStart, {
195+
trigger: "start",
196+
});
172197
} catch (error) {
173198
// GraphQL Codegen handles logging useful errors
174199
log("Generation failed on start");
@@ -180,8 +205,9 @@ export function GraphQLCodegen(options?: Options): Plugin {
180205
if (!runOnBuild) return;
181206

182207
try {
183-
await generateWithOverride(configOverrideOnBuild);
184-
log("Generation successful on build");
208+
await generateWithOverride(configOverrideOnBuild, {
209+
trigger: "build",
210+
});
185211
} catch (error) {
186212
// GraphQL Codegen handles logging useful errors
187213
log("Generation failed on build");
@@ -204,8 +230,10 @@ export function GraphQLCodegen(options?: Options): Plugin {
204230
log("File is in match cache");
205231

206232
try {
207-
await generateWithOverride(configOverrideWatcher);
208-
log("Generation successful in file watcher");
233+
await generateWithOverride(configOverrideWatcher, {
234+
trigger: "watch",
235+
filePath,
236+
});
209237
} catch {
210238
// GraphQL Codegen handles logging useful errors
211239
log("Generation failed in file watcher");

test/skip/skip.spec.ts

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
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, type SkipContext } 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 by matching start trigger",
112+
skip: ({ trigger }: SkipContext) => trigger !== "start",
113+
},
114+
{
115+
describe: "when skip resolves false by matching start trigger",
116+
skip: ({ trigger }: SkipContext) =>
117+
Promise.resolve(trigger !== "start"),
118+
},
119+
])("it should run codegen $describe", async ({ skip }, context) => {
120+
await startServer({ skip }, context as TestContextWithServer);
121+
122+
expect(codegenGenerateMock).toHaveBeenCalledWith({
123+
pluginContext: {},
124+
schema: SCHEMA_FILE,
125+
documents: `${DOCUMENT_PATH}/**/*.graphql`,
126+
watch: false,
127+
generates: {
128+
[OUTPUT_FILE]: {
129+
plugins: ["typescript", "typescript-operations"],
130+
},
131+
},
132+
});
133+
});
134+
135+
it.for([
136+
{
137+
describe: "when skip is true",
138+
skip: true,
139+
},
140+
{
141+
describe: "when skip returns true by matching start trigger",
142+
skip: ({ trigger }: SkipContext) => trigger === "start",
143+
},
144+
{
145+
describe: "when skip resolves true by matching start trigger",
146+
skip: ({ trigger }: SkipContext) =>
147+
Promise.resolve(trigger === "start"),
148+
},
149+
])("it should skip codegen $describe", async ({ skip }, context) => {
150+
await startServer({ skip }, context as TestContextWithServer);
151+
152+
expect(codegenGenerateMock).not.toHaveBeenCalled();
153+
});
154+
});
155+
156+
describe("on watch triggered", () => {
157+
it.for([
158+
{
159+
describe: "when skip is not set",
160+
skip: undefined,
161+
},
162+
{
163+
describe: "when skip is false",
164+
skip: false,
165+
},
166+
{
167+
describe: "when skip returns false by matching watch trigger",
168+
skip: ({ trigger }: SkipContext) => trigger !== "watch",
169+
},
170+
{
171+
describe: "when skip resolves false by matching watch trigger",
172+
skip: ({ trigger }: SkipContext) =>
173+
Promise.resolve(trigger !== "watch"),
174+
},
175+
{
176+
describe: "when skip resolves false by matching filePath",
177+
skip: async ({ filePath }: SkipContext) =>
178+
filePath !== (await fs.realpath(QUERY_FILE)),
179+
},
180+
])("it should run codegen $describe", async ({ skip }, context) => {
181+
await startServer(
182+
{ skip, enableWatcher: true },
183+
context as TestContextWithServer,
184+
);
185+
codegenGenerateMock.mockReset();
186+
187+
await updateQueryFile("query Foo { foo bar }");
188+
189+
expect(codegenGenerateMock).toHaveBeenCalledWith({
190+
pluginContext: {},
191+
schema: SCHEMA_FILE,
192+
documents: `${DOCUMENT_PATH}/**/*.graphql`,
193+
watch: false,
194+
generates: {
195+
[OUTPUT_FILE]: {
196+
plugins: ["typescript", "typescript-operations"],
197+
},
198+
},
199+
});
200+
});
201+
202+
it.for([
203+
{
204+
describe: "when skip is true",
205+
skip: true,
206+
},
207+
{
208+
describe: "when skip returns true by matching watch trigger",
209+
skip: ({ trigger }: SkipContext) => trigger === "watch",
210+
},
211+
{
212+
describe: "when skip resolves true by matching watch trigger",
213+
skip: ({ trigger }: SkipContext) =>
214+
Promise.resolve(trigger === "watch"),
215+
},
216+
{
217+
describe: "when skip resolves true by matching filePath",
218+
skip: async ({ filePath }: SkipContext) =>
219+
filePath === (await fs.realpath(QUERY_FILE)),
220+
},
221+
])("it should skip codegen $describe", async ({ skip }, context) => {
222+
await startServer(
223+
{ skip, enableWatcher: true },
224+
context as TestContextWithServer,
225+
);
226+
codegenGenerateMock.mockReset();
227+
228+
await updateQueryFile("query Foo { foo bar }");
229+
230+
expect(codegenGenerateMock).not.toHaveBeenCalled();
231+
});
232+
});
233+
});

0 commit comments

Comments
 (0)