@sentry/wizard 3.11.0 → 3.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/package.json +1 -1
  3. package/dist/src/android/android-wizard.js +8 -0
  4. package/dist/src/android/android-wizard.js.map +1 -1
  5. package/dist/src/android/code-tools.d.ts +8 -0
  6. package/dist/src/android/code-tools.js +20 -8
  7. package/dist/src/android/code-tools.js.map +1 -1
  8. package/dist/src/android/gradle.js +6 -1
  9. package/dist/src/android/gradle.js.map +1 -1
  10. package/dist/src/sourcemaps/tools/vite.js +36 -111
  11. package/dist/src/sourcemaps/tools/vite.js.map +1 -1
  12. package/dist/src/sourcemaps/tools/webpack.d.ts +6 -1
  13. package/dist/src/sourcemaps/tools/webpack.js +290 -25
  14. package/dist/src/sourcemaps/tools/webpack.js.map +1 -1
  15. package/dist/src/sveltekit/sdk-setup.js +2 -2
  16. package/dist/src/sveltekit/sdk-setup.js.map +1 -1
  17. package/dist/src/utils/ast-utils.d.ts +7 -3
  18. package/dist/src/utils/ast-utils.js +20 -5
  19. package/dist/src/utils/ast-utils.js.map +1 -1
  20. package/dist/src/utils/clack-utils.d.ts +52 -0
  21. package/dist/src/utils/clack-utils.js +169 -12
  22. package/dist/src/utils/clack-utils.js.map +1 -1
  23. package/dist/test/android/code-tools.test.d.ts +1 -0
  24. package/dist/test/android/code-tools.test.js +34 -0
  25. package/dist/test/android/code-tools.test.js.map +1 -0
  26. package/dist/test/sourcemaps/tools/webpack.test.d.ts +1 -0
  27. package/dist/test/sourcemaps/tools/webpack.test.js +179 -0
  28. package/dist/test/sourcemaps/tools/webpack.test.js.map +1 -0
  29. package/dist/test/utils/ast-utils.test.js +42 -7
  30. package/dist/test/utils/ast-utils.test.js.map +1 -1
  31. package/dist/test/utils/clack-utils.test.d.ts +1 -0
  32. package/dist/test/utils/clack-utils.test.js +200 -0
  33. package/dist/test/utils/clack-utils.test.js.map +1 -0
  34. package/package.json +1 -1
  35. package/src/android/android-wizard.ts +8 -0
  36. package/src/android/code-tools.ts +21 -7
  37. package/src/android/gradle.ts +6 -1
  38. package/src/sourcemaps/tools/vite.ts +22 -88
  39. package/src/sourcemaps/tools/webpack.ts +369 -30
  40. package/src/sveltekit/sdk-setup.ts +6 -2
  41. package/src/utils/ast-utils.ts +23 -7
  42. package/src/utils/clack-utils.ts +150 -2
  43. package/test/android/code-tools.test.ts +49 -0
  44. package/test/sourcemaps/tools/webpack.test.ts +303 -0
  45. package/test/utils/ast-utils.test.ts +28 -9
  46. package/test/utils/clack-utils.test.ts +142 -0
@@ -17,6 +17,7 @@ import {
17
17
  installPackageWithPackageManager,
18
18
  packageManagers,
19
19
  } from './package-manager';
20
+ import { debug } from './debug';
20
21
 
