Skip to content

Commit 95e9e36

Browse files
chargomeclaude
andcommitted
feat(remix): Add Remix 3 export subpaths and optional peer deps
Prepares @sentry/remix to hold Remix 3 support. No instrumentation yet, the v3 entries are placeholders. Fixes #24662 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 7824c40 commit 95e9e36

6 files changed

Lines changed: 131 additions & 1 deletion

File tree

‎packages/remix/package.json‎

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,23 @@
5151
"import": {
5252
"default": "./build/import-hook.mjs"
5353
}
54+
},
55+
"./v3": {
56+
"import": {
57+
"types": "./build/types/v3/index.server.d.ts",
58+
"default": "./build/esm/v3/index.server.js"
59+
}
60+
},
61+
"./v3/client": {
62+
"import": {
63+
"types": "./build/types/v3/index.client.d.ts",
64+
"default": "./build/esm/v3/index.client.js"
65+
}
66+
},
67+
"./v3/node": {
68+
"import": {
69+
"default": "./build/v3-node.mjs"
70+
}
5471
}
5572
},
5673
"publishConfig": {
@@ -81,7 +98,8 @@
8198
"@remix-run/node": "2.x",
8299
"@remix-run/react": "2.x",
83100
"@remix-run/server-runtime": "2.x",
84-
"react": "18.x"
101+
"react": "18.x",
102+
"remix": ">=3.0.0-rc.1 <4"
85103
},
86104
"scripts": {
87105
"build": "run-p build:transpile build:types",
@@ -119,5 +137,22 @@
119137
]
120138
}
121139
}
140+
},
141+
"peerDependenciesMeta": {
142+
"@remix-run/node": {
143+
"optional": true
144+
},
145+
"@remix-run/react": {
146+
"optional": true
147+
},
148+
"@remix-run/server-runtime": {
149+
"optional": true
150+
},
151+
"react": {
152+
"optional": true
153+
},
154+
"remix": {
155+
"optional": true
156+
}
122157
}
123158
}

‎packages/remix/rollup.npm.config.mjs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
1+
import { defineConfig } from 'rollup';
12
import { makeBaseNPMConfig, makeNPMConfigVariants, makeOrchestrionLoader } from '@sentry-internal/rollup-utils';
23

