Skip to content

Commit 77e79b2

Browse files
authored
Restore configuration field documentation in define* helpers (#15880)
1 parent 8d61ca8 commit 77e79b2

4 files changed

Lines changed: 162 additions & 1 deletion

File tree

‎.changeset/warm-cats-explain.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@cloudflare/config": patch
3+
---
4+
5+
Restore configuration field documentation in `define*` helpers
6+
7+
VS Code now displays JSDoc for fields passed to `defineConfig`, `defineContainer`, and `defineWorker` while retaining their exact inferred config types.

‎packages/config/src/__tests__/config-definition.test-d.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,20 @@ const projectConfig = defineConfig({
2828
}),
2929
});
3030

31+
const inlineConfig = defineConfig({
32+
worker: {
33+
name: "inline-worker",
34+
compatibilityDate: "2026-09-25",
35+
env: { MESSAGE: bindings.text("inline") },
36+
},
37+
containers: [
38+
{
39+
name: "inline-container",
40+
image: { dockerfile: "./Dockerfile" },
41+
},
42+
],
43+
});
44+
3145
// @ts-expect-error a workflow export requires a name
3246
workerExports.workflow({ limits: { steps: 10 } });
3347

@@ -41,6 +55,8 @@ const plainConfig = {
4155
} as const;
4256

4357
type ProjectWorker = UnwrapConfig<UnwrapConfig<typeof projectConfig>["worker"]>;
58+
type InlineConfig = UnwrapConfig<typeof inlineConfig>;
59+
type InlineWorker = UnwrapConfig<InlineConfig["worker"]>;
4460

4561
export type PlainConfigWorkerEnvTest = Assert<
4662
Equal<
@@ -57,3 +73,12 @@ export type ProjectConfigMainModuleTest = Assert<
5773
export type ProjectConfigDurableNamespaceTest = Assert<
5874
Equal<InferDurableNamespaces<ProjectWorker>, "Counter">
5975
>;
76+
export type InlineConfigWorkerNameTest = Assert<
77+
Equal<InlineWorker["name"], "inline-worker">
78+
>;
79+
export type InlineConfigWorkerEnvTest = Assert<
80+
Equal<InferEnv<InlineWorker>["MESSAGE"], "inline">
81+
>;
82+
export type InlineConfigContainerNameTest = Assert<
83+
Equal<InlineConfig["containers"][0]["name"], "inline-container">
84+
>;
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { fileURLToPath } from "node:url";
2+
import ts from "typescript";
3+
import { it } from "vitest";
4+
5+
const source = `
6+
import { defineConfig, defineContainer, defineWorker } from "../definition";
7+
8+
defineConfig({
9+
worker: {
10+
name: "inline-worker",
11+
compatibilityDate: "2026-09-25",
12+
},
13+
containers: [
14+
{
15+
name: "inline-container",
16+
image: { dockerfile: "./inline.Dockerfile" },
17+
},
18+
],
19+
});
20+
21+
defineWorker({
22+
name: "standalone-worker",
23+
compatibilityDate: "2026-09-25",
24+
});
25+
26+
defineContainer({
27+
name: "standalone-container",
28+
image: { dockerfile: "./Dockerfile" },
29+
});
30+
`;
31+
const testFilePath = fileURLToPath(
32+
new URL("inline-worker-jsdoc.ts", import.meta.url)
33+
);
34+
const canonicalize = (fileName: string) =>
35+
ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase();
36+
const canonicalTestFilePath = canonicalize(ts.sys.resolvePath(testFilePath));
37+
38+
it("preserves JSDoc for define helper fields", ({ expect }) => {
39+
const host: ts.LanguageServiceHost = {
40+
...ts.sys,
41+
getCompilationSettings: () => ({
42+
module: ts.ModuleKind.ESNext,
43+
moduleResolution: ts.ModuleResolutionKind.Bundler,
44+
strict: true,
45+
target: ts.ScriptTarget.ESNext,
46+
}),
47+
getDefaultLibFileName: ts.getDefaultLibFilePath,
48+
getScriptFileNames: () => [testFilePath],
49+
getScriptSnapshot: (fileName) => {
50+
const contents =
51+
canonicalize(ts.sys.resolvePath(fileName)) === canonicalTestFilePath
52+
? source
53+
: ts.sys.readFile(fileName);
54+
return contents === undefined
55+
? undefined
56+
: ts.ScriptSnapshot.fromString(contents);
57+
},
58+
getScriptVersion: () => "0",
59+
useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
60+
};
61+
const languageService = ts.createLanguageService(host);
62+
63+
try {
64+
expect(languageService.getSemanticDiagnostics(testFilePath)).toEqual([]);
65+
66+
for (const [marker, expectedDocumentation] of [
67+
['name: "inline-worker"', "The name of your Worker."],
68+
[
69+
'compatibilityDate: "2026-09-25"',
70+
"which version of the Workers runtime is used.",
71+
],
72+
['name: "inline-container"', "Name of the application."],
73+
[
74+
'image: { dockerfile: "./inline.Dockerfile" }',
75+
"The image to build or deploy.",
76+
],
77+
['dockerfile: "./inline.Dockerfile"', "The path to a Dockerfile."],
78+
['name: "standalone-worker"', "The name of your Worker."],
79+
['name: "standalone-container"', "Name of the application."],
80+
[
81+
'image: { dockerfile: "./Dockerfile" }',
82+
"The image to build or deploy.",
83+
],
84+
['dockerfile: "./Dockerfile"', "The path to a Dockerfile."],
85+
] as const) {
86+
const info = languageService.getQuickInfoAtPosition(
87+
testFilePath,
88+
source.indexOf(marker)
89+
);
90+
expect(ts.displayPartsToString(info?.documentation), marker).toContain(
91+
expectedDocumentation
92+
);
93+
}
94+
} finally {
95+
languageService.dispose();
96+
}
97+
});

‎packages/config/src/definition.ts‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,42 @@ export type ConfigInput<T> =
2323
| Promise<T>
2424
| ((ctx: ConfigContext) => T | Promise<T>);
2525

26+
/** Recursively apply declared config properties without expanding open records. */
27+
type ContextualProperties<
28+
TInput extends object,
29+
TConfig,
30+
> = TInput extends readonly unknown[]
31+
? TConfig extends readonly (infer TElement)[]
32+
? {
33+
[K in keyof TInput]: ContextualConfig<TInput[K], TElement>;
34+
}
35+
: TConfig
36+
: string extends keyof TConfig
37+
? TConfig
38+
: {
39+
[K in keyof TConfig]: K extends keyof TInput
40+
? ContextualConfig<TInput[K], TConfig[K]>
41+
: TConfig[K];
42+
};
43+
44+
type ContextualConfig<TInput, TConfig> =
45+
TInput extends Promise<infer TValue>
46+
? Promise<ContextualConfig<TValue, TConfig>>
47+
: TInput extends (ctx: ConfigContext) => infer TResult
48+
? (ctx: ConfigContext) => ContextualConfig<TResult, TConfig>
49+
: TConfig extends unknown
50+
? TInput extends TConfig
51+
? TInput &
52+
(TInput extends object
53+
? ContextualProperties<TInput, TConfig>
54+
: TConfig)
55+
: never
56+
: never;
57+
2658
/** Create a type-safe identity helper for a configuration value or factory. */
2759
export function createConfigDefiner<TConfig>() {
2860
return function define<const TInput extends ConfigInput<TConfig>>(
29-
config: TInput
61+
config: TInput & ContextualConfig<TInput, TConfig>
3062
): TInput {
3163
return config;
3264
};

0 commit comments

Comments
 (0)