@sentry/wizard 6.6.1 → 6.8.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 (122) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +97 -8
  3. package/dist/bin.js +5 -0
  4. package/dist/bin.js.map +1 -1
  5. package/dist/e2e-tests/tests/help-message.test.js +5 -1
  6. package/dist/e2e-tests/tests/help-message.test.js.map +1 -1
  7. package/dist/e2e-tests/tests/nextjs-15.test.js +79 -0
  8. package/dist/e2e-tests/tests/nextjs-15.test.js.map +1 -1
  9. package/dist/e2e-tests/tests/pnpm-workspace.test.d.ts +1 -0
  10. package/dist/e2e-tests/tests/pnpm-workspace.test.js +206 -0
  11. package/dist/e2e-tests/tests/pnpm-workspace.test.js.map +1 -0
  12. package/dist/e2e-tests/tests/react-router.test.d.ts +1 -0
  13. package/dist/e2e-tests/tests/react-router.test.js +255 -0
  14. package/dist/e2e-tests/tests/react-router.test.js.map +1 -0
  15. package/dist/e2e-tests/utils/index.d.ts +8 -2
  16. package/dist/e2e-tests/utils/index.js +72 -21
  17. package/dist/e2e-tests/utils/index.js.map +1 -1
  18. package/dist/lib/Constants.d.ts +1 -0
  19. package/dist/lib/Constants.js +5 -0
  20. package/dist/lib/Constants.js.map +1 -1
  21. package/dist/src/android/android-wizard.js +8 -1
  22. package/dist/src/android/android-wizard.js.map +1 -1
  23. package/dist/src/angular/angular-wizard.js +8 -1
  24. package/dist/src/angular/angular-wizard.js.map +1 -1
  25. package/dist/src/apple/apple-wizard.js +8 -1
  26. package/dist/src/apple/apple-wizard.js.map +1 -1
  27. package/dist/src/flutter/flutter-wizard.js +8 -1
  28. package/dist/src/flutter/flutter-wizard.js.map +1 -1
  29. package/dist/src/nextjs/nextjs-wizard.js +35 -9
  30. package/dist/src/nextjs/nextjs-wizard.js.map +1 -1
  31. package/dist/src/nextjs/templates.d.ts +3 -3
  32. package/dist/src/nextjs/templates.js +18 -7
  33. package/dist/src/nextjs/templates.js.map +1 -1
  34. package/dist/src/nuxt/nuxt-wizard.js +8 -1
  35. package/dist/src/nuxt/nuxt-wizard.js.map +1 -1
  36. package/dist/src/react-native/react-native-wizard.js +8 -1
  37. package/dist/src/react-native/react-native-wizard.js.map +1 -1
  38. package/dist/src/react-router/codemods/client.entry.d.ts +1 -0
  39. package/dist/src/react-router/codemods/client.entry.js +73 -0
  40. package/dist/src/react-router/codemods/client.entry.js.map +1 -0
  41. package/dist/src/react-router/codemods/react-router-config.d.ts +9 -0
  42. package/dist/src/react-router/codemods/react-router-config.js +178 -0
  43. package/dist/src/react-router/codemods/react-router-config.js.map +1 -0
  44. package/dist/src/react-router/codemods/root.d.ts +1 -0
  45. package/dist/src/react-router/codemods/root.js +171 -0
  46. package/dist/src/react-router/codemods/root.js.map +1 -0
  47. package/dist/src/react-router/codemods/routes-config.d.ts +1 -0
  48. package/dist/src/react-router/codemods/routes-config.js +106 -0
  49. package/dist/src/react-router/codemods/routes-config.js.map +1 -0
  50. package/dist/src/react-router/codemods/server-entry.d.ts +4 -0
  51. package/dist/src/react-router/codemods/server-entry.js +275 -0
  52. package/dist/src/react-router/codemods/server-entry.js.map +1 -0
  53. package/dist/src/react-router/codemods/utils.d.ts +2 -0
  54. package/dist/src/react-router/codemods/utils.js +13 -0
  55. package/dist/src/react-router/codemods/utils.js.map +1 -0
  56. package/dist/src/react-router/codemods/vite.d.ts +8 -0
  57. package/dist/src/react-router/codemods/vite.js +169 -0
  58. package/dist/src/react-router/codemods/vite.js.map +1 -0
  59. package/dist/src/react-router/react-router-wizard.d.ts +2 -0
  60. package/dist/src/react-router/react-router-wizard.js +254 -0
  61. package/dist/src/react-router/react-router-wizard.js.map +1 -0
  62. package/dist/src/react-router/sdk-example.d.ts +18 -0
  63. package/dist/src/react-router/sdk-example.js +306 -0
  64. package/dist/src/react-router/sdk-example.js.map +1 -0
  65. package/dist/src/react-router/sdk-setup.d.ts +17 -0
  66. package/dist/src/react-router/sdk-setup.js +250 -0
  67. package/dist/src/react-router/sdk-setup.js.map +1 -0
  68. package/dist/src/react-router/templates.d.ts +11 -0
  69. package/dist/src/react-router/templates.js +273 -0
  70. package/dist/src/react-router/templates.js.map +1 -0
  71. package/dist/src/remix/remix-wizard.js +8 -1
  72. package/dist/src/remix/remix-wizard.js.map +1 -1
  73. package/dist/src/run.d.ts +2 -1
  74. package/dist/src/run.js +6 -0
  75. package/dist/src/run.js.map +1 -1
  76. package/dist/src/sourcemaps/sourcemaps-wizard.js +8 -1
  77. package/dist/src/sourcemaps/sourcemaps-wizard.js.map +1 -1
  78. package/dist/src/sveltekit/sveltekit-wizard.js +8 -1
  79. package/dist/src/sveltekit/sveltekit-wizard.js.map +1 -1
  80. package/dist/src/utils/ast-utils.d.ts +30 -0
  81. package/dist/src/utils/ast-utils.js +71 -1
  82. package/dist/src/utils/ast-utils.js.map +1 -1
  83. package/dist/src/utils/clack/index.d.ts +5 -2
  84. package/dist/src/utils/clack/index.js +8 -0
  85. package/dist/src/utils/clack/index.js.map +1 -1
  86. package/dist/src/utils/package-json.js +86 -2
  87. package/dist/src/utils/package-json.js.map +1 -1
  88. package/dist/src/utils/types.d.ts +9 -0
  89. package/dist/src/utils/types.js.map +1 -1
  90. package/dist/src/version.d.ts +1 -1
  91. package/dist/src/version.js +1 -1
  92. package/dist/src/version.js.map +1 -1
  93. package/dist/test/nextjs/templates.test.js +20 -0
  94. package/dist/test/nextjs/templates.test.js.map +1 -1
  95. package/dist/test/react-router/codemods/client-entry.test.d.ts +1 -0
  96. package/dist/test/react-router/codemods/client-entry.test.js +168 -0
  97. package/dist/test/react-router/codemods/client-entry.test.js.map +1 -0
  98. package/dist/test/react-router/codemods/react-router-config.test.d.ts +1 -0
  99. package/dist/test/react-router/codemods/react-router-config.test.js +168 -0
  100. package/dist/test/react-router/codemods/react-router-config.test.js.map +1 -0
  101. package/dist/test/react-router/codemods/root.test.d.ts +1 -0
  102. package/dist/test/react-router/codemods/root.test.js +178 -0
  103. package/dist/test/react-router/codemods/root.test.js.map +1 -0
  104. package/dist/test/react-router/codemods/server-entry.test.d.ts +1 -0
  105. package/dist/test/react-router/codemods/server-entry.test.js +415 -0
  106. package/dist/test/react-router/codemods/server-entry.test.js.map +1 -0
  107. package/dist/test/react-router/codemods/vite.test.d.ts +1 -0
  108. package/dist/test/react-router/codemods/vite.test.js +158 -0
  109. package/dist/test/react-router/codemods/vite.test.js.map +1 -0
  110. package/dist/test/react-router/routes-config.test.d.ts +1 -0
  111. package/dist/test/react-router/routes-config.test.js +156 -0
  112. package/dist/test/react-router/routes-config.test.js.map +1 -0
  113. package/dist/test/react-router/sdk-setup.test.d.ts +1 -0
  114. package/dist/test/react-router/sdk-setup.test.js +411 -0
  115. package/dist/test/react-router/sdk-setup.test.js.map +1 -0
  116. package/dist/test/react-router/templates.test.d.ts +1 -0
  117. package/dist/test/react-router/templates.test.js +220 -0
  118. package/dist/test/react-router/templates.test.js.map +1 -0
  119. package/dist/test/utils/package-json.test.d.ts +1 -0
  120. package/dist/test/utils/package-json.test.js +428 -0
  121. package/dist/test/utils/package-json.test.js.map +1 -0
  122. package/package.json +4 -2
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ const fs = __importStar(require("fs"));
27
+ const path = __importStar(require("path"));
28
+ const vitest_1 = require("vitest");
29
+ const react_router_config_1 = require("../../../src/react-router/codemods/react-router-config");
30
+ vitest_1.vi.mock('@clack/prompts', () => ({
31
+ default: {
32
+ log: {
33
+ info: vitest_1.vi.fn(),
34
+ warn: vitest_1.vi.fn(),
35
+ success: vitest_1.vi.fn(),
36
+ },
37
+ },
38
+ }));
39
+ vitest_1.vi.mock('fs', async () => {
40
+ const actual = await vitest_1.vi.importActual('fs');
41
+ return {
42
+ ...actual,
43
+ existsSync: vitest_1.vi.fn(),
44
+ promises: {
45
+ ...actual.promises,
46
+ readFile: vitest_1.vi.fn(),
47
+ writeFile: vitest_1.vi.fn(),
48
+ },
49
+ };
50
+ });
51
+ (0, vitest_1.describe)('React Router Config File Instrumentation', () => {
52
+ const mockCwd = '/mock/project';
53
+ (0, vitest_1.beforeEach)(() => {
54
+ vitest_1.vi.clearAllMocks();
55
+ vitest_1.vi.spyOn(process, 'cwd').mockReturnValue(mockCwd);
56
+ });
57
+ (0, vitest_1.afterEach)(() => {
58
+ vitest_1.vi.restoreAllMocks();
59
+ });
60
+ (0, vitest_1.describe)('instrumentReactRouterConfig', () => {
61
+ (0, vitest_1.it)('should create new config file if it does not exist', async () => {
62
+ const writtenFiles = {};
63
+ vitest_1.vi.mocked(fs.existsSync).mockReturnValue(false);
64
+ vitest_1.vi.mocked(fs.promises.writeFile).mockImplementation((filePath, content) => {
65
+ writtenFiles[filePath] = content;
66
+ return Promise.resolve();
67
+ });
68
+ const result = await (0, react_router_config_1.instrumentReactRouterConfig)(true);
69
+ (0, vitest_1.expect)(result.ssrWasChanged).toBe(false);
70
+ const configPath = path.join(mockCwd, 'react-router.config.ts');
71
+ (0, vitest_1.expect)(writtenFiles[configPath]).toContain('ssr: true');
72
+ (0, vitest_1.expect)(writtenFiles[configPath]).toContain('sentryOnBuildEnd');
73
+ (0, vitest_1.expect)(writtenFiles[configPath]).toContain('buildEnd:');
74
+ });
75
+ (0, vitest_1.it)('should create .js config when TypeScript is not used', async () => {
76
+ const writtenFiles = {};
77
+ vitest_1.vi.mocked(fs.existsSync).mockReturnValue(false);
78
+ vitest_1.vi.mocked(fs.promises.writeFile).mockImplementation((filePath, content) => {
79
+ writtenFiles[filePath] = content;
80
+ return Promise.resolve();
81
+ });
82
+ await (0, react_router_config_1.instrumentReactRouterConfig)(false);
83
+ const configPath = path.join(mockCwd, 'react-router.config.js');
84
+ (0, vitest_1.expect)(writtenFiles[configPath]).toBeDefined();
85
+ // Verify JS config doesn't have TypeScript-only syntax
86
+ (0, vitest_1.expect)(writtenFiles[configPath]).not.toContain('import type');
87
+ (0, vitest_1.expect)(writtenFiles[configPath]).not.toContain('satisfies Config');
88
+ (0, vitest_1.expect)(writtenFiles[configPath]).toContain('sentryOnBuildEnd');
89
+ (0, vitest_1.expect)(writtenFiles[configPath]).toContain('buildEnd:');
90
+ });
91
+ (0, vitest_1.it)('should detect and skip if Sentry content already exists', async () => {
92
+ const existingConfig = `import { sentryOnBuildEnd } from '@sentry/react-router';
93
+
94
+ export default {
95
+ ssr: true,
96
+ buildEnd: async ({ viteConfig, reactRouterConfig, buildManifest }) => {
97
+ await sentryOnBuildEnd({ viteConfig, reactRouterConfig, buildManifest });
98
+ }
99
+ };`;
100
+ vitest_1.vi.mocked(fs.existsSync).mockReturnValue(true);
101
+ vitest_1.vi.mocked(fs.promises.readFile).mockResolvedValue(existingConfig);
102
+ const result = await (0, react_router_config_1.instrumentReactRouterConfig)(true);
103
+ (0, vitest_1.expect)(result.ssrWasChanged).toBe(false);
104
+ });
105
+ (0, vitest_1.it)('should add buildEnd hook to existing config', async () => {
106
+ const existingConfig = `export default {
107
+ ssr: true,
108
+ async: false
109
+ };`;
110
+ const writtenFiles = {};
111
+ vitest_1.vi.mocked(fs.existsSync).mockReturnValue(true);
112
+ vitest_1.vi.mocked(fs.promises.readFile).mockResolvedValue(existingConfig);
113
+ vitest_1.vi.mocked(fs.promises.writeFile).mockImplementation((filePath, content) => {
114
+ writtenFiles[filePath] = content;
115
+ return Promise.resolve();
116
+ });
117
+ const result = await (0, react_router_config_1.instrumentReactRouterConfig)(true);
118
+ (0, vitest_1.expect)(result.ssrWasChanged).toBe(false);
119
+ const writtenConfig = Object.values(writtenFiles)[0];
120
+ (0, vitest_1.expect)(writtenConfig).toContain('sentryOnBuildEnd');
121
+ (0, vitest_1.expect)(writtenConfig).toContain('buildEnd:');
122
+ });
123
+ (0, vitest_1.it)('should set ssr: true if missing', async () => {
124
+ const existingConfig = `export default {
125
+ async: false
126
+ };`;
127
+ const writtenFiles = {};
128
+ vitest_1.vi.mocked(fs.existsSync).mockReturnValue(true);
129
+ vitest_1.vi.mocked(fs.promises.readFile).mockResolvedValue(existingConfig);
130
+ vitest_1.vi.mocked(fs.promises.writeFile).mockImplementation((filePath, content) => {
131
+ writtenFiles[filePath] = content;
132
+ return Promise.resolve();
133
+ });
134
+ const result = await (0, react_router_config_1.instrumentReactRouterConfig)(true);
135
+ (0, vitest_1.expect)(result.ssrWasChanged).toBe(true);
136
+ const writtenConfig = Object.values(writtenFiles)[0];
137
+ (0, vitest_1.expect)(writtenConfig).toContain('ssr: true');
138
+ });
139
+ (0, vitest_1.it)('should report ssrWasChanged when changing ssr from false to true', async () => {
140
+ const existingConfig = `export default {
141
+ ssr: false
142
+ };`;
143
+ const writtenFiles = {};
144
+ vitest_1.vi.mocked(fs.existsSync).mockReturnValue(true);
145
+ vitest_1.vi.mocked(fs.promises.readFile).mockResolvedValue(existingConfig);
146
+ vitest_1.vi.mocked(fs.promises.writeFile).mockImplementation((filePath, content) => {
147
+ writtenFiles[filePath] = content;
148
+ return Promise.resolve();
149
+ });
150
+ const result = await (0, react_router_config_1.instrumentReactRouterConfig)(true);
151
+ (0, vitest_1.expect)(result.ssrWasChanged).toBe(true);
152
+ const writtenConfig = Object.values(writtenFiles)[0];
153
+ (0, vitest_1.expect)(writtenConfig).toContain('ssr: true');
154
+ });
155
+ (0, vitest_1.it)('should throw error if buildEnd already exists', async () => {
156
+ const existingConfig = `export default {
157
+ ssr: true,
158
+ buildEnd: async () => {
159
+ console.log('existing hook');
160
+ }
161
+ };`;
162
+ vitest_1.vi.mocked(fs.existsSync).mockReturnValue(true);
163
+ vitest_1.vi.mocked(fs.promises.readFile).mockResolvedValue(existingConfig);
164
+ await (0, vitest_1.expect)((0, react_router_config_1.instrumentReactRouterConfig)(true)).rejects.toThrow('A buildEnd hook already exists');
165
+ });
166
+ });
167
+ });
168
+ //# sourceMappingURL=react-router-config.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react-router-config.test.js","sourceRoot":"","sources":["../../../../test/react-router/codemods/react-router-config.test.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';\nimport { instrumentReactRouterConfig } from '../../../src/react-router/codemods/react-router-config';\n\nvi.mock('@clack/prompts', () => ({\n default: {\n log: {\n info: vi.fn(),\n warn: vi.fn(),\n success: vi.fn(),\n },\n },\n}));\nvi.mock('fs', async () => {\n const actual = await vi.importActual<typeof fs>('fs');\n return {\n ...actual,\n existsSync: vi.fn(),\n promises: {\n ...actual.promises,\n readFile: vi.fn(),\n writeFile: vi.fn(),\n },\n };\n});\n\ndescribe('React Router Config File Instrumentation', () => {\n const mockCwd = '/mock/project';\n\n beforeEach(() => {\n vi.clearAllMocks();\n vi.spyOn(process, 'cwd').mockReturnValue(mockCwd);\n });\n\n afterEach(() => {\n vi.restoreAllMocks();\n });\n\n describe('instrumentReactRouterConfig', () => {\n it('should create new config file if it does not exist', async () => {\n const writtenFiles: Record<string, string> = {};\n\n vi.mocked(fs.existsSync).mockReturnValue(false);\n vi.mocked(fs.promises.writeFile).mockImplementation(\n (filePath, content) => {\n writtenFiles[filePath as string] = content as string;\n return Promise.resolve();\n },\n );\n\n const result = await instrumentReactRouterConfig(true);\n\n expect(result.ssrWasChanged).toBe(false);\n\n const configPath = path.join(mockCwd, 'react-router.config.ts');\n expect(writtenFiles[configPath]).toContain('ssr: true');\n expect(writtenFiles[configPath]).toContain('sentryOnBuildEnd');\n expect(writtenFiles[configPath]).toContain('buildEnd:');\n });\n\n it('should create .js config when TypeScript is not used', async () => {\n const writtenFiles: Record<string, string> = {};\n\n vi.mocked(fs.existsSync).mockReturnValue(false);\n vi.mocked(fs.promises.writeFile).mockImplementation(\n (filePath, content) => {\n writtenFiles[filePath as string] = content as string;\n return Promise.resolve();\n },\n );\n\n await instrumentReactRouterConfig(false);\n\n const configPath = path.join(mockCwd, 'react-router.config.js');\n expect(writtenFiles[configPath]).toBeDefined();\n // Verify JS config doesn't have TypeScript-only syntax\n expect(writtenFiles[configPath]).not.toContain('import type');\n expect(writtenFiles[configPath]).not.toContain('satisfies Config');\n expect(writtenFiles[configPath]).toContain('sentryOnBuildEnd');\n expect(writtenFiles[configPath]).toContain('buildEnd:');\n });\n\n it('should detect and skip if Sentry content already exists', async () => {\n const existingConfig = `import { sentryOnBuildEnd } from '@sentry/react-router';\n\nexport default {\n ssr: true,\n buildEnd: async ({ viteConfig, reactRouterConfig, buildManifest }) => {\n await sentryOnBuildEnd({ viteConfig, reactRouterConfig, buildManifest });\n }\n};`;\n\n vi.mocked(fs.existsSync).mockReturnValue(true);\n vi.mocked(fs.promises.readFile).mockResolvedValue(existingConfig);\n\n const result = await instrumentReactRouterConfig(true);\n\n expect(result.ssrWasChanged).toBe(false);\n });\n\n it('should add buildEnd hook to existing config', async () => {\n const existingConfig = `export default {\n ssr: true,\n async: false\n};`;\n\n const writtenFiles: Record<string, string> = {};\n\n vi.mocked(fs.existsSync).mockReturnValue(true);\n vi.mocked(fs.promises.readFile).mockResolvedValue(existingConfig);\n vi.mocked(fs.promises.writeFile).mockImplementation(\n (filePath, content) => {\n writtenFiles[filePath as string] = content as string;\n return Promise.resolve();\n },\n );\n\n const result = await instrumentReactRouterConfig(true);\n\n expect(result.ssrWasChanged).toBe(false);\n\n const writtenConfig = Object.values(writtenFiles)[0];\n expect(writtenConfig).toContain('sentryOnBuildEnd');\n expect(writtenConfig).toContain('buildEnd:');\n });\n\n it('should set ssr: true if missing', async () => {\n const existingConfig = `export default {\n async: false\n};`;\n\n const writtenFiles: Record<string, string> = {};\n\n vi.mocked(fs.existsSync).mockReturnValue(true);\n vi.mocked(fs.promises.readFile).mockResolvedValue(existingConfig);\n vi.mocked(fs.promises.writeFile).mockImplementation(\n (filePath, content) => {\n writtenFiles[filePath as string] = content as string;\n return Promise.resolve();\n },\n );\n\n const result = await instrumentReactRouterConfig(true);\n\n expect(result.ssrWasChanged).toBe(true);\n\n const writtenConfig = Object.values(writtenFiles)[0];\n expect(writtenConfig).toContain('ssr: true');\n });\n\n it('should report ssrWasChanged when changing ssr from false to true', async () => {\n const existingConfig = `export default {\n ssr: false\n};`;\n\n const writtenFiles: Record<string, string> = {};\n\n vi.mocked(fs.existsSync).mockReturnValue(true);\n vi.mocked(fs.promises.readFile).mockResolvedValue(existingConfig);\n vi.mocked(fs.promises.writeFile).mockImplementation(\n (filePath, content) => {\n writtenFiles[filePath as string] = content as string;\n return Promise.resolve();\n },\n );\n\n const result = await instrumentReactRouterConfig(true);\n\n expect(result.ssrWasChanged).toBe(true);\n\n const writtenConfig = Object.values(writtenFiles)[0];\n expect(writtenConfig).toContain('ssr: true');\n });\n\n it('should throw error if buildEnd already exists', async () => {\n const existingConfig = `export default {\n ssr: true,\n buildEnd: async () => {\n console.log('existing hook');\n }\n};`;\n\n vi.mocked(fs.existsSync).mockReturnValue(true);\n vi.mocked(fs.promises.readFile).mockResolvedValue(existingConfig);\n\n await expect(instrumentReactRouterConfig(true)).rejects.toThrow(\n 'A buildEnd hook already exists',\n );\n });\n });\n});\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ const vitest_1 = require("vitest");
27
+ const fs = __importStar(require("fs"));
28
+ const path = __importStar(require("path"));
29
+ const root_1 = require("../../../src/react-router/codemods/root");
30
+ vitest_1.vi.mock('@clack/prompts', () => {
31
+ const mock = {
32
+ log: {
33
+ warn: vitest_1.vi.fn(),
34
+ info: vitest_1.vi.fn(),
35
+ success: vitest_1.vi.fn(),
36
+ },
37
+ };
38
+ return {
39
+ default: mock,
40
+ ...mock,
41
+ };
42
+ });
43
+ vitest_1.vi.mock('../../../src/utils/debug', () => ({
44
+ debug: vitest_1.vi.fn(),
45
+ }));
46
+ (0, vitest_1.describe)('instrumentRoot', () => {
47
+ const fixturesDir = path.join(__dirname, 'fixtures', 'root');
48
+ let tmpDir;
49
+ let appDir;
50
+ (0, vitest_1.beforeEach)(() => {
51
+ vitest_1.vi.clearAllMocks();
52
+ // Create unique tmp directory for each test
53
+ tmpDir = path.join(fixturesDir, 'tmp', `test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`);
54
+ appDir = path.join(tmpDir, 'app');
55
+ // Ensure tmp and app directories exist
56
+ fs.mkdirSync(appDir, { recursive: true });
57
+ // Mock process.cwd() to return the tmp directory
58
+ vitest_1.vi.spyOn(process, 'cwd').mockReturnValue(tmpDir);
59
+ });
60
+ (0, vitest_1.afterEach)(() => {
61
+ // Clean up tmp directory
62
+ if (fs.existsSync(tmpDir)) {
63
+ fs.rmSync(tmpDir, { recursive: true });
64
+ }
65
+ vitest_1.vi.restoreAllMocks();
66
+ });
67
+ (0, vitest_1.it)('should add ErrorBoundary when no ErrorBoundary exists and no Sentry content', async () => {
68
+ // Copy fixture to tmp directory for testing
69
+ const srcFile = path.join(fixturesDir, 'no-error-boundary.tsx');
70
+ // Create app directory and copy file
71
+ fs.copyFileSync(srcFile, path.join(appDir, 'root.tsx'));
72
+ // Mock process.cwd() to return tmpDir
73
+ await (0, root_1.instrumentRoot)('root.tsx');
74
+ // Check that the file was modified correctly
75
+ const modifiedContent = fs.readFileSync(path.join(appDir, 'root.tsx'), 'utf8');
76
+ (0, vitest_1.expect)(modifiedContent).toContain('import * as Sentry from "@sentry/react-router";');
77
+ (0, vitest_1.expect)(modifiedContent).toContain("import { Outlet, isRouteErrorResponse } from 'react-router';");
78
+ (0, vitest_1.expect)(modifiedContent).toContain('export function ErrorBoundary({ error })');
79
+ (0, vitest_1.expect)(modifiedContent).toContain('Sentry.captureException(error);');
80
+ (0, vitest_1.expect)(modifiedContent).toContain('if (isRouteErrorResponse(error))');
81
+ });
82
+ (0, vitest_1.it)('should add Sentry.captureException to existing function declaration ErrorBoundary', async () => {
83
+ const srcFile = path.join(fixturesDir, 'with-function-error-boundary.tsx');
84
+ fs.copyFileSync(srcFile, path.join(appDir, 'root.tsx'));
85
+ await (0, root_1.instrumentRoot)('root.tsx');
86
+ const modifiedContent = fs.readFileSync(path.join(appDir, 'root.tsx'), 'utf8');
87
+ (0, vitest_1.expect)(modifiedContent).toContain('import * as Sentry from "@sentry/react-router";');
88
+ (0, vitest_1.expect)(modifiedContent).toContain('Sentry.captureException(error);');
89
+ });
90
+ (0, vitest_1.it)('should add Sentry.captureException to existing variable declaration ErrorBoundary', async () => {
91
+ const srcFile = path.join(fixturesDir, 'with-variable-error-boundary.tsx');
92
+ fs.copyFileSync(srcFile, path.join(appDir, 'root.tsx'));
93
+ await (0, root_1.instrumentRoot)('root.tsx');
94
+ const modifiedContent = fs.readFileSync(path.join(appDir, 'root.tsx'), 'utf8');
95
+ (0, vitest_1.expect)(modifiedContent).toContain('import * as Sentry from "@sentry/react-router";');
96
+ // Now properly handles variable declaration ErrorBoundary
97
+ (0, vitest_1.expect)(modifiedContent).toContain('Sentry.captureException(error);');
98
+ });
99
+ (0, vitest_1.it)('should not modify file when ErrorBoundary already has Sentry.captureException', async () => {
100
+ const srcFile = path.join(fixturesDir, 'with-sentry-error-boundary.tsx');
101
+ fs.copyFileSync(srcFile, path.join(appDir, 'root.tsx'));
102
+ await (0, root_1.instrumentRoot)('root.tsx');
103
+ const modifiedContent = fs.readFileSync(path.join(appDir, 'root.tsx'), 'utf8');
104
+ // Should not add duplicate Sentry.captureException
105
+ const captureExceptionOccurrences = (modifiedContent.match(/Sentry\.captureException/g) || []).length;
106
+ (0, vitest_1.expect)(captureExceptionOccurrences).toBe(1);
107
+ });
108
+ (0, vitest_1.it)('should not add Sentry import when Sentry content already exists', async () => {
109
+ const srcFile = path.join(fixturesDir, 'with-existing-sentry.tsx');
110
+ fs.copyFileSync(srcFile, path.join(appDir, 'root.tsx'));
111
+ await (0, root_1.instrumentRoot)('root.tsx');
112
+ const modifiedContent = fs.readFileSync(path.join(appDir, 'root.tsx'), 'utf8');
113
+ // Should not duplicate Sentry imports
114
+ const sentryImportOccurrences = (modifiedContent.match(/import.*@sentry\/react-router/g) || []).length;
115
+ (0, vitest_1.expect)(sentryImportOccurrences).toBe(1);
116
+ });
117
+ (0, vitest_1.it)('should add isRouteErrorResponse import when not present and ErrorBoundary is added', async () => {
118
+ const srcFile = path.join(fixturesDir, 'no-isrouteerrorresponse.tsx');
119
+ fs.copyFileSync(srcFile, path.join(appDir, 'root.tsx'));
120
+ await (0, root_1.instrumentRoot)('root.tsx');
121
+ const modifiedContent = fs.readFileSync(path.join(appDir, 'root.tsx'), 'utf8');
122
+ (0, vitest_1.expect)(modifiedContent).toContain("import { Outlet, isRouteErrorResponse } from 'react-router';");
123
+ (0, vitest_1.expect)(modifiedContent).toContain('export function ErrorBoundary({ error })');
124
+ });
125
+ (0, vitest_1.it)('should not add duplicate isRouteErrorResponse import when already present', async () => {
126
+ const srcFile = path.join(fixturesDir, 'with-isrouteerrorresponse.tsx');
127
+ fs.copyFileSync(srcFile, path.join(appDir, 'root.tsx'));
128
+ await (0, root_1.instrumentRoot)('root.tsx');
129
+ const modifiedContent = fs.readFileSync(path.join(appDir, 'root.tsx'), 'utf8');
130
+ // Should not duplicate isRouteErrorResponse imports
131
+ const isRouteErrorResponseOccurrences = (modifiedContent.match(/isRouteErrorResponse/g) || []).length;
132
+ (0, vitest_1.expect)(isRouteErrorResponseOccurrences).toBe(2); // One import, one usage in template
133
+ });
134
+ (0, vitest_1.it)('should handle ErrorBoundary with alternative function declaration syntax', async () => {
135
+ const srcFile = path.join(fixturesDir, 'function-expression-error-boundary.tsx');
136
+ fs.copyFileSync(srcFile, path.join(appDir, 'root.tsx'));
137
+ await (0, root_1.instrumentRoot)('root.tsx');
138
+ const modifiedContent = fs.readFileSync(path.join(appDir, 'root.tsx'), 'utf8');
139
+ (0, vitest_1.expect)(modifiedContent).toContain('import * as Sentry from "@sentry/react-router";');
140
+ (0, vitest_1.expect)(modifiedContent).toContain('Sentry.captureException(error);');
141
+ });
142
+ (0, vitest_1.it)('should handle function declaration with separate export', async () => {
143
+ const srcFile = path.join(fixturesDir, 'function-declaration-separate-export.tsx');
144
+ fs.copyFileSync(srcFile, path.join(appDir, 'root.tsx'));
145
+ await (0, root_1.instrumentRoot)('root.tsx');
146
+ const modifiedContent = fs.readFileSync(path.join(appDir, 'root.tsx'), 'utf8');
147
+ (0, vitest_1.expect)(modifiedContent).toContain('import * as Sentry from "@sentry/react-router";');
148
+ (0, vitest_1.expect)(modifiedContent).toContain('Sentry.captureException(error);');
149
+ // Should preserve function declaration syntax
150
+ (0, vitest_1.expect)(modifiedContent).toMatch(/function ErrorBoundary\(/);
151
+ (0, vitest_1.expect)(modifiedContent).toContain('export { ErrorBoundary }');
152
+ });
153
+ (0, vitest_1.it)('should handle ErrorBoundary with captureException imported directly', async () => {
154
+ const srcFile = path.join(fixturesDir, 'with-direct-capture-exception.tsx');
155
+ fs.copyFileSync(srcFile, path.join(appDir, 'root.tsx'));
156
+ await (0, root_1.instrumentRoot)('root.tsx');
157
+ const modifiedContent = fs.readFileSync(path.join(appDir, 'root.tsx'), 'utf8');
158
+ // Should not add duplicate captureException calls
159
+ const captureExceptionOccurrences = (modifiedContent.match(/captureException/g) || []).length;
160
+ (0, vitest_1.expect)(captureExceptionOccurrences).toBe(2); // One import, one usage
161
+ });
162
+ (0, vitest_1.it)('should not modify an already properly configured file', async () => {
163
+ const srcFile = path.join(fixturesDir, 'fully-configured.tsx');
164
+ fs.copyFileSync(srcFile, path.join(appDir, 'root.tsx'));
165
+ await (0, root_1.instrumentRoot)('root.tsx');
166
+ const modifiedContent = fs.readFileSync(path.join(appDir, 'root.tsx'), 'utf8');
167
+ // Should not add duplicate imports or modify existing Sentry configuration
168
+ const sentryImportOccurrences = (modifiedContent.match(/import.*@sentry\/react-router/g) || []).length;
169
+ (0, vitest_1.expect)(sentryImportOccurrences).toBe(1);
170
+ const captureExceptionOccurrences = (modifiedContent.match(/Sentry\.captureException/g) || []).length;
171
+ (0, vitest_1.expect)(captureExceptionOccurrences).toBe(1);
172
+ const errorBoundaryOccurrences = (modifiedContent.match(/export function ErrorBoundary/g) || []).length;
173
+ (0, vitest_1.expect)(errorBoundaryOccurrences).toBe(1);
174
+ (0, vitest_1.expect)(modifiedContent).toContain("import * as Sentry from '@sentry/react-router';");
175
+ (0, vitest_1.expect)(modifiedContent).toContain("import { Outlet, isRouteErrorResponse } from 'react-router';");
176
+ });
177
+ });
178
+ //# sourceMappingURL=root.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root.test.js","sourceRoot":"","sources":["../../../../test/react-router/codemods/root.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { instrumentRoot } from '../../../src/react-router/codemods/root';\n\nvi.mock('@clack/prompts', () => {\n const mock = {\n log: {\n warn: vi.fn(),\n info: vi.fn(),\n success: vi.fn(),\n },\n };\n return {\n default: mock,\n ...mock,\n };\n});\n\nvi.mock('../../../src/utils/debug', () => ({\n debug: vi.fn(),\n}));\n\ndescribe('instrumentRoot', () => {\n const fixturesDir = path.join(__dirname, 'fixtures', 'root');\n let tmpDir: string;\n let appDir: string;\n\n beforeEach(() => {\n vi.clearAllMocks();\n\n // Create unique tmp directory for each test\n tmpDir = path.join(\n fixturesDir,\n 'tmp',\n `test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,\n );\n appDir = path.join(tmpDir, 'app');\n\n // Ensure tmp and app directories exist\n fs.mkdirSync(appDir, { recursive: true });\n\n // Mock process.cwd() to return the tmp directory\n vi.spyOn(process, 'cwd').mockReturnValue(tmpDir);\n });\n\n afterEach(() => {\n // Clean up tmp directory\n if (fs.existsSync(tmpDir)) {\n fs.rmSync(tmpDir, { recursive: true });\n }\n vi.restoreAllMocks();\n });\n\n it('should add ErrorBoundary when no ErrorBoundary exists and no Sentry content', async () => {\n // Copy fixture to tmp directory for testing\n const srcFile = path.join(fixturesDir, 'no-error-boundary.tsx');\n\n // Create app directory and copy file\n fs.copyFileSync(srcFile, path.join(appDir, 'root.tsx'));\n\n // Mock process.cwd() to return tmpDir\n\n await instrumentRoot('root.tsx');\n\n // Check that the file was modified correctly\n const modifiedContent = fs.readFileSync(\n path.join(appDir, 'root.tsx'),\n 'utf8',\n );\n\n expect(modifiedContent).toContain(\n 'import * as Sentry from \"@sentry/react-router\";',\n );\n expect(modifiedContent).toContain(\n \"import { Outlet, isRouteErrorResponse } from 'react-router';\",\n );\n expect(modifiedContent).toContain(\n 'export function ErrorBoundary({ error })',\n );\n expect(modifiedContent).toContain('Sentry.captureException(error);');\n expect(modifiedContent).toContain('if (isRouteErrorResponse(error))');\n });\n\n it('should add Sentry.captureException to existing function declaration ErrorBoundary', async () => {\n const srcFile = path.join(fixturesDir, 'with-function-error-boundary.tsx');\n\n fs.copyFileSync(srcFile, path.join(appDir, 'root.tsx'));\n\n await instrumentRoot('root.tsx');\n\n const modifiedContent = fs.readFileSync(\n path.join(appDir, 'root.tsx'),\n 'utf8',\n );\n\n expect(modifiedContent).toContain(\n 'import * as Sentry from \"@sentry/react-router\";',\n );\n expect(modifiedContent).toContain('Sentry.captureException(error);');\n });\n\n it('should add Sentry.captureException to existing variable declaration ErrorBoundary', async () => {\n const srcFile = path.join(fixturesDir, 'with-variable-error-boundary.tsx');\n\n fs.copyFileSync(srcFile, path.join(appDir, 'root.tsx'));\n\n await instrumentRoot('root.tsx');\n\n const modifiedContent = fs.readFileSync(\n path.join(appDir, 'root.tsx'),\n 'utf8',\n );\n\n expect(modifiedContent).toContain(\n 'import * as Sentry from \"@sentry/react-router\";',\n );\n // Now properly handles variable declaration ErrorBoundary\n expect(modifiedContent).toContain('Sentry.captureException(error);');\n });\n\n it('should not modify file when ErrorBoundary already has Sentry.captureException', async () => {\n const srcFile = path.join(fixturesDir, 'with-sentry-error-boundary.tsx');\n\n fs.copyFileSync(srcFile, path.join(appDir, 'root.tsx'));\n\n await instrumentRoot('root.tsx');\n\n const modifiedContent = fs.readFileSync(\n path.join(appDir, 'root.tsx'),\n 'utf8',\n );\n\n // Should not add duplicate Sentry.captureException\n const captureExceptionOccurrences = (\n modifiedContent.match(/Sentry\\.captureException/g) || []\n ).length;\n expect(captureExceptionOccurrences).toBe(1);\n });\n\n it('should not add Sentry import when Sentry content already exists', async () => {\n const srcFile = path.join(fixturesDir, 'with-existing-sentry.tsx');\n\n fs.copyFileSync(srcFile, path.join(appDir, 'root.tsx'));\n\n await instrumentRoot('root.tsx');\n\n const modifiedContent = fs.readFileSync(\n path.join(appDir, 'root.tsx'),\n 'utf8',\n );\n\n // Should not duplicate Sentry imports\n const sentryImportOccurrences = (\n modifiedContent.match(/import.*@sentry\\/react-router/g) || []\n ).length;\n expect(sentryImportOccurrences).toBe(1);\n });\n\n it('should add isRouteErrorResponse import when not present and ErrorBoundary is added', async () => {\n const srcFile = path.join(fixturesDir, 'no-isrouteerrorresponse.tsx');\n\n fs.copyFileSync(srcFile, path.join(appDir, 'root.tsx'));\n\n await instrumentRoot('root.tsx');\n\n const modifiedContent = fs.readFileSync(\n path.join(appDir, 'root.tsx'),\n 'utf8',\n );\n\n expect(modifiedContent).toContain(\n \"import { Outlet, isRouteErrorResponse } from 'react-router';\",\n );\n expect(modifiedContent).toContain(\n 'export function ErrorBoundary({ error })',\n );\n });\n\n it('should not add duplicate isRouteErrorResponse import when already present', async () => {\n const srcFile = path.join(fixturesDir, 'with-isrouteerrorresponse.tsx');\n\n fs.copyFileSync(srcFile, path.join(appDir, 'root.tsx'));\n\n await instrumentRoot('root.tsx');\n\n const modifiedContent = fs.readFileSync(\n path.join(appDir, 'root.tsx'),\n 'utf8',\n );\n\n // Should not duplicate isRouteErrorResponse imports\n const isRouteErrorResponseOccurrences = (\n modifiedContent.match(/isRouteErrorResponse/g) || []\n ).length;\n expect(isRouteErrorResponseOccurrences).toBe(2); // One import, one usage in template\n });\n\n it('should handle ErrorBoundary with alternative function declaration syntax', async () => {\n const srcFile = path.join(\n fixturesDir,\n 'function-expression-error-boundary.tsx',\n );\n\n fs.copyFileSync(srcFile, path.join(appDir, 'root.tsx'));\n\n await instrumentRoot('root.tsx');\n\n const modifiedContent = fs.readFileSync(\n path.join(appDir, 'root.tsx'),\n 'utf8',\n );\n\n expect(modifiedContent).toContain(\n 'import * as Sentry from \"@sentry/react-router\";',\n );\n expect(modifiedContent).toContain('Sentry.captureException(error);');\n });\n\n it('should handle function declaration with separate export', async () => {\n const srcFile = path.join(\n fixturesDir,\n 'function-declaration-separate-export.tsx',\n );\n\n fs.copyFileSync(srcFile, path.join(appDir, 'root.tsx'));\n\n await instrumentRoot('root.tsx');\n\n const modifiedContent = fs.readFileSync(\n path.join(appDir, 'root.tsx'),\n 'utf8',\n );\n\n expect(modifiedContent).toContain(\n 'import * as Sentry from \"@sentry/react-router\";',\n );\n expect(modifiedContent).toContain('Sentry.captureException(error);');\n\n // Should preserve function declaration syntax\n expect(modifiedContent).toMatch(/function ErrorBoundary\\(/);\n expect(modifiedContent).toContain('export { ErrorBoundary }');\n });\n\n it('should handle ErrorBoundary with captureException imported directly', async () => {\n const srcFile = path.join(fixturesDir, 'with-direct-capture-exception.tsx');\n\n fs.copyFileSync(srcFile, path.join(appDir, 'root.tsx'));\n\n await instrumentRoot('root.tsx');\n\n const modifiedContent = fs.readFileSync(\n path.join(appDir, 'root.tsx'),\n 'utf8',\n );\n\n // Should not add duplicate captureException calls\n const captureExceptionOccurrences = (\n modifiedContent.match(/captureException/g) || []\n ).length;\n expect(captureExceptionOccurrences).toBe(2); // One import, one usage\n });\n\n it('should not modify an already properly configured file', async () => {\n const srcFile = path.join(fixturesDir, 'fully-configured.tsx');\n\n fs.copyFileSync(srcFile, path.join(appDir, 'root.tsx'));\n\n await instrumentRoot('root.tsx');\n\n const modifiedContent = fs.readFileSync(\n path.join(appDir, 'root.tsx'),\n 'utf8',\n );\n\n // Should not add duplicate imports or modify existing Sentry configuration\n const sentryImportOccurrences = (\n modifiedContent.match(/import.*@sentry\\/react-router/g) || []\n ).length;\n expect(sentryImportOccurrences).toBe(1);\n\n const captureExceptionOccurrences = (\n modifiedContent.match(/Sentry\\.captureException/g) || []\n ).length;\n expect(captureExceptionOccurrences).toBe(1);\n\n const errorBoundaryOccurrences = (\n modifiedContent.match(/export function ErrorBoundary/g) || []\n ).length;\n expect(errorBoundaryOccurrences).toBe(1);\n\n expect(modifiedContent).toContain(\n \"import * as Sentry from '@sentry/react-router';\",\n );\n expect(modifiedContent).toContain(\n \"import { Outlet, isRouteErrorResponse } from 'react-router';\",\n );\n });\n});\n"]}