4+
// `external: /.*/` keeps the `remix` import a runtime resolution against the installed package.
5+
const v3NodeEntry = defineConfig({
6+
input: 'src/v3/node.mjs',
7+
external: /.*/,
8+
output: { format: 'esm', file: 'build/v3-node.mjs' },
9+
});
10+
311
// We rely on esbuild's defaults for JSX (`jsx: 'transform'` = classic runtime, no
412
// __self/__source attributes). React 19 prefers the new automatic transform, but switching
513
// to it would break React 17 support — so we intentionally stay on classic for now.
614
// https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html
715
export default [
16+
v3NodeEntry,
817
...makeNPMConfigVariants(
918
makeBaseNPMConfig({
1019
entrypoints: [
@@ -14,12 +23,17 @@ export default [
1423
'src/server/index.ts',
1524
'src/cloudflare/index.ts',
1625
'src/vite/index.ts',
26+
'src/v3/index.server.ts',
27+
'src/v3/index.client.ts',
1728
],
1829
packageSpecificConfig: {
1930
external: ['react-router', 'react-router-dom', 'react', 'react/jsx-runtime'],
2031
output: {
2132
// make it so Rollup calms down about the fact that we're combining default and named exports
2233
exports: 'named',
34+
// Without this, Rollup infers a common root from the whole module graph, which here reaches
35+
// into sibling workspace packages, and the v3 entries land outside `build/esm/v3/`.
36+
preserveModulesRoot: 'src',
2337
},
2438
},
2539
}),
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// A named list rather than `export * from '@sentry/browser'`: Remix 3 has no bundler, so a wildcard
2+
// makes every export reachable and nothing can be tree shaken out of the served module graph.
3+
//
4+
// `init` is withheld until the Remix 3 browser SDK lands. Re-exporting `@sentry/browser`'s would give
5+
// an app History based tracing, which yields no navigation spans in Remix 3, so it would look
6+
// configured while reporting nothing.
7+
export { captureException, captureMessage } from '@sentry/browser';
8+
export type { BrowserOptions } from '@sentry/browser';
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Placeholder until the Remix 3 server instrumentation lands. `@sentry/node`'s `init` already emits
2+
// `http.server` spans, so this is useful on its own; route parameterisation and router error capture
3+
// are what is still missing.
4+
export * from '@sentry/node';

‎packages/remix/src/v3/node.mjs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Replaces `--import remix/node-tsx` rather than adding a second flag. Sentry's module hook is
2+
// registered here once the server instrumentation lands, so for now nothing is instrumented.
3+
await import('remix/node-tsx');
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import * as fs from 'node:fs';
2+
import * as path from 'node:path';
3+
import * as semver from 'semver';
4+
import { describe, expect, it } from 'vitest';
5+
6+
import packageJson from '../package.json';
7+
8+
const packageRoot = path.resolve(__dirname, '..');
9+
10+
// `exports` mixes shapes (`"./package.json"` is a bare string), so it does not fit one index signature.
11+
const exportsMap = packageJson.exports as unknown as Record<string, { import?: Record<string, string> }>;
12+
13+
/**
14+
* A mismatch between the `exports` map and the build output is invisible: the build and lint both
15+
* pass, and the package is broken only for whoever installs it. Rollup infers its output layout from
16+
* the module graph, so an unrelated change can relocate these files.
17+
*/
18+
describe('Remix 3 exports', () => {
19+
const subpaths = ['./v3', './v3/client', './v3/node'];
20+
21+
it.each(subpaths)('%s is declared in the exports map', subpath => {
22+
expect(packageJson.exports).toHaveProperty([subpath]);
23+
});
24+
25+
it.each(subpaths)('%s only exposes an import condition', subpath => {
26+
// Remix 3's asset server rejects CommonJS outright, so a `require` condition could never load.
27+
expect(Object.keys(exportsMap[subpath] ?? {})).toEqual(['import']);
28+
});
29+
30+
it.each(subpaths)('%s points at files that exist in the build output', subpath => {
31+
const targets = Object.values(exportsMap[subpath]?.import ?? {});
32+
33+
expect(targets.length).toBeGreaterThan(0);
34+
35+
for (const target of targets) {
36+
expect(fs.existsSync(path.join(packageRoot, target)), `${subpath} -> ${target} is missing`).toBe(true);
37+
}
38+
});
39+
40+
describe('the Remix 3 peer range', () => {
41+
const range = packageJson.peerDependencies.remix;
42+
43+
// Asserted by behaviour, so the range can change after Remix 3 reaches GA without failing here.
44+
it('accepts release candidates', () => {
45+
// `3.x` does not match `3.0.0-rc.1`: semver ranges exclude prereleases unless they name one.
46+
expect(semver.satisfies('3.0.0-rc.1', range)).toBe(true);
47+
expect(semver.satisfies('3.0.0-rc.2', range)).toBe(true);
48+
});
49+
50+
it('accepts stable 3.x', () => {
51+
expect(semver.satisfies('3.0.0', range)).toBe(true);
52+
expect(semver.satisfies('3.4.2', range)).toBe(true);
53+
});
54+
55+
it('rejects the next major', () => {
56+
expect(semver.satisfies('4.0.0', range)).toBe(false);
57+
});
58+
});
59+
60+
it('marks every peer dependency optional', () => {
61+
// A Remix 3 app must not have the Remix 2 packages or React installed on its behalf.
62+
for (const name of Object.keys(packageJson.peerDependencies)) {
63+
expect(packageJson.peerDependenciesMeta, `${name} should be optional`).toHaveProperty([name, 'optional'], true);
64+
}
65+
});
66+
});

0 commit comments

Comments
 (0)