Skip to content

Commit 1c0d3ef

Browse files
Add metro serializer tests
1 parent 9fce536 commit 1c0d3ef

File tree

7 files changed

+200
-43
lines changed

7 files changed

+200
-43
lines changed

jest.config.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module.exports = {
2+
collectCoverage: true,
3+
preset: 'react-native',
4+
setupFilesAfterEnv: ['<rootDir>/test/mockConsole.ts'],
5+
globals: {
6+
__DEV__: true,
7+
'ts-jest': {
8+
tsConfig: './tsconfig.json',
9+
diagnostics: false,
10+
},
11+
},
12+
moduleFileExtensions: ['ts', 'tsx', 'js'],
13+
testPathIgnorePatterns: ['<rootDir>/test/e2e/', '<rootDir>/test/tools/', '<rootDir>/test/react-native/versions'],
14+
testEnvironment: 'node',
15+
testMatch: ['**/*.test.(ts|tsx)'],
16+
};

jest.config.tools.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
module.exports = {
2+
collectCoverage: true,
3+
preset: 'ts-jest',
4+
setupFilesAfterEnv: ['<rootDir>/test/mockConsole.ts'],
5+
globals: {
6+
__DEV__: true,
7+
},
8+
testMatch: ['**/test/tools/**/*.ts'],
9+
transform: {
10+
'^.+\\.(ts|tsx)$': [
11+
'ts-jest',
12+
{
13+
tsconfig: './tsconfig.build.tools.json',
14+
diagnostics: false,
15+
},
16+
],
17+
},
18+
};

