@sentry/wizard 6.6.1 → 6.7.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 (114) hide show
  1. package/CHANGELOG.md +22 -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/react-router.test.d.ts +1 -0
  10. package/dist/e2e-tests/tests/react-router.test.js +255 -0
  11. package/dist/e2e-tests/tests/react-router.test.js.map +1 -0
  12. package/dist/e2e-tests/utils/index.d.ts +8 -2
  13. package/dist/e2e-tests/utils/index.js +72 -21
  14. package/dist/e2e-tests/utils/index.js.map +1 -1
  15. package/dist/lib/Constants.d.ts +1 -0
  16. package/dist/lib/Constants.js +5 -0
  17. package/dist/lib/Constants.js.map +1 -1
  18. package/dist/src/android/android-wizard.js +8 -1
  19. package/dist/src/android/android-wizard.js.map +1 -1
  20. package/dist/src/angular/angular-wizard.js +8 -1
  21. package/dist/src/angular/angular-wizard.js.map +1 -1
  22. package/dist/src/apple/apple-wizard.js +8 -1
  23. package/dist/src/apple/apple-wizard.js.map +1 -1
  24. package/dist/src/flutter/flutter-wizard.js +8 -1
  25. package/dist/src/flutter/flutter-wizard.js.map +1 -1
  26. package/dist/src/nextjs/nextjs-wizard.js +35 -9
  27. package/dist/src/nextjs/nextjs-wizard.js.map +1 -1
  28. package/dist/src/nextjs/templates.d.ts +3 -3
  29. package/dist/src/nextjs/templates.js +18 -7
  30. package/dist/src/nextjs/templates.js.map +1 -1
  31. package/dist/src/nuxt/nuxt-wizard.js +8 -1
  32. package/dist/src/nuxt/nuxt-wizard.js.map +1 -1
  33. package/dist/src/react-native/react-native-wizard.js +8 -1
  34. package/dist/src/react-native/react-native-wizard.js.map +1 -1
  35. package/dist/src/react-router/codemods/client.entry.d.ts +1 -0
  36. package/dist/src/react-router/codemods/client.entry.js +73 -0
  37. package/dist/src/react-router/codemods/client.entry.js.map +1 -0
  38. package/dist/src/react-router/codemods/react-router-config.d.ts +9 -0
  39. package/dist/src/react-router/codemods/react-router-config.js +178 -0
  40. package/dist/src/react-router/codemods/react-router-config.js.map +1 -0
  41. package/dist/src/react-router/codemods/root.d.ts +1 -0
  42. package/dist/src/react-router/codemods/root.js +171 -0
  43. package/dist/src/react-router/codemods/root.js.map +1 -0
  44. package/dist/src/react-router/codemods/routes-config.d.ts +1 -0
  45. package/dist/src/react-router/codemods/routes-config.js +106 -0
  46. package/dist/src/react-router/codemods/routes-config.js.map +1 -0
  47. package/dist/src/react-router/codemods/server-entry.d.ts +4 -0
  48. package/dist/src/react-router/codemods/server-entry.js +275 -0
  49. package/dist/src/react-router/codemods/server-entry.js.map +1 -0
  50. package/dist/src/react-router/codemods/utils.d.ts +2 -0
  51. package/dist/src/react-router/codemods/utils.js +13 -0
  52. package/dist/src/react-router/codemods/utils.js.map +1 -0
  53. package/dist/src/react-router/codemods/vite.d.ts +8 -0
  54. package/dist/src/react-router/codemods/vite.js +169 -0
  55. package/dist/src/react-router/codemods/vite.js.map +1 -0
  56. package/dist/src/react-router/react-router-wizard.d.ts +2 -0
  57. package/dist/src/react-router/react-router-wizard.js +254 -0
  58. package/dist/src/react-router/react-router-wizard.js.map +1 -0
  59. package/dist/src/react-router/sdk-example.d.ts +18 -0
  60. package/dist/src/react-router/sdk-example.js +306 -0
  61. package/dist/src/react-router/sdk-example.js.map +1 -0
  62. package/dist/src/react-router/sdk-setup.d.ts +17 -0
  63. package/dist/src/react-router/sdk-setup.js +250 -0
  64. package/dist/src/react-router/sdk-setup.js.map +1 -0
  65. package/dist/src/react-router/templates.d.ts +11 -0
  66. package/dist/src/react-router/templates.js +273 -0
  67. package/dist/src/react-router/templates.js.map +1 -0
  68. package/dist/src/remix/remix-wizard.js +8 -1
  69. package/dist/src/remix/remix-wizard.js.map +1 -1
  70. package/dist/src/run.d.ts +2 -1
  71. package/dist/src/run.js +6 -0
  72. package/dist/src/run.js.map +1 -1
  73. package/dist/src/sourcemaps/sourcemaps-wizard.js +8 -1
  74. package/dist/src/sourcemaps/sourcemaps-wizard.js.map +1 -1
  75. package/dist/src/sveltekit/sveltekit-wizard.js +8 -1
  76. package/dist/src/sveltekit/sveltekit-wizard.js.map +1 -1
  77. package/dist/src/utils/ast-utils.d.ts +30 -0
  78. package/dist/src/utils/ast-utils.js +71 -1
  79. package/dist/src/utils/ast-utils.js.map +1 -1
  80. package/dist/src/utils/clack/index.d.ts +5 -2
  81. package/dist/src/utils/clack/index.js +8 -0
  82. package/dist/src/utils/clack/index.js.map +1 -1
  83. package/dist/src/utils/types.d.ts +9 -0
  84. package/dist/src/utils/types.js.map +1 -1
  85. package/dist/src/version.d.ts +1 -1
  86. package/dist/src/version.js +1 -1
  87. package/dist/src/version.js.map +1 -1
  88. package/dist/test/nextjs/templates.test.js +20 -0
  89. package/dist/test/nextjs/templates.test.js.map +1 -1
  90. package/dist/test/react-router/codemods/client-entry.test.d.ts +1 -0
  91. package/dist/test/react-router/codemods/client-entry.test.js +168 -0
  92. package/dist/test/react-router/codemods/client-entry.test.js.map +1 -0
  93. package/dist/test/react-router/codemods/react-router-config.test.d.ts +1 -0
  94. package/dist/test/react-router/codemods/react-router-config.test.js +168 -0
  95. package/dist/test/react-router/codemods/react-router-config.test.js.map +1 -0
  96. package/dist/test/react-router/codemods/root.test.d.ts +1 -0
  97. package/dist/test/react-router/codemods/root.test.js +178 -0
  98. package/dist/test/react-router/codemods/root.test.js.map +1 -0
  99. package/dist/test/react-router/codemods/server-entry.test.d.ts +1 -0
  100. package/dist/test/react-router/codemods/server-entry.test.js +415 -0
  101. package/dist/test/react-router/codemods/server-entry.test.js.map +1 -0
  102. package/dist/test/react-router/codemods/vite.test.d.ts +1 -0
  103. package/dist/test/react-router/codemods/vite.test.js +158 -0
  104. package/dist/test/react-router/codemods/vite.test.js.map +1 -0
  105. package/dist/test/react-router/routes-config.test.d.ts +1 -0
  106. package/dist/test/react-router/routes-config.test.js +156 -0
  107. package/dist/test/react-router/routes-config.test.js.map +1 -0
  108. package/dist/test/react-router/sdk-setup.test.d.ts +1 -0
  109. package/dist/test/react-router/sdk-setup.test.js +411 -0
  110. package/dist/test/react-router/sdk-setup.test.js.map +1 -0
  111. package/dist/test/react-router/templates.test.d.ts +1 -0
  112. package/dist/test/react-router/templates.test.js +220 -0
  113. package/dist/test/react-router/templates.test.js.map +1 -0
  114. package/package.json +2 -2
