Skip to content
This repository has been archived by the owner on Dec 12, 2024. It is now read-only.

Commit

Permalink
feat: add Svelte Config rewrite transformer
Browse files Browse the repository at this point in the history
  • Loading branch information
Lms24 committed Aug 25, 2023
1 parent 578be66 commit dfa126e
Show file tree
Hide file tree
Showing 8 changed files with 431 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Changelog

- feat: Add Svelte Config rewrite transformer
- feat: Add telemetry data collection
- fix: Fix incorrect deduping of named to namespace imports

Expand Down
27 changes: 27 additions & 0 deletions src/transformers/rewriteSvelteConfig/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import path from 'node:path';
import url from 'url';

import { runJscodeshift } from '../../utils/jscodeshift.js';

/** @type {import('types').Transformer} */
export default {
name: 'Rewrite Svelte Config',
async transform(files, options) {
if (!options.sdk || !['@sentry/svelte', '@sentry/sveltekit'].includes(options.sdk)) {
// No need to run this transformer if the SDK is not a Svelte SDK (or not specified at all)
return;
}

// The only file this transformer touches svelte.config.js.
// Using filter for the remote chance that there's more than one such a file (monorepos?)
const svelteConfigs = files.filter(file => path.basename(file) === 'svelte.config.js');

if (!svelteConfigs.length) {
return;
}

const transformPath = path.join(path.dirname(url.fileURLToPath(import.meta.url)), './transform.cjs');

await runJscodeshift(transformPath, svelteConfigs, options);
},
};
172 changes: 172 additions & 0 deletions src/transformers/rewriteSvelteConfig/rewriteSvelteConfig.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { afterEach, describe, it } from 'node:test';
import * as assert from 'node:assert';
import { rmSync } from 'node:fs';

import { getDirFileContent, getFixturePath, makeTmpDir } from '../../../test-helpers/testPaths.js';
import { assertStringEquals } from '../../../test-helpers/assert.js';

import rewriteSvelteConfig from './index.js';

