Skip to content

Commit 4b22708

Browse files
authored
feat(sveltekit): Read adapter output directory from svelte.config.js (#7863)
Load and read the `svelte.config.js` file. This is necessary to automatically find the output directory that users can specify when setting up the Node adapter. This is a "little" hacky though, because we can't just access the output dir variable. Instead, we actually invoke the adapter (which we can access) and pass a minimal, mostly no-op adapter builder, which will report back the output directory.
1 parent 4c57ca4 commit 4b22708

File tree

6 files changed

+225
-34
lines changed

6 files changed

+225
-34
lines changed

packages/sveltekit/src/vite/sentryVitePlugins.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const DEFAULT_PLUGIN_OPTIONS: SentrySvelteKitPluginOptions = {
3737
* Sentry adds a few additional properties to your Vite config.
3838
* Make sure, it is registered before the SvelteKit plugin.
3939
*/
40-
export function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {}): Plugin[] {
40+
export async function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {}): Promise<Plugin[]> {
4141
const mergedOptions = {
4242
...DEFAULT_PLUGIN_OPTIONS,
4343
...options,
@@ -50,7 +50,7 @@ export function sentrySvelteKit(options: SentrySvelteKitPluginOptions = {}): Plu
5050
...mergedOptions.sourceMapsUploadOptions,
5151
debug: mergedOptions.debug, // override the plugin's debug flag with the one from the top-level options
5252
};
53-
sentryPlugins.push(makeCustomSentryVitePlugin(pluginOptions));
53+
sentryPlugins.push(await makeCustomSentryVitePlugin(pluginOptions));
5454
}
5555

5656
return sentryPlugins;

packages/sveltekit/src/vite/sourceMaps.ts

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,7 @@ import * as path from 'path';
77
import * as sorcery from 'sorcery';
88
import type { Plugin } from 'vite';
99

10-
const DEFAULT_PLUGIN_OPTIONS: SentryVitePluginOptions = {
11-
// TODO: Read these values from the node adapter somehow as the out dir can be changed in the adapter options
12-
include: [
13-
{ paths: ['build/client'] },
14-
{ paths: ['build/server/chunks'] },
15-
{ paths: ['build/server'], ignore: ['chunks/**'] },
16-
],
17-
};
10+
import { getAdapterOutputDir, loadSvelteConfig } from './svelteConfig';
1811

1912
// sorcery has no types, so these are some basic type definitions:
2013
type Chain = {
@@ -45,17 +38,30 @@ type SentryVitePluginOptionsOptionalInclude = Omit<SentryVitePluginOptions, 'inc
4538
*
4639
* @returns the custom Sentry Vite plugin
4740
*/
48-
export function makeCustomSentryVitePlugin(options?: SentryVitePluginOptionsOptionalInclude): Plugin {
41+
export async function makeCustomSentryVitePlugin(options?: SentryVitePluginOptionsOptionalInclude): Promise<Plugin> {
42+
const svelteConfig = await loadSvelteConfig();
43+
44+
const outputDir = await getAdapterOutputDir(svelteConfig);
45+
46+
const defaultPluginOptions: SentryVitePluginOptions = {
47+
include: [
48+
{ paths: [`${outputDir}/client`] },
49+
{ paths: [`${outputDir}/server/chunks`] },
50+
{ paths: [`${outputDir}/server`], ignore: ['chunks/**'] },
51+
],
52+
};
53+
4954
const mergedOptions = {
50-
...DEFAULT_PLUGIN_OPTIONS,
55+
...defaultPluginOptions,
5156
...options,
5257
};
58+
5359
const sentryPlugin: Plugin = sentryVitePlugin(mergedOptions);
5460

5561
const { debug } = mergedOptions;
5662
const { buildStart, resolveId, transform, renderChunk } = sentryPlugin;
5763

58-
let upload = true;
64+
let isSSRBuild = true;
5965

6066
const customPlugin: Plugin = {
6167
name: 'sentry-vite-plugin-custom',
@@ -88,19 +94,19 @@ export function makeCustomSentryVitePlugin(options?: SentryVitePluginOptionsOpti
8894
// `config.build.ssr` is `true` for that first build and `false` in the other ones.
8995
// Hence we can use it as a switch to upload source maps only once in main build.
9096
if (!config.build.ssr) {
91-
upload = false;
97+
isSSRBuild = false;
9298
}
9399
},
94100

95101
// We need to start uploading source maps later than in the original plugin
96-
// because SvelteKit is still doing some stuff at closeBundle.
102+
// because SvelteKit is invoking the adapter at closeBundle.
103+
// This means that we need to wait until the adapter is done before we start uploading.
97104
closeBundle: async () => {
98-
if (!upload) {
105+
if (!isSSRBuild) {
99106
return;
100107
}
101108

102-
// TODO: Read the out dir from the node adapter somehow as it can be changed in the adapter options
103-
const outDir = path.resolve(process.cwd(), 'build');
109+
const outDir = path.resolve(process.cwd(), outputDir);
104110

105111
const jsFiles = getFiles(outDir).filter(file => file.endsWith('.js'));
106112
// eslint-disable-next-line no-console
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/* eslint-disable @sentry-internal/sdk/no-optional-chaining */
2+
3+
import type { Builder, Config } from '@sveltejs/kit';
4+
import * as fs from 'fs';
5+
import * as path from 'path';
6+
import * as url from 'url';
7+
8+
/**
9+
* Imports the svelte.config.js file and returns the config object.
10+
* The sveltekit plugins import the config in the same way.
11+
* See: https://github.com/sveltejs/kit/blob/master/packages/kit/src/core/config/index.js#L63
12+
*/
13+
export async function loadSvelteConfig(): Promise<Config> {
14+
// This can only be .js (see https://github.com/sveltejs/kit/pull/4031#issuecomment-1049475388)
15+
const SVELTE_CONFIG_FILE = 'svelte.config.js';
16+
17+
const configFile = path.join(process.cwd(), SVELTE_CONFIG_FILE);
18+
19+
try {
20+
if (!fs.existsSync(configFile)) {
21+
return {};
22+
}
23+
// @ts-ignore - we explicitly want to import the svelte config here.
24+
const svelteConfigModule = await import(`${url.pathToFileURL(configFile).href}`);
25+
26+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
27+
return (svelteConfigModule?.default as Config) || {};
28+
} catch (e) {
29+
// eslint-disable-next-line no-console
30+
console.warn("[Source Maps Plugin] Couldn't load svelte.config.js:");
31+
// eslint-disable-next-line no-console
32+
console.log(e);
33+
34+
return {};
35+
}
36+
}
37+
38+
/**
39+
* Attempts to read a custom output directory that can be specidied in the options
40+
* of a SvelteKit adapter. If no custom output directory is specified, the default
41+
* directory is returned.
42+
*
43+
* To get the directory, we have to apply a hack and call the adapter's adapt method
44+
* with a custom adapter `Builder` that only calls the `writeClient` method.
45+
* This method is the first method that is called with the output directory.
46+
* Once we obtained the output directory, we throw an error to exit the adapter.
47+
*
48+
* see: https://github.com/sveltejs/kit/blob/master/packages/adapter-node/index.js#L17
49+
*
50+
*/
51+
export async function getAdapterOutputDir(svelteConfig: Config): Promise<string> {
52+
// 'build' is the default output dir for the node adapter
53+
let outputDir = 'build';
54+
55+
if (!svelteConfig.kit?.adapter) {
56+
return outputDir;
57+
}
58+
59+
const adapter = svelteConfig.kit.adapter;
60+
61+
const adapterBuilder: Builder = {
62+
writeClient(dest: string) {
63+
outputDir = dest.replace(/\/client.*/, '');
64+
throw new Error('We got what we came for, throwing to exit the adapter');
65+
},
66+
// @ts-ignore - No need to implement the other methods
67+
log: {
68+
// eslint-disable-next-line @typescript-eslint/no-empty-function -- this should be a noop
69+
minor() {},
70+
},
71+
getBuildDirectory: () => '',
72+
// eslint-disable-next-line @typescript-eslint/no-empty-function -- this should be a noop
73+
rimraf: () => {},
74+
// eslint-disable-next-line @typescript-eslint/no-empty-function -- this should be a noop
75+
mkdirp: () => {},
76+
77+
config: {
78+
kit: {
79+
// @ts-ignore - the builder expects a validated config but for our purpose it's fine to just pass this partial config
80+
paths: {
81+
base: svelteConfig.kit?.paths?.base || '',
82+
},
83+
},
84+
},
85+
};
86+
87+
try {
88+
await adapter.adapt(adapterBuilder);
89+
} catch (_) {
90+
// We expect the adapter to throw in writeClient!
91+
}
92+
93+
return outputDir;
94+
}

packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,26 @@ import { sentrySvelteKit } from '../../src/vite/sentryVitePlugins';
44
import * as sourceMaps from '../../src/vite/sourceMaps';
55

66
describe('sentryVite()', () => {
7-
it('returns an array of Vite plugins', () => {
8-
const plugins = sentrySvelteKit();
7+
it('returns an array of Vite plugins', async () => {
8+
const plugins = await sentrySvelteKit();
99
expect(plugins).toBeInstanceOf(Array);
1010
expect(plugins).toHaveLength(1);
1111
});
1212

13-
it('returns the custom sentry source maps plugin by default', () => {
14-
const plugins = sentrySvelteKit();
13+
it('returns the custom sentry source maps plugin by default', async () => {
14+
const plugins = await sentrySvelteKit();
1515
const plugin = plugins[0];
1616
expect(plugin.name).toEqual('sentry-vite-plugin-custom');
1717
});
1818

19-
it("doesn't return the custom sentry source maps plugin if autoUploadSourcemaps is `false`", () => {
20-
const plugins = sentrySvelteKit({ autoUploadSourceMaps: false });
19+
it("doesn't return the custom sentry source maps plugin if autoUploadSourcemaps is `false`", async () => {
20+
const plugins = await sentrySvelteKit({ autoUploadSourceMaps: false });
2121
expect(plugins).toHaveLength(0);
2222
});
2323

24-
it('passes user-specified vite pugin options to the custom sentry source maps plugin', () => {
24+
it('passes user-specified vite pugin options to the custom sentry source maps plugin', async () => {
2525
const makePluginSpy = vi.spyOn(sourceMaps, 'makeCustomSentryVitePlugin');
26-
const plugins = sentrySvelteKit({
26+
const plugins = await sentrySvelteKit({
2727
debug: true,
2828
sourceMapsUploadOptions: {
2929
include: ['foo.js'],

packages/sveltekit/test/vite/sourceMaps.test.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ beforeEach(() => {
2424
});
2525

2626
describe('makeCustomSentryVitePlugin()', () => {
27-
it('returns the custom sentry source maps plugin', () => {
28-
const plugin = makeCustomSentryVitePlugin();
27+
it('returns the custom sentry source maps plugin', async () => {
28+
const plugin = await makeCustomSentryVitePlugin();
2929
expect(plugin.name).toEqual('sentry-vite-plugin-custom');
3030
expect(plugin.apply).toEqual('build');
3131
expect(plugin.enforce).toEqual('post');
@@ -41,8 +41,8 @@ describe('makeCustomSentryVitePlugin()', () => {
4141
});
4242

4343
describe('Custom sentry vite plugin', () => {
44-
it('enables source map generation', () => {
45-
const plugin = makeCustomSentryVitePlugin();
44+
it('enables source map generation', async () => {
45+
const plugin = await makeCustomSentryVitePlugin();
4646
// @ts-ignore this function exists!
4747
const sentrifiedConfig = plugin.config({ build: { foo: {} }, test: {} });
4848
expect(sentrifiedConfig).toEqual({
@@ -54,17 +54,17 @@ describe('makeCustomSentryVitePlugin()', () => {
5454
});
5555
});
5656

57-
it('uploads source maps during the SSR build', () => {
58-
const plugin = makeCustomSentryVitePlugin();
57+
it('uploads source maps during the SSR build', async () => {
58+
const plugin = await makeCustomSentryVitePlugin();
5959
// @ts-ignore this function exists!
6060
plugin.configResolved({ build: { ssr: true } });
6161
// @ts-ignore this function exists!
6262
plugin.closeBundle();
6363
expect(mockedSentryVitePlugin.writeBundle).toHaveBeenCalledTimes(1);
6464
});
6565

66-
it("doesn't upload source maps during the non-SSR builds", () => {
67-
const plugin = makeCustomSentryVitePlugin();
66+
it("doesn't upload source maps during the non-SSR builds", async () => {
67+
const plugin = await makeCustomSentryVitePlugin();
6868

6969
// @ts-ignore this function exists!
7070
plugin.configResolved({ build: { ssr: false } });
@@ -73,4 +73,26 @@ describe('makeCustomSentryVitePlugin()', () => {
7373
expect(mockedSentryVitePlugin.writeBundle).not.toHaveBeenCalled();
7474
});
7575
});
76+
77+
it('catches errors while uploading source maps', async () => {
78+
mockedSentryVitePlugin.writeBundle.mockImplementationOnce(() => {
79+
throw new Error('test error');
80+
});
81+
82+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
83+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
84+
85+
const plugin = await makeCustomSentryVitePlugin();
86+
87+
// @ts-ignore this function exists!
88+
expect(plugin.closeBundle).not.toThrow();
89+
90+
// @ts-ignore this function exists!
91+
plugin.configResolved({ build: { ssr: true } });
92+
// @ts-ignore this function exists!
93+
plugin.closeBundle();
94+
95+
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to upload source maps'));
96+
expect(consoleLogSpy).toHaveBeenCalled();
97+
});
7698
});
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { vi } from 'vitest';
2+
3+
import { getAdapterOutputDir, loadSvelteConfig } from '../../src/vite/svelteConfig';
4+
5+
let existsFile;
6+
7+
describe('loadSvelteConfig', () => {
8+
vi.mock('fs', () => {
9+
return {
10+
existsSync: () => existsFile,
11+
};
12+
});
13+
14+
vi.mock(`${process.cwd()}/svelte.config.js`, () => {
15+
return {
16+
default: {
17+
kit: {
18+
adapter: {},
19+
},
20+
},
21+
};
22+
});
23+
24+
// url apparently doesn't exist in the test environment, therefore we mock it:
25+
vi.mock('url', () => {
26+
return {
27+
pathToFileURL: path => {
28+
return {
29+
href: path,
30+
};
31+
},
32+
};
33+
});
34+
35+
beforeEach(() => {
36+
existsFile = true;
37+
vi.clearAllMocks();
38+
});
39+
40+
it('returns the svelte config', async () => {
41+
const config = await loadSvelteConfig();
42+
expect(config).toStrictEqual({
43+
kit: {
44+
adapter: {},
45+
},
46+
});
47+
});
48+
49+
it('returns an empty object if svelte.config.js does not exist', async () => {
50+
existsFile = false;
51+
52+
const config = await loadSvelteConfig();
53+
expect(config).toStrictEqual({});
54+
});
55+
});
56+
57+
describe('getAdapterOutputDir', () => {
58+
const mockedAdapter = {
59+
name: 'mocked-adapter',
60+
adapt(builder) {
61+
builder.writeClient('customBuildDir');
62+
},
63+
};
64+
65+
it('returns the output directory of the adapter', async () => {
66+
const outputDir = await getAdapterOutputDir({ kit: { adapter: mockedAdapter } });
67+
expect(outputDir).toEqual('customBuildDir');
68+
});
69+
});

0 commit comments

Comments
 (0)