Skip to content

Commit

Permalink
fix(sourcemaps): Enable source map generation when modifying vite con…
Browse files Browse the repository at this point in the history
…fig (#421)

Fixes an oversight where we previously inserted the vite plugin into the config but didn't enable source map generation.
  • Loading branch information
Lms24 authored Sep 5, 2023
1 parent 45af819 commit c52ef93
Show file tree
Hide file tree
Showing 3 changed files with 284 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ brew install getsentry/tools/sentry-wizard
- fix: Handle no projects available (#412)
- fix: Support org auth tokens in old wizards (#409)
- fix: Treat user-entered DSN as a public DSN (#410)
- fix(sourcemaps): Enable source map generation when modifying Vite config (#421)


## 3.10.0
Expand Down
138 changes: 134 additions & 4 deletions src/sourcemaps/tools/vite.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
// @ts-ignore - clack is ESM and TS complains about that. It works though
import clack, { select } from '@clack/prompts';
import * as clack from '@clack/prompts';
// @ts-ignore - magicast is ESM and TS complains about that. It works though
import { generateCode, parseModule } from 'magicast';
// @ts-ignore - magicast is ESM and TS complains about that. It works though
import { addVitePlugin } from 'magicast/helpers';

import type { namedTypes as t } from 'ast-types';

import * as recast from 'recast';

import * as Sentry from '@sentry/node';

import chalk from 'chalk';
Expand Down Expand Up @@ -139,7 +143,7 @@ async function createNewViteConfig(
}
}

async function addVitePluginToConfig(
export async function addVitePluginToConfig(
viteConfigPath: string,
options: SourceMapUploadToolConfigurationOptions,
): Promise<boolean> {
Expand Down Expand Up @@ -176,6 +180,12 @@ async function addVitePluginToConfig(
}
}

const enabledSourcemaps = enableSourcemapGeneration(mod.$ast as t.Program);
if (!enabledSourcemaps) {
Sentry.setTag('ast-mod-fail-reason', 'insertion-fail');
return false;
}

const { orgSlug: org, projectSlug: project, selfHosted, url } = options;

addVitePlugin(mod, {
Expand All @@ -194,7 +204,7 @@ async function addVitePluginToConfig(
await fs.promises.writeFile(viteConfigPath, code);

clack.log.success(
`Added the Sentry Vite plugin to ${prettyViteConfigFilename}`,
`Added the Sentry Vite plugin to ${prettyViteConfigFilename} and enabled source maps`,
);

return true;
Expand All @@ -218,7 +228,7 @@ async function showCopyPasteInstructions(
console.log(`\n${getViteConfigSnippet(options, true)}`);

await abortIfCancelled(
select({
clack.select({
message: 'Did you copy the snippet above?',
options: [{ label: 'Yes, continue!', value: true }],
initialValue: true,
Expand Down Expand Up @@ -258,3 +268,123 @@ async function askForViteConfigPath(): Promise<string | undefined> {
}),
);
}

function enableSourcemapGeneration(program: t.Program): boolean {
const configObj = getViteConfigObject(program);

if (!configObj) {
return false;
}

const b = recast.types.builders;

const buildProp = configObj.properties.find(
(p: t.ObjectProperty) =>
p.key.type === 'Identifier' && p.key.name === 'build',
);

// case 1: build property doesn't exist yet, so we can just add it
if (!buildProp) {
configObj.properties.push(
b.objectProperty(
b.identifier('build'),
b.objectExpression([
b.objectProperty(b.identifier('sourcemap'), b.booleanLiteral(true)),
]),
),
);
return true;
}

const isValidBuildProp =
buildProp.type === 'ObjectProperty' &&
buildProp.value.type === 'ObjectExpression';

if (!isValidBuildProp) {
return false;
}

const sourceMapsProp =
buildProp.value.type === 'ObjectExpression' &&
buildProp.value.properties.find(
(p: t.ObjectProperty) =>
p.key.type === 'Identifier' && p.key.name === 'sourcemap',
);

// case 2: build.sourcemap property doesn't exist yet, so we just add it
if (!sourceMapsProp && buildProp.value.type === 'ObjectExpression') {
buildProp.value.properties.push(
b.objectProperty(b.identifier('sourcemap'), b.booleanLiteral(true)),
);
return true;
}

if (!sourceMapsProp || sourceMapsProp.type !== 'ObjectProperty') {
return false;
}

// case 3: build.sourcemap property exists, and it's set to 'hidden'
if (
sourceMapsProp.value.type === 'StringLiteral' &&
sourceMapsProp.value.value === 'hidden'
) {
// nothing to do for us
return true;
}

// case 4: build.sourcemap property exists, but it's not enabled, so we set it to true
// or it is already true in which case this is a noop
sourceMapsProp.value = b.booleanLiteral(true);
return true;
}

function getViteConfigObject(
program: t.Program,
): t.ObjectExpression | undefined {
const defaultExport = program.body.find(
(s) => s.type === 'ExportDefaultDeclaration',
) as t.ExportDefaultDeclaration;

if (!defaultExport) {
return undefined;
}

if (defaultExport.declaration.type === 'ObjectExpression') {
return defaultExport.declaration;
}

if (
defaultExport.declaration.type === 'CallExpression' &&
defaultExport.declaration.arguments[0].type === 'ObjectExpression'
) {
return defaultExport.declaration.arguments[0];
}

if (defaultExport.declaration.type === 'Identifier') {
const configId = defaultExport.declaration.name;
return findConfigNode(configId, program);
}

return undefined;
}

function findConfigNode(
configId: string,
program: t.Program,
): t.ObjectExpression | undefined {
for (const node of program.body) {
if (node.type === 'VariableDeclaration') {
for (const declaration of node.declarations) {
if (
declaration.type === 'VariableDeclarator' &&
declaration.id.type === 'Identifier' &&
declaration.id.name === configId &&
declaration.init?.type === 'ObjectExpression'
) {
return declaration.init;
}
}
}
}
return undefined;
}
149 changes: 149 additions & 0 deletions test/sourcemaps/tools/vite.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import * as fs from 'fs';
import { addVitePluginToConfig } from '../../../src/sourcemaps/tools/vite';

function updateFileContent(content: string): void {
fileContent = content;
}

let fileContent = '';

jest.mock('@clack/prompts', () => {
return {
log: {
info: jest.fn(),
success: jest.fn(),
},
};
});

jest
.spyOn(fs.promises, 'readFile')
.mockImplementation(() => Promise.resolve(fileContent));

const writeFileSpy = jest
.spyOn(fs.promises, 'writeFile')
.mockImplementation(() => Promise.resolve(void 0));

describe('addVitePluginToConfig', () => {
afterEach(() => {
fileContent = '';
jest.clearAllMocks();
});

it.each([
[
'no build options',
`
export default defineConfig({
plugins: [
vue(),
],
})
`,
`import { sentryVitePlugin } from "@sentry/vite-plugin";
export default defineConfig({
plugins: [vue(), sentryVitePlugin({
org: "my-org",
project: "my-project"
})],
build: {
sourcemap: true
}
})`,
],
[
'no build.sourcemap options',
`
export default defineConfig({
plugins: [
vue(),
],
build: {
test: 1,
}
})
`,
`import { sentryVitePlugin } from "@sentry/vite-plugin";
export default defineConfig({
plugins: [vue(), sentryVitePlugin({
org: "my-org",
project: "my-project"
})],
build: {
test: 1,
sourcemap: true
}
})`,
],
[
'keep sourcemap: "hidden"',
`
export default {
plugins: [
vue(),
],
build: {
sourcemap: "hidden",
}
}
`,
`import { sentryVitePlugin } from "@sentry/vite-plugin";
export default {
plugins: [vue(), sentryVitePlugin({
org: "my-org",
project: "my-project"
})],
build: {
sourcemap: "hidden",
}
}`,
],
[
'rewrite sourcemap: false to true',
`
const cfg = {
plugins: [
vue(),
],
build: {
sourcemap: false,
}
}
export default cfg;
`,
`import { sentryVitePlugin } from "@sentry/vite-plugin";
const cfg = {
plugins: [vue(), sentryVitePlugin({
org: "my-org",
project: "my-project"
})],
build: {
sourcemap: true,
}
}
export default cfg;`,
],
])(
'adds the plugin and enables source maps generation (%s)',
async (_, originalCode, expectedCode) => {
updateFileContent(originalCode);

const addedCode = await addVitePluginToConfig('', {
authToken: '',
orgSlug: 'my-org',
projectSlug: 'my-project',
selfHosted: false,
url: 'https://sentry.io/',
});

expect(writeFileSpy).toHaveBeenCalledTimes(1);
const [[, fileContent]] = writeFileSpy.mock.calls;
expect(fileContent).toBe(expectedCode);
expect(addedCode).toBe(true);
},
);
});

0 comments on commit c52ef93

Please sign in to comment.