@@ -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"]}
@@ -0,0 +1,415 @@
1
+ "use strict";
2
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
3
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
4
+ /* eslint-disable @typescript-eslint/no-explicit-any */
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || function (mod) {
22
+ if (mod && mod.__esModule) return mod;
23
+ var result = {};
24
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
25
+ __setModuleDefault(result, mod);
26
+ return result;
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ const vitest_1 = require("vitest");
30
+ const fs = __importStar(require("fs"));
31
+ const path = __importStar(require("path"));
32
+ const recast = __importStar(require("recast"));
33
+ const server_entry_1 = require("../../../src/react-router/codemods/server-entry");
34
+ // @ts-expect-error - magicast is ESM and TS complains about that. It works though
35
+ const magicast_1 = require("magicast");
36
+ vitest_1.vi.mock('@clack/prompts', () => {
37
+ const mock = {
38
+ log: {
39
+ warn: vitest_1.vi.fn(),
40
+ info: vitest_1.vi.fn(),
41
+ success: vitest_1.vi.fn(),
42
+ },
43
+ };
44
+ return {
45
+ default: mock,
46
+ ...mock,
47
+ };
48
+ });
49
+ vitest_1.vi.mock('../../../src/utils/debug', () => ({
50
+ debug: vitest_1.vi.fn(),
51
+ }));
52
+ (0, vitest_1.describe)('instrumentServerEntry', () => {
53
+ const fixturesDir = path.join(__dirname, 'fixtures', 'server-entry');
54
+ let tmpDir;
55
+ let tmpFile;
56
+ (0, vitest_1.beforeEach)(() => {
57
+ vitest_1.vi.clearAllMocks();
58
+ // Create unique tmp directory for each test
59
+ tmpDir = path.join(__dirname, 'fixtures', 'tmp', `test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`);
60
+ tmpFile = path.join(tmpDir, 'entry.server.tsx');
61
+ // Ensure tmp directory exists
62
+ fs.mkdirSync(tmpDir, { recursive: true });
63
+ });
64
+ (0, vitest_1.afterEach)(() => {
65
+ // Clean up tmp directory
66
+ if (fs.existsSync(tmpDir)) {
67
+ fs.rmSync(tmpDir, { recursive: true });
68
+ }
69
+ });
70
+ (0, vitest_1.it)('should add Sentry import and wrap handleRequest function', async () => {
71
+ const basicContent = fs.readFileSync(path.join(fixturesDir, 'basic.tsx'), 'utf8');
72
+ fs.writeFileSync(tmpFile, basicContent);
73
+ await (0, server_entry_1.instrumentServerEntry)(tmpFile);
74
+ const modifiedContent = fs.readFileSync(tmpFile, 'utf8');
75
+ // Should add Sentry import
76
+ (0, vitest_1.expect)(modifiedContent).toContain('import * as Sentry from "@sentry/react-router";');
77
+ // Should wrap the existing handleRequest function
78
+ (0, vitest_1.expect)(modifiedContent).toContain('export default Sentry.wrapSentryHandleRequest(handleRequest);');
79
+ // Should add the Sentry import at the top of the file (after existing imports)
80
+ const lines = modifiedContent.split('\n');
81
+ const sentryImportLine = lines.findIndex((line) => line.includes('import * as Sentry from "@sentry/react-router";'));
82
+ (0, vitest_1.expect)(sentryImportLine).toBeGreaterThanOrEqual(0);
83
+ // Should create default handleError since none exists
84
+ (0, vitest_1.expect)(modifiedContent).toContain('export const handleError = Sentry.createSentryHandleError({');
85
+ (0, vitest_1.expect)(modifiedContent).toContain('logErrors: false');
86
+ });
87
+ (0, vitest_1.it)('should handle already instrumented server entry without duplication', async () => {
88
+ const alreadyInstrumentedContent = fs.readFileSync(path.join(fixturesDir, 'already-instrumented.tsx'), 'utf8');
89
+ fs.writeFileSync(tmpFile, alreadyInstrumentedContent);
90
+ await (0, server_entry_1.instrumentServerEntry)(tmpFile);
91
+ const modifiedContent = fs.readFileSync(tmpFile, 'utf8');
92
+ // Should not add duplicate imports or wrapping since already instrumented
93
+ (0, vitest_1.expect)(modifiedContent).toContain("import * as Sentry from '@sentry/react-router';");
94
+ (0, vitest_1.expect)(modifiedContent).toContain('export default Sentry.wrapSentryHandleRequest(handleRequest);');
95
+ // Should NOT add a new createSentryHandleError export since handleError already has captureException
96
+ (0, vitest_1.expect)(modifiedContent).not.toContain('export const handleError = Sentry.createSentryHandleError({');
97
+ // Should preserve the existing handleError function with captureException
98
+ (0, vitest_1.expect)(modifiedContent).toContain('Sentry.captureException(error);');
99
+ (0, vitest_1.expect)(modifiedContent).toContain('export async function handleError');
100
+ });
101
+ (0, vitest_1.it)('should handle variable export pattern with existing export', async () => {
102
+ const variableExportContent = fs.readFileSync(path.join(fixturesDir, 'variable-export.tsx'), 'utf8');
103
+ fs.writeFileSync(tmpFile, variableExportContent);
104
+ await (0, server_entry_1.instrumentServerEntry)(tmpFile);
105
+ const modifiedContent = fs.readFileSync(tmpFile, 'utf8');
106
+ // Should add Sentry import and wrap handleRequest
107
+ (0, vitest_1.expect)(modifiedContent).toContain('import * as Sentry from "@sentry/react-router";');
108
+ (0, vitest_1.expect)(modifiedContent).toContain('export default Sentry.wrapSentryHandleRequest(handleRequest);');
109
+ // Should instrument the existing handleError variable with captureException
110
+ (0, vitest_1.expect)(modifiedContent).toContain('Sentry.captureException(error);');
111
+ // Should preserve the variable export pattern
112
+ (0, vitest_1.expect)(modifiedContent).toContain('export const handleError');
113
+ });
114
+ });
115
+ (0, vitest_1.describe)('instrumentHandleRequest', () => {
116
+ let tmpDir;
117
+ (0, vitest_1.beforeEach)(() => {
118
+ tmpDir = path.join(__dirname, 'fixtures', 'tmp', `handle-request-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`);
119
+ fs.mkdirSync(tmpDir, { recursive: true });
120
+ });
121
+ (0, vitest_1.afterEach)(() => {
122
+ if (fs.existsSync(tmpDir)) {
123
+ fs.rmSync(tmpDir, { recursive: true });
124
+ }
125
+ });
126
+ (0, vitest_1.it)('should add required imports when creating new handleRequest', async () => {
127
+ const content = `// Empty server entry file`;
128
+ const tempFile = path.join(tmpDir, 'entry.server.tsx');
129
+ fs.writeFileSync(tempFile, content);
130
+ const mod = await (0, magicast_1.loadFile)(tempFile);
131
+ (0, server_entry_1.instrumentHandleRequest)(mod);
132
+ // Check if required imports were added
133
+ const imports = mod.imports.$items;
134
+ const hasServerRouter = imports.some((item) => item.imported === 'ServerRouter' && item.from === 'react-router');
135
+ const hasRenderToPipeableStream = imports.some((item) => item.imported === 'renderToPipeableStream' &&
136
+ item.from === 'react-dom/server');
137
+ (0, vitest_1.expect)(hasServerRouter).toBe(true);
138
+ (0, vitest_1.expect)(hasRenderToPipeableStream).toBe(true);
139
+ });
140
+ (0, vitest_1.it)('should not duplicate imports if they already exist', async () => {
141
+ const content = `
142
+ import { ServerRouter } from 'react-router';
143
+ import { renderToPipeableStream } from 'react-dom/server';
144
+ import { createReadableStreamFromReadable } from '@react-router/node';
145
+ `;
146
+ const tempFile = path.join(tmpDir, 'entry.server.tsx');
147
+ fs.writeFileSync(tempFile, content);
148
+ const mod = await (0, magicast_1.loadFile)(tempFile);
149
+ const originalImportsCount = mod.imports.$items.length;
150
+ (0, server_entry_1.instrumentHandleRequest)(mod);
151
+ // Should not add duplicate imports
152
+ (0, vitest_1.expect)(mod.imports.$items.length).toBe(originalImportsCount);
153
+ });
154
+ });
155
+ (0, vitest_1.describe)('instrumentHandleError', () => {
156
+ let tmpDir;
157
+ (0, vitest_1.beforeEach)(() => {
158
+ tmpDir = path.join(__dirname, 'fixtures', 'tmp', `handle-error-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`);
159
+ fs.mkdirSync(tmpDir, { recursive: true });
160
+ });
161
+ (0, vitest_1.afterEach)(() => {
162
+ if (fs.existsSync(tmpDir)) {
163
+ fs.rmSync(tmpDir, { recursive: true });
164
+ }
165
+ });
166
+ (0, vitest_1.it)('should not modify existing handleError with captureException', async () => {
167
+ const content = `
168
+ export function handleError(error: unknown) {
169
+ Sentry.captureException(error);
170
+ console.error(error);
171
+ }
172
+ `;
173
+ const tempFile = path.join(tmpDir, 'entry.server.tsx');
174
+ fs.writeFileSync(tempFile, content);
175
+ const mod = await (0, magicast_1.loadFile)(tempFile);
176
+ const originalBodyLength = mod.$ast.body.length;
177
+ (0, server_entry_1.instrumentHandleError)(mod);
178
+ // Should not modify since captureException already exists
179
+ (0, vitest_1.expect)(mod.$ast.body.length).toBe(originalBodyLength);
180
+ });
181
+ (0, vitest_1.it)('should not modify existing handleError with createSentryHandleError', async () => {
182
+ const content = `
183
+ export const handleError = Sentry.createSentryHandleError({
184
+ logErrors: false
185
+ });
186
+ `;
187
+ const tempFile = path.join(tmpDir, 'entry.server.tsx');
188
+ fs.writeFileSync(tempFile, content);
189
+ const mod = await (0, magicast_1.loadFile)(tempFile);
190
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
191
+ const originalBodyLength = mod.$ast.body.length;
192
+ (0, server_entry_1.instrumentHandleError)(mod);
193
+ // Should not modify since createSentryHandleError already exists
194
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
195
+ (0, vitest_1.expect)(mod.$ast.body.length).toBe(originalBodyLength);
196
+ });
197
+ (0, vitest_1.it)('should add captureException to existing handleError function declaration without breaking AST', async () => {
198
+ const content = `
199
+ export function handleError(error: unknown) {
200
+ console.error('Custom error handling:', error);
201
+ // some other logic here
202
+ }
203
+ `;
204
+ const tempFile = path.join(tmpDir, 'entry.server.tsx');
205
+ fs.writeFileSync(tempFile, content);
206
+ const mod = await (0, magicast_1.loadFile)(tempFile);
207
+ // This should not throw an error due to broken AST manipulation
208
+ (0, vitest_1.expect)(() => (0, server_entry_1.instrumentHandleError)(mod)).not.toThrow();
209
+ // Verify the function was modified correctly
210
+ const modifiedCode = (0, magicast_1.generateCode)(mod.$ast).code;
211
+ (0, vitest_1.expect)(modifiedCode).toContain('Sentry.captureException(error)');
212
+ (0, vitest_1.expect)(modifiedCode).toContain("console.error('Custom error handling:', error)");
213
+ });
214
+ (0, vitest_1.it)('should add captureException to existing handleError variable declaration without breaking AST', async () => {
215
+ const content = `
216
+ export const handleError = (error: unknown, { request }: { request: Request }) => {
217
+ console.log('Handling error:', error.message);
218
+ return new Response('Error occurred', { status: 500 });
219
+ };
220
+ `;
221
+ const tempFile = path.join(tmpDir, 'entry.server.tsx');
222
+ fs.writeFileSync(tempFile, content);
223
+ const mod = await (0, magicast_1.loadFile)(tempFile);
224
+ // This should not throw an error due to broken AST manipulation
225
+ (0, vitest_1.expect)(() => (0, server_entry_1.instrumentHandleError)(mod)).not.toThrow();
226
+ // Verify the function was modified correctly
227
+ const modifiedCode = (0, magicast_1.generateCode)(mod.$ast).code;
228
+ (0, vitest_1.expect)(modifiedCode).toContain('Sentry.captureException(error)');
229
+ (0, vitest_1.expect)(modifiedCode).toContain("console.log('Handling error:', error.message)");
230
+ });
231
+ (0, vitest_1.it)('should handle existing handleError with only error parameter and add request parameter', async () => {
232
+ const content = `
233
+ export const handleError = (error: unknown) => {
234
+ console.error('Simple error handler:', error);
235
+ };
236
+ `;
237
+ const tempFile = path.join(tmpDir, 'entry.server.tsx');
238
+ fs.writeFileSync(tempFile, content);
239
+ const mod = await (0, magicast_1.loadFile)(tempFile);
240
+ // This should not throw an error due to broken AST manipulation
241
+ (0, vitest_1.expect)(() => (0, server_entry_1.instrumentHandleError)(mod)).not.toThrow();
242
+ // Verify the function signature was updated correctly
243
+ const modifiedCode = (0, magicast_1.generateCode)(mod.$ast).code;
244
+ (0, vitest_1.expect)(modifiedCode).toContain('Sentry.captureException(error)');
245
+ (0, vitest_1.expect)(modifiedCode).toContain('if (!request.signal.aborted)');
246
+ // Should add request parameter
247
+ (0, vitest_1.expect)(modifiedCode).toMatch(/handleError.*=.*\(\s*error.*,\s*\{\s*request\s*\}/);
248
+ });
249
+ });
250
+ (0, vitest_1.describe)('instrumentHandleError AST manipulation edge cases', () => {
251
+ let tmpDir;
252
+ (0, vitest_1.beforeEach)(() => {
253
+ tmpDir = path.join(__dirname, 'fixtures', 'tmp', `ast-edge-cases-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`);
254
+ fs.mkdirSync(tmpDir, { recursive: true });
255
+ });
256
+ (0, vitest_1.afterEach)(() => {
257
+ if (fs.existsSync(tmpDir)) {
258
+ fs.rmSync(tmpDir, { recursive: true });
259
+ }
260
+ });
261
+ (0, vitest_1.it)('should handle function declaration with existing try-catch block', async () => {
262
+ const content = `
263
+ export function handleError(error: unknown, { request }: { request: Request }) {
264
+ try {
265
+ console.error('Error occurred:', error);
266
+ logToExternalService(error);
267
+ } catch (loggingError) {
268
+ console.warn('Failed to log error:', loggingError);
269
+ }
270
+ }
271
+ `;
272
+ const tempFile = path.join(tmpDir, 'entry.server.tsx');
273
+ fs.writeFileSync(tempFile, content);
274
+ const mod = await (0, magicast_1.loadFile)(tempFile);
275
+ // This test will expose the broken AST logic
276
+ (0, vitest_1.expect)(() => (0, server_entry_1.instrumentHandleError)(mod)).not.toThrow();
277
+ const modifiedCode = (0, magicast_1.generateCode)(mod.$ast).code;
278
+ (0, vitest_1.expect)(modifiedCode).toContain('Sentry.captureException(error)');
279
+ (0, vitest_1.expect)(modifiedCode).toContain('if (!request.signal.aborted)');
280
+ // Should preserve existing try-catch
281
+ (0, vitest_1.expect)(modifiedCode).toContain('try {');
282
+ (0, vitest_1.expect)(modifiedCode).toContain('} catch (loggingError) {');
283
+ });
284
+ (0, vitest_1.it)('should handle arrow function with block body', async () => {
285
+ const content = `
286
+ export const handleError = (error: unknown, context: any) => {
287
+ const { request } = context;
288
+ console.error('Error in route:', error);
289
+ return new Response('Internal Server Error', { status: 500 });
290
+ };
291
+ `;
292
+ const tempFile = path.join(tmpDir, 'entry.server.tsx');
293
+ fs.writeFileSync(tempFile, content);
294
+ const mod = await (0, magicast_1.loadFile)(tempFile);
295
+ // This test will expose the broken AST logic
296
+ (0, vitest_1.expect)(() => (0, server_entry_1.instrumentHandleError)(mod)).not.toThrow();
297
+ const modifiedCode = (0, magicast_1.generateCode)(mod.$ast).code;
298
+ (0, vitest_1.expect)(modifiedCode).toContain('Sentry.captureException(error)');
299
+ (0, vitest_1.expect)(modifiedCode).toContain('if (!request.signal.aborted)');
300
+ });
301
+ (0, vitest_1.it)('should demonstrate that the AST bug is now fixed - no longer throws TypeError', async () => {
302
+ const content = `
303
+ export function handleError(error: unknown) {
304
+ console.error('Error occurred:', error);
305
+ }
306
+ `;
307
+ const tempFile = path.join(tmpDir, 'entry.server.tsx');
308
+ fs.writeFileSync(tempFile, content);
309
+ const mod = await (0, magicast_1.loadFile)(tempFile);
310
+ // This test specifically targets the broken AST logic at lines 279-284 in server-entry.ts
311
+ // The bug is in this code:
312
+ // implementation.declarations[0].init.arguments[0].body.body.unshift(...)
313
+ // Where 'implementation' is an IfStatement, not a VariableDeclaration
314
+ let thrownError = null;
315
+ try {
316
+ (0, server_entry_1.instrumentHandleError)(mod);
317
+ }
318
+ catch (error) {
319
+ thrownError = error;
320
+ }
321
+ // The bug is fixed - no error should be thrown
322
+ (0, vitest_1.expect)(thrownError).toBeNull();
323
+ // And the code should be successfully modified
324
+ const modifiedCode = (0, magicast_1.generateCode)(mod.$ast).code;
325
+ (0, vitest_1.expect)(modifiedCode).toContain('Sentry.captureException(error)');
326
+ // The error occurs because recast.parse() creates an IfStatement:
327
+ // { type: 'IfStatement', test: ..., consequent: ... }
328
+ // But the code tries to access .declarations[0] as if it were a VariableDeclaration
329
+ });
330
+ (0, vitest_1.it)('should demonstrate the specific line that breaks - recast.parse creates IfStatement not VariableDeclaration', () => {
331
+ // This test shows exactly what the problematic line 278 in server-entry.ts creates
332
+ const problematicCode = `if (!request.signal.aborted) {
333
+ Sentry.captureException(error);
334
+ }`;
335
+ // This is what line 278 does: recast.parse(problematicCode).program.body[0]
336
+ const implementation = recast.parse(problematicCode).program.body[0];
337
+ // The implementation is an IfStatement, not a VariableDeclaration
338
+ (0, vitest_1.expect)(implementation.type).toBe('IfStatement');
339
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unnecessary-type-assertion
340
+ (0, vitest_1.expect)(implementation.declarations).toBeUndefined();
341
+ // But lines 279-284 try to access implementation.declarations[0].init.arguments[0].body.body
342
+ // This will throw "Cannot read properties of undefined (reading '0')"
343
+ (0, vitest_1.expect)(() => {
344
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unnecessary-type-assertion
345
+ const declarations = implementation.declarations;
346
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
347
+ return declarations[0]; // This line will throw the error
348
+ }).toThrow('Cannot read properties of undefined');
349
+ });
350
+ });
351
+ // Test for Bug #1: Array access vulnerability
352
+ (0, vitest_1.describe)('Array access vulnerability bugs', () => {
353
+ let tmpDir;
354
+ let tmpFile;
355
+ (0, vitest_1.beforeEach)(() => {
356
+ vitest_1.vi.clearAllMocks();
357
+ // Create unique tmp directory for each test
358
+ tmpDir = path.join(__dirname, 'fixtures', 'tmp', `test-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`);
359
+ tmpFile = path.join(tmpDir, 'entry.server.tsx');
360
+ // Ensure tmp directory exists
361
+ fs.mkdirSync(tmpDir, { recursive: true });
362
+ });
363
+ (0, vitest_1.afterEach)(() => {
364
+ // Clean up tmp directory
365
+ if (fs.existsSync(tmpDir)) {
366
+ fs.rmSync(tmpDir, { recursive: true, force: true });
367
+ }
368
+ });
369
+ (0, vitest_1.it)('should safely handle VariableDeclaration with empty declarations array', () => {
370
+ // This test verifies that the bug fix works correctly
371
+ // Previously this would crash, but now it handles empty arrays safely
372
+ // The implementation now includes proper safety checks, so we test that
373
+ // it can handle edge cases without crashing
374
+ // Test the actual safe implementation behavior
375
+ const testResult = () => {
376
+ // Simulate the safe check logic from the actual implementation
377
+ const declarations = []; // Empty array
378
+ if (!declarations || declarations.length === 0) {
379
+ return false; // Safe early return
380
+ }
381
+ // This code would never be reached due to the safe check
382
+ return declarations[0].id.name === 'handleError';
383
+ };
384
+ // Should return false safely without throwing
385
+ (0, vitest_1.expect)(testResult()).toBe(false);
386
+ });
387
+ (0, vitest_1.it)('should safely handle VariableDeclaration with empty declarations array after fix', async () => {
388
+ // This test will pass after we fix the bug
389
+ fs.writeFileSync(tmpFile, 'export const handleError = () => {};');
390
+ const mod = await (0, magicast_1.loadFile)(tmpFile);
391
+ // Create a problematic AST structure
392
+ const problematicNode = {
393
+ type: 'ExportNamedDeclaration',
394
+ declaration: {
395
+ type: 'VariableDeclaration',
396
+ kind: 'const',
397
+ declarations: [], // Empty declarations array
398
+ },
399
+ };
400
+ // Add the problematic node to the AST
401
+ // @ts-expect-error - We need to access body for this test even though it's typed as any
402
+ mod.$ast.body.push(problematicNode);
403
+ // After the fix, this should NOT throw an error
404
+ let thrownError = null;
405
+ try {
406
+ (0, server_entry_1.instrumentHandleError)(mod);
407
+ }
408
+ catch (error) {
409
+ thrownError = error;
410
+ }
411
+ // After the fix, no error should be thrown
412
+ (0, vitest_1.expect)(thrownError).toBeNull();
413
+ });
414
+ });
415
+ //# sourceMappingURL=server-entry.test.js.map