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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ node_modules
../api/node_modules
/bin/
/___automation___.npmrc___
.DS_Store
15 changes: 13 additions & 2 deletions script/react-native-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as childProcess from "child_process";
import { valid } from "semver";
import { downloadBlob, extractArchive } from "./utils/file-utils";
import { findHermesc, isBaseBytecodeCompatible } from "./utils/hermes-utils";
import { restoreSourceMapMetadata } from "./utils/source-map-utils";
import * as dotenv from "dotenv";
import * as cli from "./types/cli";
import { log, sdk } from "./command-executor";
Expand Down Expand Up @@ -109,9 +110,13 @@ async function composeHermesSourceMaps(
outputFolder: string,
sourcemapOutputFolder: string
): Promise<void> {
const composeSourceMaps = path.join(getReactNativePackagePath(), "scripts", "compose-source-maps.js");
let composeSourceMaps = path.join(getReactNativePackagePath(), "scripts", "compose-source-maps.js");
// On iOS, react-native-xcode.sh lets COMPOSE_SOURCEMAP_PATH replace RN's script; an empty value is ignored
if (command.platform === "ios") {
composeSourceMaps = getXcodeDotEnvValue("COMPOSE_SOURCEMAP_PATH") || composeSourceMaps;
}
if (!fs.existsSync(composeSourceMaps)) {
throw new Error("react-native compose-source-maps.js script is not found");
throw new Error(`Source map compose script ${composeSourceMaps} is not found`);
}

const hermesSourceMap = path.join(outputFolder, bundleName + ".hbc.map");
Expand All @@ -126,7 +131,13 @@ async function composeHermesSourceMaps(
combinedSourceMap = command.platform === "android" ? path.join(sourcemapOutputFolder, bundleName + ".map") : packagerSourceMap;
}

// Read before composing: on iOS the composed map overwrites the packager map
const packagerSourceMapJson = JSON.parse(await fs.promises.readFile(packagerSourceMap, "utf8"));
await runCommand("compose-source-maps", "node", [composeSourceMaps, packagerSourceMap, hermesSourceMap, "-o", combinedSourceMap]);
const restoredFields = await restoreSourceMapMetadata(combinedSourceMap, packagerSourceMapJson);
if (restoredFields.length > 0) {
console.log(`Restored packager source map metadata: ${restoredFields.join(", ")}`);
}
// Not part of the release
await fs.promises.unlink(hermesSourceMap);
}
Expand Down
37 changes: 37 additions & 0 deletions script/utils/source-map-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as fs from "fs";

// Fields tied to the bundle layout, which compose rewrites; packager values would point at the wrong sources
const POSITIONAL_FIELDS = new Set([
"version",
"file",
"sourceRoot",
"sources",
"sourcesContent",
"names",
"mappings",
"ignoreList",
"sections",
"x_google_ignoreList",
"x_facebook_sources",
"x_facebook_offsets",
"x_facebook_segments",
"x_hermes_function_offsets",
]);

/** compose-source-maps.js drops fields it doesn't know, such as `debugId`. Copies them back; returns their names. */
export async function restoreSourceMapMetadata(
composedSourceMap: string,
packagerSourceMap: Record<string, unknown>
): Promise<string[]> {
const fields = Object.keys(packagerSourceMap).filter((field) => !POSITIONAL_FIELDS.has(field));
// Skip reading the (possibly huge) composed map when there is nothing to copy
if (fields.length === 0) return [];

const composed = JSON.parse(await fs.promises.readFile(composedSourceMap, "utf8"));
const restored = fields.filter((field) => !Object.hasOwn(composed, field));
if (restored.length === 0) return [];

for (const field of restored) composed[field] = packagerSourceMap[field];
await fs.promises.writeFile(composedSourceMap, JSON.stringify(composed));
return restored;
}
76 changes: 76 additions & 0 deletions test/source-map-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import * as assert from "assert";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { restoreSourceMapMetadata } from "../script/utils/source-map-utils";

describe("Source map metadata", () => {
let testDirectory: string;

beforeEach(() => {
testDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "revopush-source-map-"));
});

afterEach(() => {
fs.rmSync(testDirectory, { recursive: true, force: true });
});

function createSourceMap(fields: Record<string, unknown>): string {
const sourceMap = path.join(testDirectory, "composed.map");
fs.writeFileSync(sourceMap, JSON.stringify(fields));
return sourceMap;
}

function readSourceMap(sourceMap: string): unknown {
return JSON.parse(fs.readFileSync(sourceMap, "utf8"));
}

it("restores fields the composer dropped without touching the ones it produced", async () => {
const composed = createSourceMap({ version: 3, file: "main.jsbundle.hbc", mappings: "AAAA;AACA", x_google_ignoreList: [] });

const restored = await restoreSourceMapMetadata(composed, {
version: 3,
file: "main.jsbundle",
mappings: "AAAA;ACAA",
debugId: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
x_google_ignoreList: [1],
x_custom_vendor: { nested: true },
});

assert.deepStrictEqual(restored, ["debugId", "x_custom_vendor"]);
assert.deepStrictEqual(readSourceMap(composed), {
version: 3,
file: "main.jsbundle.hbc",
mappings: "AAAA;AACA",
x_google_ignoreList: [],
debugId: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
x_custom_vendor: { nested: true },
});
});

it("never restores positional fields, which index into the packager's sources", async () => {
const composed = createSourceMap({ version: 3, sources: ["/abs/a.js"], mappings: "AAAA" });

const restored = await restoreSourceMapMetadata(composed, {
sourceRoot: "/abs/",
sections: [],
ignoreList: [1],
x_google_ignoreList: [1],
x_facebook_sources: [null],
x_hermes_function_offsets: { "0": [0] },
debugId: "aaaa-bbbb",
});

assert.deepStrictEqual(restored, ["debugId"]);
assert.deepStrictEqual(readSourceMap(composed), { version: 3, sources: ["/abs/a.js"], mappings: "AAAA", debugId: "aaaa-bbbb" });
});

it("leaves the composed source map untouched when there is nothing to restore", async () => {
const composed = createSourceMap({ version: 3, file: "main.jsbundle.hbc", debugId: "from-compose" });
const contents = fs.readFileSync(composed, "utf8");

assert.deepStrictEqual(await restoreSourceMapMetadata(composed, { version: 3 }), []);
assert.deepStrictEqual(await restoreSourceMapMetadata(composed, { debugId: "from-packager" }), []);
assert.strictEqual(fs.readFileSync(composed, "utf8"), contents);
});
});
Loading