Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@
]
},
"dependencies": {
"@react-native-community/cli": "^12.2.1",
"commander": "^11.1.0"
"commander": "^11.1.0",
"hermes-profile-transformer": "^0.0.9"
}
}
29 changes: 15 additions & 14 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
#!/usr/bin/env node
import getContext from '@react-native-community/cli-config';
import { logger, CLIError } from '@react-native-community/cli-tools';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { execSync } from 'child_process';
import type { Config } from '@react-native-community/cli-types';
import transformer from 'hermes-profile-transformer';
import {
findSourcemap,
generateSourcemap,
} from '@react-native-community/cli-hermes/build/profileHermes/sourcemapUtils';
import { getAndroidProject } from '@react-native-community/cli-platform-android';
import { getMetroBundleOptions } from '@react-native-community/cli-hermes/build/profileHermes/metroBundleOptions';
import { getMetroBundleOptions } from './getMetroBundleOptions';
import { generateSourcemap, findSourcemap } from './sourcemapUtils';
import getConfig from './getConfig';

// Most of the file is just a copy of https://github.com/react-native-community/cli/blob/main/packages/cli-hermes/src/profileHermes/downloadProfile.ts

Expand Down Expand Up @@ -89,7 +84,6 @@ function maybeAddLineAndColumn(path: string): void {
* @param appIdSuffix
*/
export async function downloadProfile(
ctx: Config,
local: string | undefined,
fromDownload: Boolean | undefined,
dstPath: string,
Expand All @@ -101,15 +95,23 @@ export async function downloadProfile(
appId?: string,
appIdSuffix?: string
) {
let ctx = await getConfig();

try {
const androidProject = getAndroidProject(ctx);
const androidProject = ctx?.project.android;
const packageNameWithSuffix = [
appId || androidProject.packageName,
appId || androidProject?.packageName,
appIdSuffix,
]
.filter(Boolean)
.join('.');

if (!packageNameWithSuffix) {
throw new Error(
"Failed to retrieve the package name from the project's Android manifest file. Please provide the package name with the --appId flag."
);
}

// If file name is not specified, pull the latest file from device
let file =
filename ||
Expand All @@ -125,7 +127,7 @@ export async function downloadProfile(
logger.info(`File to be pulled: ${file}`);

// If destination path is not specified, pull to the current directory
dstPath = dstPath || ctx.root;
dstPath = dstPath || ctx?.root || './';

logger.debug('Internal commands run to pull the file:');

Expand Down Expand Up @@ -162,7 +164,7 @@ export async function downloadProfile(
);
}
maybeAddLineAndColumn(tempFilePath);
const bundleOptions = getMetroBundleOptions(tempFilePath);
const bundleOptions = getMetroBundleOptions(tempFilePath, 'localhost');
Comment thread
szymonrybczak marked this conversation as resolved.

// If path to source map is not given
if (!sourcemapPath) {
Expand Down Expand Up @@ -237,7 +239,6 @@ program.parse();
const options = program.opts();
const dstPath = './';
downloadProfile(
getContext(),
options.local,
options.fromDownload,
dstPath,
Expand Down
31 changes: 31 additions & 0 deletions src/getConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { exec } from 'child_process';

interface Config {
project: {
android: {
packageName: string;
};
};
root: string;
}
Comment thread
kirillzyusko marked this conversation as resolved.

function getConfig(): Promise<Config | null> {
return new Promise((resolve, reject) => {
exec('npx react-native config', (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
reject(error);
return;
}

resolve(JSON.parse(stdout));

if (stderr) {
resolve(null);
throw new Error(stderr);
}
Comment on lines +21 to +26
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@szymonrybczak why exactly did we do that here? That on stderr we resolve but throw an error?

(cc @kirillzyusko)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm entirely sure what do you mean? Can you specify?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean why would we "resolve" on an stderr output but then throw an error?
Screenshot 2024-06-27 at 10 27 07

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of rejecting on an stderr?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there's something inside stderr there's no config, soo it was more logical to me to just return null. Not strong opinion here tho.

});
});
}

export default getConfig;
68 changes: 68 additions & 0 deletions src/getMetroBundleOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copy of https://github.com/react-native-community/cli/blob/13.x/packages/cli-hermes/src/profileHermes/metroBundleOptions.ts as `cli-hermes` was recently removed from React Native Community CLI

import fs from 'fs';
import type { HermesCPUProfile } from 'hermes-profile-transformer/dist/types/HermesProfile';

export interface MetroBundleOptions {
platform: string;
dev: boolean;
minify: boolean;
host: string;
}

export function getMetroBundleOptions(
downloadedProfileFilePath: string,
host: string
): MetroBundleOptions {
let options: MetroBundleOptions = {
platform: 'android',
dev: true,
minify: false,
host,
Comment thread
szymonrybczak marked this conversation as resolved.
};

try {
const contents: HermesCPUProfile = JSON.parse(
fs.readFileSync(downloadedProfileFilePath, {
encoding: 'utf8',
})
);
const matchBundleUrl = /^.*\((.*index\.bundle.*)\)/;
let containsExpoDevMenu = false;
let hadMatch = false;
for (const frame of Object.values(contents.stackFrames)) {
if (frame.name.includes('EXDevMenuApp')) {
containsExpoDevMenu = true;
}
const match = matchBundleUrl.exec(frame.name);
if (match) {
// @ts-ignore
const parsed = new URL(match[1]);
const platform = parsed.searchParams.get('platform'),
dev = parsed.searchParams.get('dev'),
minify = parsed.searchParams.get('minify');
if (platform) {
options.platform = platform;
}
if (dev) {
options.dev = dev === 'true';
}
if (minify) {
options.minify = minify === 'true';
}

hadMatch = true;
break;
}
}
if (containsExpoDevMenu && !hadMatch) {
console.warn(`Found references to the Expo Dev Menu in your profiling sample.
You might have accidentally recorded the Expo Dev Menu instead of your own application.
To work around this, please reload your app twice before starting a profiler recording.`);
}
} catch (e) {
throw e;
}

return options;
}
117 changes: 117 additions & 0 deletions src/sourcemapUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copy of https://github.com/react-native-community/cli/blob/13.x/packages/cli-hermes/src/profileHermes/metroBundleOptions.ts as `cli-hermes` was recently removed from React Native Community CLI

import fs from 'fs';
import path from 'path';
import os from 'os';
import type { SourceMap } from 'hermes-profile-transformer';
import type { MetroBundleOptions } from './getMetroBundleOptions';

type Config = any;

function getTempFilePath(filename: string) {
return path.join(os.tmpdir(), filename);
}

function writeJsonSync(targetPath: string, data: any) {
let json;
try {
json = JSON.stringify(data);
} catch (e) {
throw new Error(
`Failed to serialize data to json before writing to ${targetPath}`,
e as Error
);
}

try {
fs.writeFileSync(targetPath, json, 'utf-8');
} catch (e) {
throw new Error(`Failed to write json to ${targetPath}`, e as Error);
}
}

async function getSourcemapFromServer(
port: string,
{ platform, dev, minify, host }: MetroBundleOptions
): Promise<SourceMap | undefined> {
console.log('Getting source maps from Metro packager server');

const requestURL = `http://${host}:${port}/index.map?platform=${platform}&dev=${dev}&minify=${minify}`;
console.log(`Downloading from ${requestURL}`);
Comment thread
hannojg marked this conversation as resolved.
try {
// @ts-ignore
const { data } = await fetch(requestURL);
return data as SourceMap;
} catch (e) {
console.log(`Failed to fetch source map from "${requestURL}"`);
return undefined;
}
}

/**
* Generate a sourcemap by fetching it from a running metro server
*/
export async function generateSourcemap(
port: string,
bundleOptions: MetroBundleOptions
): Promise<string | undefined> {
// Fetch the source map to a temp directory
const sourceMapPath = getTempFilePath('index.map');
const sourceMapResult = await getSourcemapFromServer(port, bundleOptions);

if (sourceMapResult) {
console.log('Using source maps from Metro packager server');
writeJsonSync(sourceMapPath, sourceMapResult);
console.log(
`Successfully obtained the source map and stored it in ${sourceMapPath}`
);
return sourceMapPath;
} else {
console.log('Error: Cannot obtain source maps from Metro packager server');
return undefined;
}
}

/**
*
* @param ctx
*/
export async function findSourcemap(
ctx: Config,
port: string,
bundleOptions: MetroBundleOptions
): Promise<string | undefined> {
const intermediateBuildPath = path.join(
ctx.root,
'android',
'app',
'build',
'intermediates',
'sourcemaps',
'react',
'debug',
'index.android.bundle.packager.map'
);

const generatedBuildPath = path.join(
ctx.root,
'android',
'app',
'build',
'generated',
'sourcemaps',
'react',
'debug',
'index.android.bundle.map'
);

if (fs.existsSync(generatedBuildPath)) {
console.log(`Getting the source map from ${generateSourcemap}`);
return generatedBuildPath;
} else if (fs.existsSync(intermediateBuildPath)) {
console.log(`Getting the source map from ${intermediateBuildPath}`);
return intermediateBuildPath;
} else {
return generateSourcemap(port, bundleOptions);
}
}
Loading