describe('transformers | rewriteSvelteConfig', () => {
let tmpDir = '';

afterEach(() => {
if (tmpDir) {
rmSync(tmpDir, { force: true, recursive: true });
tmpDir = '';
}
});

it('has correct name', () => {
assert.equal(rewriteSvelteConfig.name, 'Rewrite Svelte Config');
});

it('no-ops with app without Sentry', async () => {
tmpDir = makeTmpDir(getFixturePath('noSentry'));
await rewriteSvelteConfig.transform([tmpDir], { filePatterns: [] });

const actual = getDirFileContent(tmpDir, 'app.js');
assert.equal(actual, getDirFileContent(`${process.cwd()}/test-fixtures/noSentry`, 'app.js'));
});

it('no-ops if no SDK is specified', async () => {
tmpDir = makeTmpDir(getFixturePath('svelteAppNamed'));
await rewriteSvelteConfig.transform([tmpDir], { filePatterns: [] });

const actual = getDirFileContent(tmpDir, 'svelte.config.js');
assert.equal(actual, getDirFileContent(`${process.cwd()}/test-fixtures/svelteAppNamed`, 'svelte.config.js'));
});

it('no-ops if a non-Svelte SDK is specified', async () => {
tmpDir = makeTmpDir(getFixturePath('svelteAppNamed'));
await rewriteSvelteConfig.transform([tmpDir], { filePatterns: [], sdk: '@sentry/react' });

const actual = getDirFileContent(tmpDir, 'svelte.config.js');
assert.equal(actual, getDirFileContent(`${process.cwd()}/test-fixtures/svelteAppNamed`, 'svelte.config.js'));
});

it('no-ops if `componentTrackingPreprocessor` is not used in the file', async () => {
tmpDir = makeTmpDir(getFixturePath('svelteAppUnchanged'));
await rewriteSvelteConfig.transform([tmpDir], { filePatterns: [], sdk: '@sentry/svelte' });

const actual = getDirFileContent(tmpDir, 'svelte.config.js');
assert.equal(actual, getDirFileContent(`${process.cwd()}/test-fixtures/svelteAppUnchanged`, 'svelte.config.js'));
});

it('works with a svelte config with named imports', async () => {
tmpDir = makeTmpDir(getFixturePath('svelteAppNamed'));
await rewriteSvelteConfig.transform([`${tmpDir}/svelte.config.js`], { filePatterns: [], sdk: '@sentry/svelte' });

const actual = getDirFileContent(tmpDir, 'svelte.config.js');

assertStringEquals(
actual,
`import adapter from '@sveltejs/adapter-vercel';
import preprocess from 'svelte-preprocess';
import { mdsvex } from 'mdsvex';
import { withSentryConfig } from "@sentry/svelte";
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: [preprocess({ postcss: true }), mdsvex({
extensions: ['.md']
})],
extensions: ['.svelte', '.md'],
kit: {
adapter: adapter(),
files: {
lib: './src/lib'
}
},
vitePlugin: {
inspector: true
}
};
export default withSentryConfig(config);`
);
});

it('works with a svelte config with a namespace Sentry import', async () => {
tmpDir = makeTmpDir(getFixturePath('svelteAppNamespace'));
await rewriteSvelteConfig.transform([`${tmpDir}/svelte.config.js`], { filePatterns: [], sdk: '@sentry/sveltekit' });

const actual = getDirFileContent(tmpDir, 'svelte.config.js');

assertStringEquals(
actual,
`import adapter from '@sveltejs/adapter-vercel';
import preprocess from 'svelte-preprocess';
import { mdsvex } from 'mdsvex';
import * as Sentry from '@sentry/sveltekit';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: [preprocess({ postcss: true }), mdsvex({
extensions: ['.md']
})],
extensions: ['.svelte', '.md'],
kit: {
adapter: adapter(),
files: {
lib: './src/lib'
}
},
vitePlugin: {
inspector: true
}
};
export default Sentry.withSentryConfig(config);`
);
});

it('only removes the preprocessor if the wrapper is already applied', async () => {
tmpDir = makeTmpDir(getFixturePath('svelteAppOnlyRemove'));
await rewriteSvelteConfig.transform([`${tmpDir}/svelte.config.js`], { filePatterns: [], sdk: '@sentry/svelte' });

const actual = getDirFileContent(tmpDir, 'svelte.config.js');

assertStringEquals(
actual,
`import adapter from '@sveltejs/adapter-vercel';
import preprocess from 'svelte-preprocess';
import { mdsvex } from 'mdsvex';
import * as Sentry from '@sentry/svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: [preprocess({ postcss: true }), mdsvex({
extensions: ['.md']
})],
extensions: ['.svelte', '.md'],
kit: {
adapter: adapter(),
files: {
lib: './src/lib'
}
},
vitePlugin: {
inspector: true
}
};
export default Sentry.withSentryConfig(config);`
);
});
});
92 changes: 92 additions & 0 deletions src/transformers/rewriteSvelteConfig/transform.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
const { hasSentryImportOrRequire } = require('../../utils/jscodeshift.cjs');