package.json

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
"build:tools": "tsc -p tsconfig.build.tools.json",
2525
"downlevel": "downlevel-dts dist ts3.8/dist --to=3.8",
2626
"clean": "rimraf dist coverage",
27-
"test": "jest",
27+
"test": "yarn test:sdk && yarn test:tools",
28+
"test:sdk": "jest",
29+
"test:tools": "jest --config jest.config.tools.js",
2830
"fix": "yarn fix:eslint && yarn fix:prettier",
2931
"fix:eslint": "eslint --config .eslintrc.js --fix .",
3032
"fix:prettier": "prettier --write \"{src,test,scripts}/**/**.ts\"",
@@ -98,33 +100,6 @@
98100
},
99101
"ios": {}
100102
},
101-
"jest": {
102-
"collectCoverage": true,
103-
"preset": "react-native",
104-
"setupFilesAfterEnv": [
105-
"<rootDir>/test/mockConsole.ts"
106-
],
107-
"globals": {
108-
"__DEV__": true,
109-
"ts-jest": {
110-
"tsConfig": "./tsconfig.json",
111-
"diagnostics": false
112-
}
113-
},
114-
"moduleFileExtensions": [
115-
"ts",
116-
"tsx",
117-
"js"
118-
],
119-
"testPathIgnorePatterns": [
120-
"<rootDir>/test/e2e/",
121-
"<rootDir>/test/react-native/versions"
122-
],
123-
"testEnvironment": "node",
124-
"testMatch": [
125-
"**/*.test.(ts|tsx)"
126-
]
127-
},
128103
"codegenConfig": {
129104
"name": "RNSentrySpec",
130105
"type": "modules",

src/js/tools/sentryMetroSerializer.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,19 @@ type SourceMap = Record<string, unknown>;
1111

1212
type ExpectedSerializedConfigThisContext = Partial<SerializerConfigT>;
1313

14-
type MetroSerializer = (
14+
export type VirtualJSOutput = {
15+
type: string;
16+
data: {
17+
code: string;
18+
lineCount: number;
19+
map: [];
20+
};
21+
};
22+
export type MetroSerializer = (
1523
entryPoint: string,
1624
preModules: ReadonlyArray<Module>,
1725
graph: ReadOnlyGraph,
18-
options: SerializerOptions & { sentryBundleCallback: (bundle: Bundle) => Bundle },
26+
options: SerializerOptions & { sentryBundleCallback?: (bundle: Bundle) => Bundle },
1927
) => string | { code: string; map: string } | Promise<string | { code: string; map: string }>;
2028

2129
type MetroModuleId = number;
@@ -39,14 +47,7 @@ const createDebugIdSnippet = (debugId: string): string => {
3947

4048
const createDebugIdModule = (
4149
debugId: string,
42-
): Module<{
43-
type: string;
44-
data: {
45-
code: string;
46-
lineCount: number;
47-
map: [];
48-
};
49-
}> => {
50+
): Module<VirtualJSOutput> => {
5051
const debugIdCode = createDebugIdSnippet(debugId);
5152

5253
return {
@@ -95,8 +96,6 @@ const calculateDebugId = (bundle: Bundle): string => {
9596
hash.update(bundle.post);
9697

9798
const debugId = stringToUUID(hash.digest('hex'));
98-
// eslint-disable-next-line no-console
99-
console.log('info ' + `Bundle Debug ID: ${debugId}`);
10099
return debugId;
101100
};
102101

@@ -186,7 +185,7 @@ export const createSentryMetroSerializer = (customSerializer?: MetroSerializer):
186185
if (!containsDebugIdModule) {
187186
const debugIdModule = createDebugIdModule(DEBUG_ID_PLACE_HOLDER);
188187
const tmpPreModules = [...preModules];
189-
if (tmpPreModules[0].path === PRELUDE_MODULE_PATH) {
188+
if (tmpPreModules.length > 0 && tmpPreModules[0] !== undefined && tmpPreModules[0].path === PRELUDE_MODULE_PATH) {
190189
// prelude module must be first as it measures the bundle startup time
191190
tmpPreModules.unshift(preModules[0]);
192191
tmpPreModules[1] = debugIdModule;
@@ -225,6 +224,11 @@ export const createSentryMetroSerializer = (customSerializer?: MetroSerializer):
225224
'Debug ID was not found in the bundle. Call `options.sentryBundleCallback` if you are using a custom serializer.',
226225
);
227226
}
227+
if (graph.transformOptions.hot !== true) {
228+
// eslint-disable-next-line no-console
229+
console.log('info ' + `Bundle Debug ID: ${debugId}`);
230+
}
231+
228232
currentDebugIdModule?.output[0].data.code.replace(DEBUG_ID_PLACE_HOLDER, debugId);
229233
const debugIdComment = `${DEBUG_ID_COMMENT}${debugId}`;
230234
const indexOfSourceMapComment = bundleCode.lastIndexOf(SOURCE_MAP_COMMENT);
@@ -237,8 +241,7 @@ export const createSentryMetroSerializer = (customSerializer?: MetroSerializer):
237241
indexOfSourceMapComment,
238242
)}`;
239243

240-
if (this.processModuleFilter === undefined) {
241-
// processModuleFilter is undefined when processing build request from the dev server
244+
if (graph.transformOptions.hot === true) {
242245
return bundleCodeWithDebugId;
243246
}
244247

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
__mock_prelude__
2+
var _sentryDebugIds={},_sentryDebugIdIdentifier="";try{var e=Error().stack;e&&(_sentryDebugIds[e]="dd782ff1-06cb-4d23-8de6-d6bc3f36ea8d",_sentryDebugIdIdentifier="sentry-dbid-dd782ff1-06cb-4d23-8de6-d6bc3f36ea8d")}catch(r){}
3+
__mock_index_js__
4+
//# debugId=dd782ff1-06cb-4d23-8de6-d6bc3f36ea8d
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"debug_id":"dd782ff1-06cb-4d23-8de6-d6bc3f36ea8d","debugId":"dd782ff1-06cb-4d23-8de6-d6bc3f36ea8d"}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import * as fs from 'fs';
2+
import type { MixedOutput , Module } from 'metro';
3+
import CountingSet from 'metro/src/lib/CountingSet';
4+
import * as countLines from 'metro/src/lib/countLines';
5+
6+
import type { MetroSerializer, VirtualJSOutput } from '../../src/js/tools/sentryMetroSerializer';
7+
import { createSentryMetroSerializer } from '../../src/js/tools/sentryMetroSerializer';
8+
9+
describe('Sentry Metro Serializer', () => {
10+
11+
test('generates bundle and source map with deterministic uuidv5 debug id', async () => {
12+
const serializer = createSentryMetroSerializer();
13+
14+
const bundle = await serializer(...mockMinSerializerArgs());
15+
if (typeof bundle === 'string') {
16+
fail('Expected bundle to be an object with a "code" property');
17+
}
18+
19+
expect(bundle.code)
20+
.toEqual('var _sentryDebugIds={},_sentryDebugIdIdentifier="";try{var e=Error().stack;e&&(_sentryDebugIds[e]="4a945039-d159-4021-938a-83830ce8127c",_sentryDebugIdIdentifier="sentry-dbid-4a945039-d159-4021-938a-83830ce8127c")}catch(r){}\n//# debugId=4a945039-d159-4021-938a-83830ce8127c');
21+
expect(bundle.map)
22+
.toEqual('{"debug_id":"4a945039-d159-4021-938a-83830ce8127c","debugId":"4a945039-d159-4021-938a-83830ce8127c"}');
23+
});
24+
25+
test('generated debug id is uuid v4 format', async () => {
26+
const serializer = createSentryMetroSerializer();
27+
const bundle = await serializer(...mockMinSerializerArgs());
28+
const debugId = determineDebugIdFromBundleSource(typeof bundle === 'string' ? bundle : bundle.code);
29+
expect(debugId).toEqual('4a945039-d159-4021-938a-83830ce8127c');
30+
});
31+
32+
test('adds debug id snipped after prelude module and before ', async () => {
33+
const serializer = createSentryMetroSerializer();
34+
35+
const bundle = await serializer(...mockWithPreludAndDepsSerializerArgs());
36+
if (typeof bundle === 'string') {
37+
fail('Expected bundle to be an object with a "code" property');
38+
}
39+
40+
expect(bundle.code).toEqual(fs.readFileSync(`${__dirname}/fixtures/bundleWithPrelude.js.fixture`, 'utf8'));
41+
expect(bundle.map).toEqual(fs.readFileSync(`${__dirname}/fixtures/bundleWithPrelude.js.fixture.map`, 'utf8'));
42+
});
43+
});
44+
45+
function mockMinSerializerArgs(): Parameters<MetroSerializer> {
46+
let modulesCounter = 0;
47+
return [
48+
'index.js',
49+
[],
50+
{
51+
entryPoints: new Set(),
52+
dependencies: new Map(),
53+
transformOptions: {
54+
hot: false,
55+
dev: false,
56+
minify: false,
57+
type: 'script',
58+
unstable_transformProfile: 'hermes-stable',
59+
}
60+
},
61+
{
62+
asyncRequireModulePath: 'asyncRequire',
63+
createModuleId: (_filePath: string): number => modulesCounter++,
64+
dev: false,
65+
getRunModuleStatement: (_moduleId: string | number): string => '',
66+
includeAsyncPaths: false,
67+
modulesOnly: false,
68+
processModuleFilter: (_module: Module<MixedOutput>) => true,
69+
projectRoot: '/project/root',
70+
runBeforeMainModule: [],
71+
runModule: false,
72+
serverRoot: '/server/root',
73+
shouldAddToIgnoreList: (_module: Module<MixedOutput>) => false,
74+
},
75+
];
76+
}
77+
78+
function mockWithPreludAndDepsSerializerArgs(): Parameters<MetroSerializer> {
79+
const mockPreludeCode = '__mock_prelude__';
80+
const indexJsCode = '__mock_index_js__';
81+
const args = mockMinSerializerArgs();
82+
args[1] = [
83+
{
84+
dependencies: new Map(),
85+
getSource: () => Buffer.from(mockPreludeCode),
86+
inverseDependencies: new CountingSet(),
87+
path: '__prelude__',
88+
output: [
89+
<VirtualJSOutput> {
90+
type: 'js/script/virtual',
91+
data: {
92+
code: mockPreludeCode,
93+
lineCount: countLines(indexJsCode),
94+
map: [],
95+
},
96+
},
97+
],
98+
},
99+
];
100+
101+
// @ts-expect-error - This is a mock
102+
args[2].dependencies = <Parameters<MetroSerializer>[2]['dependencies']>new Map([
103+
[
104+
'index.js',
105+
<Module<VirtualJSOutput>> {
106+
dependencies: new Map(),
107+
getSource: () => Buffer.from(indexJsCode),
108+
inverseDependencies: new CountingSet(),
109+
path: 'index.js',
110+
output: [
111+
{
112+
type: 'js/script/virtual',
113+
data: {
114+
code: indexJsCode,
115+
lineCount: countLines(indexJsCode),
116+
map: [],
117+
},
118+
},
119+
],
120+
},
121+
]
122+
]);
123+
124+
return args;
125+
}
126+
127+
/**
128+
* This function is on purpose not shared with the actual implementation.
129+
*/
130+
function determineDebugIdFromBundleSource(code: string): string | undefined {
131+
const match = code.match(
132+
/sentry-dbid-([0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12})/,
133+
);
134+
135+
if (match) {
136+
return match[1];
137+
} else {
138+
return undefined;
139+
}
140+
}

0 commit comments

Comments
 (0)