Skip to content

Commit 66b0c70

Browse files
tyfficalclaude
andcommitted
fix: set ssr:true when bundling backend functions
getBaseBackendBuildConfig never set Vite's build.ssr option, so Vite defaulted to a browser-target build. Any *.backend.ts file importing a real Node builtin module (e.g. node:crypto) got that import externalized to a broken __vite-browser-external:* stub instead of a working import, breaking the bundle at build time. Backend functions run server-side, never in a browser, so ssr:true is the correct target. Affects both the existing cloud round-trip and any future local-execution path, since both share this config. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 1d131e2 commit 66b0c70

2 files changed

Lines changed: 92 additions & 0 deletions

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License.
2+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
3+
// Copyright 2019-Present Datadog, Inc.
4+
5+
/**
6+
* Real, unmocked test: getBaseBackendBuildConfig must produce a working
7+
* bundle for a backend function that imports a real Node built-in module
8+
* (e.g. node:crypto), not the browser-target `__vite-browser-external:*`
9+
* externalization stub Vite falls back to when `build.ssr` isn't set.
10+
*
11+
* Discovered while building a local-execution POC: without `ssr: true`,
12+
* Vite defaults to a browser-target build, and any `*.backend.ts` file that
13+
* imports a real Node built-in fails to bundle correctly -- this affects
14+
* dev-server.ts's existing bundleBackendFunction() today (both the current
15+
* cloud round-trip and any future local-execution path), not just new code.
16+
*/
17+
18+
import { outputFileSync } from '@dd/core/helpers/fs';
19+
import { getTempWorkingDir } from '@dd/tests/_jest/helpers/env';
20+
import { build } from 'vite';
21+
22+
import { getBaseBackendBuildConfig } from './build-config';
23+
24+
describe('getBaseBackendBuildConfig', () => {
25+
test('bundles a backend function that imports a real Node builtin module with a working import, not a browser-external stub', async () => {
26+
const workingDir = getTempWorkingDir(`build-config-ssr-${Date.now()}`);
27+
const absolutePath = `${workingDir}/src/usesCrypto.backend.ts`;
28+
29+
outputFileSync(
30+
absolutePath,
31+
`
32+
import { randomBytes } from 'node:crypto';
33+
export async function usesCrypto() {
34+
return randomBytes(4).toString('hex');
35+
}
36+
`,
37+
);
38+
39+
const virtualId = 'virtual:dd-backend-test:usesCrypto';
40+
const virtualContent = `import { usesCrypto } from ${JSON.stringify(absolutePath)};\nexport async function main($) { return await usesCrypto(); }`;
41+
const baseConfig = getBaseBackendBuildConfig(
42+
workingDir,
43+
{ [virtualId]: virtualContent },
44+
[],
45+
);
46+
47+
const result = await build({
48+
...baseConfig,
49+
build: {
50+
...baseConfig.build,
51+
write: false,
52+
rollupOptions: {
53+
...baseConfig.build.rollupOptions,
54+
input: virtualId,
55+
output: baseConfig.build.rollupOptions.output,
56+
},
57+
},
58+
});
59+
60+
const output = Array.isArray(result) ? result[0] : result;
61+
if (!('output' in output)) {
62+
throw new Error('Unexpected vite.build result');
63+
}
64+
const chunk = output.output[0];
65+
const code = chunk.type === 'chunk' ? chunk.code : '';
66+
67+
// Without `ssr: true`, Vite externalizes node:crypto to
68+
// `__vite-browser-external:node:crypto`, which has no real exports --
69+
// calling randomBytes() from it throws at runtime, and the import
70+
// specifier itself is rewritten away from 'node:crypto'. Assert the
71+
// real, working import survived instead.
72+
expect(code).toContain("from 'node:crypto'");
73+
expect(code).not.toContain('__vite-browser-external');
74+
});
75+
});

packages/plugins/apps/src/vite/build-config.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ export function getBaseBackendBuildConfig(
4848
build: {
4949
minify: false,
5050
target: 'esnext',
51+
// Backend functions run server-side, never in a browser. Without
52+
// this, Vite defaults to a browser-target build and externalizes
53+
// real Node builtin imports (node:crypto, fs, etc.) to a
54+
// `__vite-browser-external:*` stub with no real exports, silently
55+
// breaking any backend function that imports one directly.
56+
ssr: true,
5157
rollupOptions: {
5258
output: { format: 'es', exports: 'named', inlineDynamicImports: true },
5359
preserveEntrySignatures: 'exports-only',
@@ -63,6 +69,17 @@ export function getBaseBackendBuildConfig(
6369
resolve: {
6470
extensions: [...BACKEND_CODE_EXTENSIONS, '.json'],
6571
},
72+
// Vite's SSR build mode (enabled above) externalizes any real npm
73+
// dependency it finds in node_modules by default, on the assumption
74+
// a server runtime can `require()` it at runtime. Backend-function
75+
// bundles don't get that guarantee: dev-server.ts writes them to a
76+
// standalone temp file, and the local-execution path imports the
77+
// bundle from a data: URL with no filesystem context at all -- so
78+
// every real dependency must be inlined, matching the pre-ssr:true
79+
// browser-mode behavior this config otherwise replaces.
80+
ssr: {
81+
noExternal: true,
82+
},
6683
plugins: [createVirtualPlugin('dd-backend-resolve', virtualEntries), ...plugins],
6784
};
6885
}

0 commit comments

Comments
 (0)