Skip to content

Commit 0015911

Browse files
committed
ref: Cleanup wranglerConfig
1 parent ae2c06a commit 0015911

2 files changed

Lines changed: 133 additions & 55 deletions

File tree

Lines changed: 18 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { existsSync } from 'node:fs';
2-
import { dirname, resolve } from 'node:path';
1+
import { resolve } from 'node:path';
32
import { type Unstable_Config, unstable_readConfig } from 'wrangler';
43

54
/**
@@ -30,38 +29,28 @@ export interface WranglerConfig {
3029
export function resolveWranglerConfig(
3130
root: string,
3231
explicitPath?: string,
33-
): { config: WranglerConfig; configDir: string } | undefined {
34-
const configPath = explicitPath
35-
? resolve(root, explicitPath)
36-
: ['wrangler.json', 'wrangler.jsonc', 'wrangler.toml'].map(name => resolve(root, name)).find(existsSync);
32+
): WranglerConfig | undefined {
33+
const configPath = explicitPath ? resolve(root, explicitPath) : undefined;
3734

38-
if (!configPath || !existsSync(configPath)) {
39-
return undefined;
40-
}
41-
42-
let raw: Unstable_Config;
4335
try {
4436
// `hideWarnings` keeps wrangler's config diagnostics (e.g. missing DO
4537
// migrations) out of the Vite build output.
46-
raw = unstable_readConfig({ config: configPath }, { hideWarnings: true });
47-
} catch {
48-
return undefined;
49-
}
38+
const raw = unstable_readConfig({ config: configPath }, { hideWarnings: true }) as Unstable_Config;
5039

51-
const durableObjects: WranglerConfig['durableObjects'] = [];
52-
const seenClassNames = new Set<string>();
53-
for (const binding of raw.durable_objects?.bindings ?? []) {
54-
// `script_name` bindings reference a class exported by a *different* worker
55-
// — there is nothing to wrap in this worker's entry file.
56-
if (typeof binding?.class_name !== 'string' || binding.script_name || seenClassNames.has(binding.class_name)) {
57-
continue;
40+
const durableObjects: WranglerConfig['durableObjects'] = [];
41+
const seenClassNames = new Set<string>();
42+
for (const binding of raw.durable_objects?.bindings ?? []) {
43+
// `script_name` bindings reference a class exported by a *different* worker
44+
// — there is nothing to wrap in this worker's entry file.
45+
if (typeof binding?.class_name !== 'string' || binding.script_name || seenClassNames.has(binding.class_name)) {
46+
continue;
47+
}
48+
seenClassNames.add(binding.class_name);
49+
durableObjects.push({ name: binding.name, className: binding.class_name });
5850
}
59-
seenClassNames.add(binding.class_name);
60-
durableObjects.push({ name: binding.name, className: binding.class_name });
61-
}
6251

63-
return {
64-
config: { main: raw.main, durableObjects },
65-
configDir: dirname(raw.configPath ?? configPath),
66-
};
52+
return { main: raw.main, durableObjects };
53+
} catch {
54+
return undefined;
55+
}
6756
}

packages/cloudflare/test/vite/wranglerConfig.test.ts

Lines changed: 115 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,30 @@
1-
import { mkdtempSync, writeFileSync } from 'node:fs';
1+
import { mkdtempSync, realpathSync, writeFileSync } from 'node:fs';
22
import { tmpdir } from 'node:os';
33
import { join } from 'node:path';
4-
import { describe, expect, it } from 'vitest';
4+
import { afterEach, describe, expect, it } from 'vitest';
5+
import { unstable_readConfig } from 'wrangler';
56
import { resolveWranglerConfig } from '../../src/vite/wranglerConfig';
67

8+
// `resolveWranglerConfig` locates the config via wrangler's own discovery from
9+
// `process.cwd()` (unless given an explicit path), so each test writes its
10+
// fixtures to a temp dir and switches the cwd into it. `realpathSync` collapses
11+
// the macOS `/var` → `/private/var` symlink so path assertions match what
12+
// wrangler resolves against the (real) cwd.
713
function writeTempDir(files: Record<string, string>): string {
8-
const dir = mkdtempSync(join(tmpdir(), 'sentry-cf-'));
14+
const dir = realpathSync(mkdtempSync(join(tmpdir(), 'sentry-cf-')));
915
for (const [name, content] of Object.entries(files)) {
1016
writeFileSync(join(dir, name), content);
1117
}
18+
process.chdir(dir);
1219
return dir;
1320
}
1421

1522
describe('resolveWranglerConfig', () => {
23+
const originalCwd = process.cwd();
24+
afterEach(() => {
25+
process.chdir(originalCwd);
26+
});
27+
1628
it('parses wrangler.toml', () => {
1729
const dir = writeTempDir({
1830
'wrangler.toml': [
@@ -27,8 +39,8 @@ describe('resolveWranglerConfig', () => {
2739
const result = resolveWranglerConfig(dir);
2840
expect(result).toBeDefined();
2941
// wrangler resolves `main` to an absolute path against the config dir.
30-
expect(result!.config.main).toBe(join(dir, 'src/index.ts'));
31-
expect(result!.config.durableObjects).toEqual([{ name: 'MY_DO', className: 'MyDurableObject' }]);
42+
expect(result!.main).toBe(join(dir, 'src/index.ts'));
43+
expect(result!.durableObjects).toEqual([{ name: 'MY_DO', className: 'MyDurableObject' }]);
3244
});
3345

3446
it('parses wrangler.json', () => {
@@ -43,8 +55,8 @@ describe('resolveWranglerConfig', () => {
4355

4456
const result = resolveWranglerConfig(dir);
4557
expect(result).toBeDefined();
46-
expect(result!.config.main).toBe(join(dir, 'src/worker.ts'));
47-
expect(result!.config.durableObjects).toEqual([{ name: 'DO_A', className: 'A' }]);
58+
expect(result!.main).toBe(join(dir, 'src/worker.ts'));
59+
expect(result!.durableObjects).toEqual([{ name: 'DO_A', className: 'A' }]);
4860
});
4961

5062
it('parses wrangler.jsonc (strips comments)', () => {
@@ -65,8 +77,8 @@ describe('resolveWranglerConfig', () => {
6577

6678
const result = resolveWranglerConfig(dir);
6779
expect(result).toBeDefined();
68-
expect(result!.config.main).toBe(join(dir, 'src/index.ts'));
69-
expect(result!.config.durableObjects).toEqual([{ name: 'DO', className: 'MyDO' }]);
80+
expect(result!.main).toBe(join(dir, 'src/index.ts'));
81+
expect(result!.durableObjects).toEqual([{ name: 'DO', className: 'MyDO' }]);
7082
});
7183

7284
it('parses JSONC with trailing commas', () => {
@@ -84,15 +96,15 @@ describe('resolveWranglerConfig', () => {
8496
});
8597

8698
const result = resolveWranglerConfig(dir);
87-
expect(result!.config.main).toBe(join(dir, 'src/index.ts'));
88-
expect(result!.config.durableObjects).toEqual([{ name: 'DO', className: 'MyDO' }]);
99+
expect(result!.main).toBe(join(dir, 'src/index.ts'));
100+
expect(result!.durableObjects).toEqual([{ name: 'DO', className: 'MyDO' }]);
89101
});
90102

91103
it('parses TOML single-quoted (literal) strings', () => {
92104
const dir = writeTempDir({ 'wrangler.toml': "main = 'src/index.ts'" });
93105

94106
const result = resolveWranglerConfig(dir);
95-
expect(result!.config.main).toBe(join(dir, 'src/index.ts'));
107+
expect(result!.main).toBe(join(dir, 'src/index.ts'));
96108
});
97109

98110
it('prefers wrangler.json over wrangler.toml (matching wrangler itself)', () => {
@@ -102,7 +114,7 @@ describe('resolveWranglerConfig', () => {
102114
});
103115

104116
const result = resolveWranglerConfig(dir);
105-
expect(result!.config.main).toBe(join(dir, 'from-json.ts'));
117+
expect(result!.main).toBe(join(dir, 'from-json.ts'));
106118
});
107119

108120
it('prefers wrangler.jsonc over wrangler.toml (matching wrangler itself)', () => {
@@ -112,7 +124,7 @@ describe('resolveWranglerConfig', () => {
112124
});
113125

114126
const result = resolveWranglerConfig(dir);
115-
expect(result!.config.main).toBe(join(dir, 'from-jsonc.ts'));
127+
expect(result!.main).toBe(join(dir, 'from-jsonc.ts'));
116128
});
117129

118130
it('handles TOML with commented-out bindings', () => {
@@ -131,7 +143,7 @@ describe('resolveWranglerConfig', () => {
131143
});
132144

133145
const result = resolveWranglerConfig(dir);
134-
expect(result!.config.durableObjects).toEqual([{ name: 'REAL', className: 'RealDO' }]);
146+
expect(result!.durableObjects).toEqual([{ name: 'REAL', className: 'RealDO' }]);
135147
});
136148

137149
it('handles multiple DO bindings', () => {
@@ -150,14 +162,16 @@ describe('resolveWranglerConfig', () => {
150162
});
151163

152164
const result = resolveWranglerConfig(dir);
153-
expect(result!.config.durableObjects).toHaveLength(2);
154-
expect(result!.config.durableObjects[0]).toEqual({ name: 'DO_A', className: 'A' });
155-
expect(result!.config.durableObjects[1]).toEqual({ name: 'DO_B', className: 'B' });
165+
expect(result!.durableObjects).toHaveLength(2);
166+
expect(result!.durableObjects[0]).toEqual({ name: 'DO_A', className: 'A' });
167+
expect(result!.durableObjects[1]).toEqual({ name: 'DO_B', className: 'B' });
156168
});
157169

158-
it('returns undefined when no config exists', () => {
170+
it('returns an empty config when no config file is found', () => {
171+
// With nothing to discover, wrangler yields a default (empty) config rather
172+
// than throwing, so the result is defined but carries no entry or bindings.
159173
const dir = writeTempDir({});
160-
expect(resolveWranglerConfig(dir)).toBeUndefined();
174+
expect(resolveWranglerConfig(dir)).toEqual({ main: undefined, durableObjects: [] });
161175
});
162176

163177
it('returns undefined for explicit non-existent path', () => {
@@ -169,8 +183,7 @@ describe('resolveWranglerConfig', () => {
169183

170184
const result = resolveWranglerConfig(dir, 'custom.toml');
171185
expect(result).toBeDefined();
172-
expect(result!.config.main).toBe(join(dir, 'src/index.ts'));
173-
expect(result!.configDir).toBe(dir);
186+
expect(result!.main).toBe(join(dir, 'src/index.ts'));
174187
});
175188

176189
it('returns undefined for an empty config file instead of crashing', () => {
@@ -197,7 +210,7 @@ describe('resolveWranglerConfig', () => {
197210
});
198211

199212
const result = resolveWranglerConfig(dir);
200-
expect(result!.config.durableObjects).toEqual([{ name: 'LOCAL', className: 'LocalDO' }]);
213+
expect(result!.durableObjects).toEqual([{ name: 'LOCAL', className: 'LocalDO' }]);
201214
});
202215

203216
it('uses only the active environment DO bindings (does not union across envs)', () => {
@@ -219,7 +232,7 @@ describe('resolveWranglerConfig', () => {
219232
});
220233

221234
const result = resolveWranglerConfig(dir);
222-
expect(result!.config.durableObjects).toEqual([{ name: 'TOP', className: 'TopDO' }]);
235+
expect(result!.durableObjects).toEqual([{ name: 'TOP', className: 'TopDO' }]);
223236
});
224237

225238
it('honors CLOUDFLARE_ENV for both main and DO bindings', () => {
@@ -240,11 +253,87 @@ describe('resolveWranglerConfig', () => {
240253
process.env.CLOUDFLARE_ENV = 'staging';
241254
try {
242255
const result = resolveWranglerConfig(dir)!;
243-
expect(result.config.main).toBe(join(dir, 'src/staging.ts'));
244-
expect(result.config.durableObjects).toEqual([{ name: 'STAGING_DO', className: 'StagingDO' }]);
256+
expect(result.main).toBe(join(dir, 'src/staging.ts'));
257+
expect(result.durableObjects).toEqual([{ name: 'STAGING_DO', className: 'StagingDO' }]);
245258
} finally {
246259
if (previous === undefined) delete process.env.CLOUDFLARE_ENV;
247260
else process.env.CLOUDFLARE_ENV = previous;
248261
}
249262
});
250263
});
264+
265+
// ---------------------------------------------------------------------------
266+
// What `unstable_readConfig` exposes about service-binding `entrypoint`.
267+
//
268+
// These characterize the wrangler API directly (not our wrapper) to justify a
269+
// design decision: a service binding's `entrypoint` names a *named export on
270+
// the target worker being bound to*, not an entrypoint this worker exposes.
271+
// So it cannot, in general, tell auto-wrap which of *this* worker's exports is
272+
// a handler — with one exception: a self-binding (`service === own name`).
273+
// ---------------------------------------------------------------------------
274+
275+
describe('unstable_readConfig: service-binding entrypoint semantics', () => {
276+
function readConfig(files: Record<string, string>) {
277+
const dir = writeTempDir(files);
278+
return unstable_readConfig({ config: join(dir, Object.keys(files)[0]!) }, { hideWarnings: true });
279+
}
280+
281+
it('resolves `main` to an absolute path', () => {
282+
const raw = readConfig({
283+
'wrangler.json': JSON.stringify({ main: 'src/index.ts', compatibility_date: '2024-01-01' }),
284+
});
285+
// Not the literal `src/index.ts` from the file — wrangler resolves it.
286+
expect(raw.main).not.toBe('src/index.ts');
287+
expect(raw.main?.endsWith(join('src', 'index.ts'))).toBe(true);
288+
});
289+
290+
it("an outward service binding names the *target* worker's export, not ours", () => {
291+
const raw = readConfig({
292+
'wrangler.json': JSON.stringify({
293+
name: 'worker-a',
294+
main: 'src/index.ts',
295+
compatibility_date: '2024-01-01',
296+
services: [{ binding: 'MY_SVC', service: 'worker-b', entrypoint: 'SomeEntry' }],
297+
}),
298+
});
299+
300+
expect(raw.name).toBe('worker-a');
301+
// `entrypoint` belongs to `worker-b`, a different worker this build isn't
302+
// compiling — nothing in *our* entry file to wrap from this.
303+
expect(raw.services).toEqual([{ binding: 'MY_SVC', service: 'worker-b', entrypoint: 'SomeEntry' }]);
304+
expect(raw.services?.[0]?.service).not.toBe(raw.name);
305+
});
306+
307+
it('a self-binding (service === own name) does name one of *our* exports', () => {
308+
const raw = readConfig({
309+
'wrangler.json': JSON.stringify({
310+
name: 'worker-self',
311+
main: 'src/index.ts',
312+
compatibility_date: '2024-01-01',
313+
services: [
314+
{ binding: 'SELF', service: 'worker-self', entrypoint: 'InternalEntry' },
315+
{ binding: 'OTHER', service: 'worker-x', entrypoint: 'RemoteEntry' },
316+
],
317+
}),
318+
});
319+
320+
// Only the self-bound entrypoint is ours; the other points at `worker-x`.
321+
const ownEntrypoints = (raw.services ?? []).filter(s => s.service === raw.name).map(s => s.entrypoint);
322+
expect(ownEntrypoints).toEqual(['InternalEntry']);
323+
});
324+
325+
it('leaves `name` undefined when the config omits it (no self-binding is derivable)', () => {
326+
const raw = readConfig({
327+
'wrangler.json': JSON.stringify({
328+
main: 'src/index.ts',
329+
compatibility_date: '2024-01-01',
330+
services: [{ binding: 'S', service: 'x', entrypoint: 'E' }],
331+
}),
332+
});
333+
334+
// Without a worker name there is no `service === name` to match against, so
335+
// even self-bindings can't be identified.
336+
expect(raw.name).toBeUndefined();
337+
expect(raw.topLevelName).toBeUndefined();
338+
});
339+
});

0 commit comments

Comments
 (0)