diff --git a/CHANGELOG.md b/CHANGELOG.md index 013ca5c2f..efb1e0bb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Fixes + +- fix(remix): Use `npx @sentry/remix --upload-sourcemaps` instead of `sentry-upload-sourcemaps` to avoid global bin collisions + ### Features - feat(react-router): Use `sentryOnError` on `HydratedRouter` instead of mutating `root.tsx` ErrorBoundary diff --git a/src/remix/sdk-setup.ts b/src/remix/sdk-setup.ts index d9513be6b..e18375e94 100644 --- a/src/remix/sdk-setup.ts +++ b/src/remix/sdk-setup.ts @@ -334,7 +334,7 @@ export async function updateBuildScript(args: { : 'remix build'; const instrumentedBuildCommand = - `${buildCommand} --sourcemap && sentry-upload-sourcemaps --org ${args.org} --project ${args.project}` + + `${buildCommand} --sourcemap && npx @sentry/remix --upload-sourcemaps --org ${args.org} --project ${args.project}` + (args.url ? ` --url ${args.url}` : '') + (args.isHydrogen ? ' --buildPath ./dist' : ''); diff --git a/src/sourcemaps/tools/remix.ts b/src/sourcemaps/tools/remix.ts index 9d2445ae6..9c7bf13f1 100644 --- a/src/sourcemaps/tools/remix.ts +++ b/src/sourcemaps/tools/remix.ts @@ -50,13 +50,13 @@ In case you already tried the wizard, we can also show you how to configure your `Build your app with ${chalk.cyan( 'remix build --sourcemap', )}, then upload your source maps using ${chalk.cyan( - 'sentry-upload-sourcemaps', + 'npx @sentry/remix --upload-sourcemaps', )} cli tool.`, ); clack.log.step( `You can add ${chalk.cyan( - 'sentry-upload-sourcemaps', + 'npx @sentry/remix --upload-sourcemaps', )} to your build script in ${chalk.cyan('package.json')} like this:`, ); @@ -66,9 +66,9 @@ In case you already tried the wizard, we can also show you how to configure your clack.log.step(`or run it manually after building your app. -To see all available options for ${chalk.cyan( - 'sentry-upload-sourcemaps', - )}, run ${chalk.cyan('sentry-upload-sourcemaps --help')} +To see all available options, run ${chalk.cyan( + 'npx @sentry/remix --upload-sourcemaps --help', + )} `); await abortIfCancelled( @@ -84,7 +84,7 @@ To see all available options for ${chalk.cyan( const codeSnippet = chalk.gray(` "scripts": { ${chalk.greenBright( - '"build": "remix build --sourcemap && sentry-upload-sourcemaps"', + '"build": "remix build --sourcemap && npx @sentry/remix --upload-sourcemaps"', )}; } `); diff --git a/test/remix/build-script.test.ts b/test/remix/build-script.test.ts new file mode 100644 index 000000000..a30f0d013 --- /dev/null +++ b/test/remix/build-script.test.ts @@ -0,0 +1,143 @@ +import * as fs from 'fs'; + +import { updateBuildScript } from '../../src/remix/sdk-setup'; +import { getPackageDotJson } from '../../src/utils/clack'; + +import { vi, it, describe, expect, afterEach } from 'vitest'; + +const writeFileSpy = vi + .spyOn(fs.promises, 'writeFile') + .mockImplementation(() => Promise.resolve()); + +vi.mock('@clack/prompts', () => { + const mock = { + log: { + info: vi.fn(), + success: vi.fn(), + warn: vi.fn(), + }, + confirm: vi.fn().mockResolvedValue(true), + isCancel: vi.fn().mockReturnValue(false), + }; + return { ...mock, default: mock }; +}); + +// eslint-disable-next-line @typescript-eslint/no-unsafe-return +vi.mock('../../src/utils/clack', async () => ({ + ...(await vi.importActual('../../src/utils/clack')), + getPackageDotJson: vi.fn().mockResolvedValue({ + scripts: { + build: 'remix build', + }, + version: '1.0.0', + }), +})); + +describe('updateBuildScript', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it('uses npx @sentry/remix --upload-sourcemaps for the upload command', async () => { + await updateBuildScript({ + org: 'my-org', + project: 'my-project', + isHydrogen: false, + }); + + expect(writeFileSpy).toHaveBeenCalledWith( + expect.stringContaining('package.json'), + expect.stringContaining( + 'npx @sentry/remix --upload-sourcemaps --org my-org --project my-project', + ), + ); + }); + + it('replaces the remix build command with sourcemap flag and upload', async () => { + await updateBuildScript({ + org: 'my-org', + project: 'my-project', + isHydrogen: false, + }); + + expect(writeFileSpy).toHaveBeenCalledWith( + expect.stringContaining('package.json'), + expect.stringContaining( + 'remix build --sourcemap && npx @sentry/remix --upload-sourcemaps', + ), + ); + }); + + it('includes --url flag when a custom url is provided', async () => { + await updateBuildScript({ + org: 'my-org', + project: 'my-project', + url: 'https://self-hosted.example.com', + isHydrogen: false, + }); + + expect(writeFileSpy).toHaveBeenCalledWith( + expect.stringContaining('package.json'), + expect.stringContaining('--url https://self-hosted.example.com'), + ); + }); + + it('uses shopify hydrogen build for hydrogen apps', async () => { + vi.mocked(getPackageDotJson).mockResolvedValue({ + scripts: { + build: 'shopify hydrogen build', + }, + version: '1.0.0', + }); + + await updateBuildScript({ + org: 'my-org', + project: 'my-project', + isHydrogen: true, + }); + + expect(writeFileSpy).toHaveBeenCalledWith( + expect.stringContaining('package.json'), + expect.stringContaining( + 'shopify hydrogen build --sourcemap && npx @sentry/remix --upload-sourcemaps --org my-org --project my-project --buildPath ./dist', + ), + ); + }); + + it('sets build script when none exists', async () => { + vi.mocked(getPackageDotJson).mockResolvedValue({ + scripts: {}, + version: '1.0.0', + }); + + await updateBuildScript({ + org: 'my-org', + project: 'my-project', + isHydrogen: false, + }); + + expect(writeFileSpy).toHaveBeenCalledWith( + expect.stringContaining('package.json'), + expect.stringContaining( + 'remix build --sourcemap && npx @sentry/remix --upload-sourcemaps', + ), + ); + }); + + it('throws when build script has an unknown command', async () => { + vi.mocked(getPackageDotJson).mockResolvedValue({ + scripts: { + build: 'some-custom-build-tool', + }, + version: '1.0.0', + }); + + await expect( + updateBuildScript({ + org: 'my-org', + project: 'my-project', + isHydrogen: false, + }), + ).rejects.toThrow("build` script doesn't contain a known build command"); + }); +});