21
22
  const opn = require('opn') as (
22
23
  url: string,
@@ -778,14 +779,21 @@ async function askForWizardLogin(options: {
778
779
  async function askForProjectSelection(
779
780
  projects: SentryProjectData[],
780
781
  ): Promise<SentryProjectData> {
782
+ const label = (project: SentryProjectData): string => {
783
+ return `${project.organization.slug}/${project.slug}`;
784
+ };
785
+ const sortedProjects = [...projects];
786
+ sortedProjects.sort((a: SentryProjectData, b: SentryProjectData) => {
787
+ return label(a).localeCompare(label(b));
788
+ });
781
789
  const selection: SentryProjectData | symbol = await abortIfCancelled(
782
790
  clack.select({
783
791
  maxItems: 12,
784
792
  message: 'Select your Sentry project.',
785
- options: projects.map((project) => {
793
+ options: sortedProjects.map((project) => {
786
794
  return {
787
795
  value: project,
788
- label: `${project.organization.slug}/${project.slug}`,
796
+ label: label(project),
789
797
  };
790
798
  }),
791
799
  }),
@@ -797,3 +805,143 @@ async function askForProjectSelection(
797
805
 
798
806
  return selection;
799
807
  }
808
+
809
+ /**
810
+ * Asks users if they have a config file for @param tool (e.g. Vite).
811
+ * If yes, asks users to specify the path to their config file.
812
+ *
813
+ * Use this helper function as a fallback mechanism if the lookup for
814
+ * a config file with its most usual location/name fails.
815
+ *
816
+ * @param toolName Name of the tool for which we're looking for the config file
817
+ * @param configFileName Name of the most common config file name (e.g. vite.config.js)
818
+ *
819
+ * @returns a user path to the config file or undefined if the user doesn't have a config file
820
+ */
821
+ export async function askForToolConfigPath(
822
+ toolName: string,
823
+ configFileName: string,
824
+ ): Promise<string | undefined> {
825
+ const hasConfig = await abortIfCancelled(
826
+ clack.confirm({
827
+ message: `Do you have a ${toolName} config file (e.g. ${chalk.cyan(
828
+ configFileName,
829
+ )}?`,
830
+ initialValue: true,
831
+ }),
832
+ );
833
+
834
+ if (!hasConfig) {
835
+ return undefined;
836
+ }
837
+
838
+ return await abortIfCancelled(
839
+ clack.text({
840
+ message: `Please enter the path to your ${toolName} config file:`,
841
+ placeholder: path.join('.', configFileName),
842
+ validate: (value) => {
843
+ if (!value) {
844
+ return 'Please enter a path.';
845
+ }
846
+
847
+ try {
848
+ fs.accessSync(value);
849
+ } catch {
850
+ return 'Could not access the file at this path.';
851
+ }
852
+ },
853
+ }),
854
+ );
855
+ }
856
+
857
+ /**
858
+ * Prints copy/paste-able instructions to the console.
859
+ * Afterwards asks the user if they added the code snippet to their file.
860
+ *
861
+ * While there's no point in providing a "no" answer here, it gives users time to fulfill the
862
+ * task before the wizard continues with additional steps.
863
+ *
864
+ * Use this function if you want to show users instructions on how to add/modify
865
+ * code in their file. This is helpful if automatic insertion failed or is not possible/feasible.
866
+ *
867
+ * @param filename the name of the file to which the code snippet should be applied.
868
+ * If a path is provided, only the filename will be used.
869
+ * @param codeSnippet the snippet to be printed.
870
+ * Make sure to follow the diff-like format of highlighting lines that require changes
871
+ * and showing unchanged lines in gray.
872
+ *
873
+ * TODO: Link to wizard spec (develop) once it is live
874
+ * TODO: refactor copy paste instructions across different wizards to use this function.
875
+ * this might require adding a custom message parameter to the function
876
+ */
877
+ export async function showCopyPasteInstructions(
878
+ filename: string,
879
+ codeSnippet: string,
880
+ ): Promise<void> {
881
+ clack.log.step(
882
+ `Add the following code to your ${chalk.cyan(
883
+ path.basename(filename),
884
+ )} file:`,
885
+ );
886
+
887
+ // Intentionally logging directly to console here so that the code can be copied/pasted directly
888
+ // eslint-disable-next-line no-console
889
+ console.log(`\n${codeSnippet}`);
890
+
891
+ await abortIfCancelled(
892
+ clack.select({
893
+ message: 'Did you apply the snippet above?',
894
+ options: [{ label: 'Yes, continue!', value: true }],
895
+ initialValue: true,
896
+ }),
897
+ );
898
+ }
899
+
900
+ /**
901
+ * Creates a new config file with the given @param filepath and @param codeSnippet.
902
+ *
903
+ * Use this function to create a new config file for users. This is useful
904
+ * when users answered that they don't yet have a config file for a tool.
905
+ *
906
+ * (This doesn't mean that they don't yet have some other way of configuring
907
+ * their tool but we can leave it up to them to figure out how to merge configs
908
+ * here.)
909
+ *
910
+ * @param filepath absolute path to the new config file
911
+ * @param codeSnippet the snippet to be inserted into the file
912
+ * @param moreInformation (optional) the message to be printed after the file was created
913
+ * For example, this can be a link to more information about configuring the tool.
914
+ *
915
+ * @returns true on sucess, false otherwise
916
+ */
917
+ export async function createNewConfigFile(
918
+ filepath: string,
919
+ codeSnippet: string,
920
+ moreInformation?: string,
921
+ ): Promise<boolean> {
922
+ if (!path.isAbsolute(filepath)) {
923
+ debug(`createNewConfigFile: filepath is not absolute: ${filepath}`);
924
+ return false;
925
+ }
926
+
927
+ const prettyFilename = chalk.cyan(path.relative(process.cwd(), filepath));
928
+
929
+ try {
930
+ await fs.promises.writeFile(filepath, codeSnippet);
931
+
932
+ clack.log.success(`Added new ${prettyFilename} file.`);
933
+
934
+ if (moreInformation) {
935
+ clack.log.info(chalk.gray(moreInformation));
936
+ }
937
+
938
+ return true;
939
+ } catch (e) {
940
+ debug(e);
941
+ clack.log.warn(
942
+ `Could not create a new ${prettyFilename} file. Please create one manually and follow the instructions below.`,
943
+ );
944
+ }
945
+
946
+ return false;
947
+ }
@@ -0,0 +1,49 @@
1
+ //@ts-ignore
2
+ import { getLastImportLineLocation } from '../../src/android/code-tools';
3
+
4
+ describe('code-tools', () => {
5
+ describe('getLastImportLineLocation', () => {
6
+ it('returns proper line index', () => {
7
+ const code = `import a.b.c;\n` + `//<insert-location>\n` + `class X {}`;
8
+ expect(getLastImportLineLocation(code)).toBe(
9
+ code.indexOf('//<insert-location>'),
10
+ );
11
+ });
12
+
13
+ it('returns proper line index when static import is used', () => {
14
+ const code =
15
+ `import static a.b.c;\n` + `//<insert-location>\n` + `class X {}`;
16
+ expect(getLastImportLineLocation(code)).toBe(
17
+ code.indexOf('//<insert-location>'),
18
+ );
19
+ });
20
+
21
+ it('returns proper line index when wildcard import is used', () => {
22
+ const code = `import a.b.*\n` + `//<insert-location>\n` + `class X {}`;
23
+ expect(getLastImportLineLocation(code)).toBe(
24
+ code.indexOf('//<insert-location>'),
25
+ );
26
+ });
27
+
28
+ it('returns proper line index when alias import is used', () => {
29
+ const code =
30
+ `import static a.b.c as d\n` + `//<insert-location>\n` + `class X {}`;
31
+ expect(getLastImportLineLocation(code)).toBe(
32
+ code.indexOf('//<insert-location>'),
33
+ );
34
+ });
35
+
36
+ it('returns proper line index when multiple imports are present', () => {
37
+ const code =
38
+ `import static a.b.c as d\n` +
39
+ `import a.b.*\n` +
40
+ `import static a.b.c;\n` +
41
+ `import a.b.c;\n` +
42
+ `//<insert-location>\n` +
43
+ `class X {}`;
44
+ expect(getLastImportLineLocation(code)).toBe(
45
+ code.indexOf('//<insert-location>'),
46
+ );
47
+ });
48
+ });
49
+ });
@@ -0,0 +1,303 @@
1
+ import * as fs from 'fs';
2
+
3
+ import { modifyWebpackConfig } from '../../../src/sourcemaps/tools/webpack';
4
+
5
+ function updateFileContent(content: string): void {
6
+ fileContent = content;
7
+ }
8
+
9
+ let fileContent = '';
10
+
11
+ jest.mock('@clack/prompts', () => {
12
+ return {
13
+ log: {
14
+ info: jest.fn(),
15
+ success: jest.fn(),
16
+ },
17
+ select: jest.fn().mockImplementation(() => Promise.resolve(true)),
18
+ isCancel: jest.fn().mockReturnValue(false),
19
+ };
20
+ });
21
+
22
+ jest
23
+ .spyOn(fs.promises, 'readFile')
24
+ .mockImplementation(() => Promise.resolve(fileContent));
25
+
26
+ const writeFileSpy = jest
27
+ .spyOn(fs.promises, 'writeFile')
28
+ .mockImplementation(() => Promise.resolve(void 0));
29
+
30
+ const noSourcemapNoPluginsPojo = `module.exports = {
31
+ entry: "./src/index.js",
32
+ output: {
33
+ filename: "main.js",
34
+ path: path.resolve(__dirname, "build"),
35
+ },
36
+ };`;
37
+
38
+ const noSourcemapNoPluginsPojoResult = `const {
39
+ sentryWebpackPlugin
40
+ } = require("@sentry/webpack-plugin");
41
+
42
+ module.exports = {
43
+ entry: "./src/index.js",
44
+
45
+ output: {
46
+ filename: "main.js",
47
+ path: path.resolve(__dirname, "build"),
48
+ },
49
+
50
+ devtool: "source-map",
51
+
52
+ plugins: [sentryWebpackPlugin({
53
+ authToken: process.env.SENTRY_AUTH_TOKEN,
54
+ org: "my-org",
55
+ project: "my-project"
56
+ })]
57
+ };`;
58
+
59
+ const noSourcemapsNoPluginsId = `const config = {
60
+ entry: "./src/index.js",
61
+
62
+ output: {
63
+ filename: "main.js",
64
+ path: path.resolve(__dirname, "build"),
65
+ },
66
+ };
67
+
68
+ module.exports = config;`;
69
+
70
+ const noSourcemapsNoPluginsIdResult = `const {
71
+ sentryWebpackPlugin
72
+ } = require("@sentry/webpack-plugin");
73
+
74
+ const config = {
75
+ entry: "./src/index.js",
76
+
77
+ output: {
78
+ filename: "main.js",
79
+ path: path.resolve(__dirname, "build"),
80
+ },
81
+
82
+ devtool: "source-map",
83
+
84
+ plugins: [sentryWebpackPlugin({
85
+ authToken: process.env.SENTRY_AUTH_TOKEN,
86
+ org: "my-org",
87
+ project: "my-project"
88
+ })]
89
+ };
90
+
91
+ module.exports = config;`;
92
+
93
+ const hiddenSourcemapNoPluginsId = `const config = {
94
+ entry: "./src/index.js",
95
+
96
+ output: {
97
+ filename: "main.js",
98
+ path: path.resolve(__dirname, "build"),
99
+ },
100
+
101
+ devtool: "hidden-cheap-source-map",
102
+ };
103
+
104
+ module.exports = config;
105
+ `;
106
+ const hiddenSourcemapNoPluginsIdResult = `const {
107
+ sentryWebpackPlugin
108
+ } = require("@sentry/webpack-plugin");
109
+
110
+ const config = {
111
+ entry: "./src/index.js",
112
+
113
+ output: {
114
+ filename: "main.js",
115
+ path: path.resolve(__dirname, "build"),
116
+ },
117
+
118
+ devtool: "hidden-source-map",
119
+
120
+ plugins: [sentryWebpackPlugin({
121
+ authToken: process.env.SENTRY_AUTH_TOKEN,
122
+ org: "my-org",
123
+ project: "my-project"
124
+ })]
125
+ };
126
+
127
+ module.exports = config;`;
128
+
129
+ const arbitrarySourcemapNoPluginsId = `
130
+ const config = {
131
+ entry: "./src/index.js",
132
+
133
+ output: {
134
+ filename: "main.js",
135
+ path: path.resolve(__dirname, "build"),
136
+ },
137
+
138
+ devtool: getSourcemapSetting(),
139
+ };
140
+
141
+ module.exports = config;
142
+ `;
143
+ const arbitrarySourcemapNoPluginsIdResult = `const {
144
+ sentryWebpackPlugin
145
+ } = require("@sentry/webpack-plugin");
146
+
147
+ const config = {
148
+ entry: "./src/index.js",
149
+
150
+ output: {
151
+ filename: "main.js",
152
+ path: path.resolve(__dirname, "build"),
153
+ },
154
+
155
+ devtool: "source-map",
156
+
157
+ plugins: [sentryWebpackPlugin({
158
+ authToken: process.env.SENTRY_AUTH_TOKEN,
159
+ org: "my-org",
160
+ project: "my-project"
161
+ })]
162
+ };
163
+
164
+ module.exports = config;`;
165
+
166
+ const noSourcemapUndefinedPluginsPojo = `module.exports = {
167
+ entry: "./src/index.js",
168
+ plugins: undefined,
169
+ output: {
170
+ filename: "main.js",
171
+ path: path.resolve(__dirname, "build"),
172
+ },
173
+ };`;
174
+
175
+ const noSourcemapUndefinedPluginsPojoResult = `const {
176
+ sentryWebpackPlugin
177
+ } = require("@sentry/webpack-plugin");
178
+
179
+ module.exports = {
180
+ entry: "./src/index.js",
181
+
182
+ plugins: [sentryWebpackPlugin({
183
+ authToken: process.env.SENTRY_AUTH_TOKEN,
184
+ org: "my-org",
185
+ project: "my-project"
186
+ })],
187
+
188
+ output: {
189
+ filename: "main.js",
190
+ path: path.resolve(__dirname, "build"),
191
+ },
192
+
193
+ devtool: "source-map"
194
+ };`;
195
+
196
+ const noSourcemapPluginsPojo = `module.exports = {
197
+ entry: "./src/index.js",
198
+ plugins: [
199
+ new HtmlWebpackPlugin(),
200
+ new MiniCssExtractPlugin(),
201
+ ],
202
+ output: {
203
+ filename: "main.js",
204
+ path: path.resolve(__dirname, "build"),
205
+ },
206
+ };`;
207
+
208
+ const noSourcemapPluginsPojoResult = `const {
209
+ sentryWebpackPlugin
210
+ } = require("@sentry/webpack-plugin");
211
+
212
+ module.exports = {
213
+ entry: "./src/index.js",
214
+
215
+ plugins: [new HtmlWebpackPlugin(), new MiniCssExtractPlugin(), sentryWebpackPlugin({
216
+ authToken: process.env.SENTRY_AUTH_TOKEN,
217
+ org: "my-org",
218
+ project: "my-project"
219
+ })],
220
+
221
+ output: {
222
+ filename: "main.js",
223
+ path: path.resolve(__dirname, "build"),
224
+ },
225
+
226
+ devtool: "source-map"
227
+ };`;
228
+
229
+ describe('modifyWebpackConfig', () => {
230
+ afterEach(() => {
231
+ fileContent = '';
232
+ jest.clearAllMocks();
233
+ });
234
+
235
+ it.each([
236
+ [
237
+ 'no sourcemap option, no plugins, object',
238
+ noSourcemapNoPluginsPojo,
239
+ noSourcemapNoPluginsPojoResult,
240
+ ],
241
+ [
242
+ 'no sourcemap option, no plugins, identifier',
243
+ noSourcemapsNoPluginsId,
244
+ noSourcemapsNoPluginsIdResult,
245
+ ],
246
+ [
247
+ 'hidden sourcemap option, no plugins, identifier',
248
+ hiddenSourcemapNoPluginsId,
249
+ hiddenSourcemapNoPluginsIdResult,
250
+ ],
251
+ [
252
+ 'arbitrary sourcemap option, no plugins, identifier',
253
+ arbitrarySourcemapNoPluginsId,
254
+ arbitrarySourcemapNoPluginsIdResult,
255
+ ],
256
+ [
257
+ 'no sourcemap option, plugins, object',
258
+ noSourcemapUndefinedPluginsPojo,
259
+ noSourcemapUndefinedPluginsPojoResult,
260
+ ],
261
+ [
262
+ 'no sourcemap option, plugins, object',
263
+ noSourcemapPluginsPojo,
264
+ noSourcemapPluginsPojoResult,
265
+ ],
266
+ ])(
267
+ 'adds plugin and source maps emission to the webpack config (%s)',
268
+ async (_, originalCode, expectedCode) => {
269
+ updateFileContent(originalCode);
270
+
271
+ // updateFileContent(originalCode);
272
+ const addedCode = await modifyWebpackConfig('', {
273
+ authToken: '',
274
+ orgSlug: 'my-org',
275
+ projectSlug: 'my-project',
276
+ selfHosted: false,
277
+ url: 'https://sentry.io/',
278
+ });
279
+
280
+ expect(writeFileSpy).toHaveBeenCalledTimes(1);
281
+ const [[, fileContent]] = writeFileSpy.mock.calls;
282
+ expect(fileContent).toBe(expectedCode);
283
+ expect(addedCode).toBe(true);
284
+ },
285
+ );
286
+
287
+ it('adds the url parameter to the webpack plugin options if self-hosted', async () => {
288
+ updateFileContent(noSourcemapNoPluginsPojo);
289
+
290
+ const addedCode = await modifyWebpackConfig('', {
291
+ authToken: '',
292
+ orgSlug: 'my-org',
293
+ projectSlug: 'my-project',
294
+ selfHosted: true,
295
+ url: 'https://santry.io/',
296
+ });
297
+
298
+ expect(writeFileSpy).toHaveBeenCalledTimes(1);
299
+ const [[, fileContent]] = writeFileSpy.mock.calls;
300
+ expect(fileContent).toContain('url: "https://santry.io/"');
301
+ expect(addedCode).toBe(true);
302
+ });
303
+ });
@@ -1,22 +1,37 @@
1
- //@ts-ignore
2
- import { parseModule } from 'magicast';
3
1
  import { hasSentryContent } from '../../src/utils/ast-utils';
4
2
 
3
+ import * as recast from 'recast';
4
+
5
5
  describe('AST utils', () => {
6
6
  describe('hasSentryContent', () => {
7
- it("returns true if a '@sentry/' import was found in the parsed module", () => {
8
- const code = `
7
+ it.each([
8
+ `
9
+ const { sentryVitePlugin } = require("@sentry/vite-plugin");
10
+ const somethingelse = require('gs');
11
+ `,
12
+ `
9
13
  import { sentryVitePlugin } from "@sentry/vite-plugin";
10
14
  import * as somethingelse from 'gs';
11
15
 
12
16
  export default {
13
17
  plugins: [sentryVitePlugin()]
14
18
  }
15
- `;
19
+ `,
20
+ ])(
21
+ "returns true if a require('@sentry/') call was found in the parsed module",
22
+ (code) => {
23
+ // recast.parse returns a Program node (or fails) but it's badly typed as any
24
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
25
+ const program = recast.parse(code)
26
+ .program as recast.types.namedTypes.Program;
27
+ expect(hasSentryContent(program)).toBe(true);
28
+ },
29
+ );
16
30
 
17
- expect(hasSentryContent(parseModule(code))).toBe(true);
18
- });
19
31
  it.each([
32
+ `const whatever = require('something')`,
33
+ `// const {sentryWebpackPlugin} = require('@sentry/webpack-plugin')`,
34
+ `const {sAntryWebpackPlugin} = require('webpack-plugin-@sentry')`,
20
35
  `
21
36
  import * as somethingelse from 'gs';
22
37
  export default {
@@ -35,9 +50,13 @@ describe('AST utils', () => {
35
50
  }
36
51
  `,
37
52
  ])(
38
- "reutrns false for modules without a valid '@sentry/' import",
53
+ "returns false if the file doesn't contain any require('@sentry/') calls",
39
54
  (code) => {
40
- expect(hasSentryContent(parseModule(code))).toBe(false);
55
+ // recast.parse returns a Program node (or fails) but it's badly typed as any
56
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
57
+ const program = recast.parse(code)
58
+ .program as recast.types.namedTypes.Program;
59
+ expect(hasSentryContent(program)).toBe(false);
41
60
  },
42
61
  );
43
62
  });