/**
* This transformer rewrites the `svelte.config.js` file to use the `withSentryConfig` function
*
* Replaces the `componentTrackingPreprocessor` function call with wrapping `withSentryConfig` around the
* default exprt of the config file.
*
* @param {import('jscodeshift').FileInfo} fileInfo
* @param {import('jscodeshift').API} api
* @param {import('jscodeshift').Options & { sentry: import('types').RunOptions & {sdk: string} }} options
*/
module.exports = function transform(fileInfo, api, options) {
const j = api.jscodeshift;
const source = fileInfo.source;

// We're already filtering beforehand for @sentry/svelte or @sentry/sveltekit
// so `options.sentry.sdk` is guaranteed to be one of those two
if (!hasSentryImportOrRequire(fileInfo.source, options.sentry.sdk)) {
return undefined;
}

const root = j(source, options);

const sentryNamespaceImports = root
.find(j.ImportDeclaration, { source: { value: options.sentry.sdk } })
.filter(
importPath => !!importPath.node.specifiers?.some(specifier => specifier.type === 'ImportNamespaceSpecifier')
);

/** { @type import('jscodeshift').ASTPath<import('jscodeshift').ImportDeclaration> | undefined */
const sentryNamespaceImport = sentryNamespaceImports.length ? sentryNamespaceImports.get() : undefined;
const sentryNameSpace = sentryNamespaceImport?.node.specifiers?.[0].local?.name;

// 1. remove `sentryComponentTrackingPreprocessor()`

if (sentryNameSpace) {
const preprocCalls = root.find(j.CallExpression, {
callee: {
type: 'MemberExpression',
object: { type: 'Identifier', name: sentryNameSpace },
property: { type: 'Identifier', name: 'componentTrackingPreprocessor' },
},
});
preprocCalls.remove();
} else {
const preprocCalls = root.find(j.CallExpression, { callee: { name: 'componentTrackingPreprocessor' } });
preprocCalls.remove();
}

// 2. wrap withSentryConfig around the default export

/** { @type import('jscodeshift').ASTPath<import('jscodeshift').ExportDefaultDeclaration> */
const defaultExport = root.find(j.ExportDefaultDeclaration).get(0);

const oldDefaultExportValue = defaultExport.node.declaration;

// if the old declaration value already has withSentryConfig, we are done
if (j(oldDefaultExportValue).toSource().includes('withSentryConfig(')) {
return root.toSource();
}

if (sentryNameSpace) {
const newDefaultExportDeclaration = j.callExpression(
j.memberExpression(j.identifier(sentryNameSpace), j.identifier('withSentryConfig')),
// @ts-ignore there's probably a few edge cases which is why TS is complaining. I'm lazy :)
[oldDefaultExportValue]
);
defaultExport.node.declaration = newDefaultExportDeclaration;
} else {
// @ts-ignore there's probably a few edge cases which is why TS is complaining. I'm lazy :)
const newDefaultExportDeclaration = j.callExpression(j.identifier('withSentryConfig'), [oldDefaultExportValue]);
defaultExport.node.declaration = newDefaultExportDeclaration;

root
.find(j.ImportDeclaration, { source: { value: options.sentry.sdk } })
.insertAfter(
j.importDeclaration([j.importSpecifier(j.identifier('withSentryConfig'))], j.literal(options.sentry.sdk))
);

root.find(j.ImportDeclaration, { source: { value: options.sentry.sdk } }).forEach(importPath => {
importPath.node.specifiers = importPath.node.specifiers?.filter(
specifier => specifier.type !== 'ImportSpecifier' || specifier.imported.name !== 'componentTrackingPreprocessor'
);

if (!importPath.node.specifiers?.length) {
importPath.prune();
}
});
}
return root.toSource();
};
36 changes: 36 additions & 0 deletions test-fixtures/svelteAppNamed/svelte.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import adapter from '@sveltejs/adapter-vercel';
import preprocess from 'svelte-preprocess';
import { mdsvex } from 'mdsvex';
import { componentTrackingPreprocessor } from '@sentry/svelte';

/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: [
preprocess({ postcss: true }),
componentTrackingPreprocessor({
trackComponents: ['Header', 'App', 'Footer'],
trackInit: true,
trackUpdates: false,
}),
mdsvex({
extensions: ['.md']
}),
],

extensions: ['.svelte', '.md'],

kit: {
adapter: adapter(),
files: {
lib: './src/lib'
}
},

vitePlugin: {
inspector: true
}
};

export default config;
36 changes: 36 additions & 0 deletions test-fixtures/svelteAppNamespace/svelte.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import adapter from '@sveltejs/adapter-vercel';
import preprocess from 'svelte-preprocess';
import { mdsvex } from 'mdsvex';
import * as Sentry from '@sentry/sveltekit';

/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: [
preprocess({ postcss: true }),
mdsvex({
extensions: ['.md']
}),
Sentry.componentTrackingPreprocessor({
trackComponents: ['Header', 'App', 'Footer'],
trackInit: true,
trackUpdates: false,
}),
],

extensions: ['.svelte', '.md'],

kit: {
adapter: adapter(),
files: {
lib: './src/lib'
}
},

vitePlugin: {
inspector: true
}
};

export default config;
Loading

0 comments on commit dfa126e

Please sign in to comment.