fix(metro-serializer-esbuild): fix source maps not pointing to source (#1856)

This commit is contained in:
Tommy Nguyen 2022-08-31 11:48:42 +02:00 коммит произвёл GitHub
Родитель 7d82009815
Коммит 61151646d4
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 142 добавлений и 23 удалений

Просмотреть файл

@ -0,0 +1,5 @@
---
"@rnx-kit/metro-serializer-esbuild": patch
---
Fix source maps not pointing to source

Просмотреть файл

@ -152,6 +152,15 @@ use environment names. See the full documentation for a list of supported names.
Defaults to `hermes0.7.0`.
### `sourceMapPaths`
Determines whether paths in the output source map are absolute or relative to
the directory containing the source map.
Values: `absolute` | `relative`
Defaults to `relative`.
### `analyze`
Sets whether esbuild should output a report at the end of bundling.

Просмотреть файл

@ -7,12 +7,14 @@ import type { Dependencies, Graph, Module, SerializerOptions } from "metro";
import type { SerializerConfigT } from "metro-config";
import * as path from "path";
import * as semver from "semver";
import { absolutizeSourceMap, generateSourceMappingURL } from "./sourceMap";
export { esbuildTransformerConfig } from "./esbuildTransformerConfig";
export type Options = Pick<BuildOptions, "logLevel" | "minify" | "target"> & {
analyze?: boolean | "verbose";
fabric?: boolean;
sourceMapPaths?: "absolute" | "relative";
};
function assertVersion(requiredVersion: string): void {
@ -28,22 +30,6 @@ function escapePath(path: string): string {
return path.replace(/\\+/g, "\\\\");
}
function fixSourceMap(outputPath: string, text: string): string {
/**
* All paths in the source map are relative to the directory
* containing the source map.
*
* See https://esbuild.github.io/api/#source-root
*/
const sourceRoot = path.dirname(outputPath);
const sourcemap = JSON.parse(text);
const sources = sourcemap.sources.map((file: string) =>
path.resolve(sourceRoot, file)
);
return JSON.stringify({ ...sourcemap, sources });
}
/**
* Returns whether the specified module has any side effects.
*
@ -104,7 +90,28 @@ function isRedundantPolyfill(modulePath: string): boolean {
}
function outputOf(module: Module | undefined): string | undefined {
return module?.output?.map(({ data }) => data.code).join("\n");
if (!module) {
return undefined;
}
const jsModules = module.output.filter(({ type }) => type.startsWith("js/"));
if (jsModules.length !== 1) {
throw new Error(
`Modules must have exactly one JS output, but ${module.path} has ${jsModules.length}`
);
}
const code = jsModules[0].data.code;
const moduleWithModuleNameOnly = {
...module,
// esbuild only needs the base file name. It derives the path from the
// imported path, and appends the file name to it. If we don't trim the path
// here, we will end up with "double" paths, e.g.
// `src/Users/<user>/Source/rnx-kit/packages/test-app/src/App.native.tsx`.
path: path.basename(module.path),
};
return `${code}\n${generateSourceMappingURL([moduleWithModuleNameOnly])}\n`;
}
/**
@ -328,7 +335,10 @@ export function MetroSerializer(
if (outputPath === "<stdout>" || outputPath.endsWith(outfile)) {
result.code = text;
} else if (outputPath.endsWith(sourcemapfile)) {
result.map = fixSourceMap(outputPath, text);
result.map =
buildOptions?.sourceMapPaths === "absolute"
? absolutizeSourceMap(outputPath, text)
: text;
}
});
if (metafile) {

Просмотреть файл

@ -0,0 +1,35 @@
import type { Module } from "metro";
// @ts-expect-error No declaration file for module
import sourceMapString from "metro/src/DeltaBundler/Serializers/sourceMapString";
import * as path from "path";
const sourceMappingOptions = {
processModuleFilter: () => true,
excludeSource: false,
};
export function absolutizeSourceMap(outputPath: string, text: string): string {
/**
* All paths in the source map are relative to the directory
* containing the source map.
*
* See https://esbuild.github.io/api/#source-root
*/
const sourceRoot = path.dirname(outputPath);
const sourcemap = JSON.parse(text);
const sources = sourcemap.sources.map((file: string) =>
path.resolve(sourceRoot, file)
);
return JSON.stringify({ ...sourcemap, sources });
}
export function getInlineSourceMappingURL(modules: readonly Module[]): string {
const sourceMap = sourceMapString(modules, sourceMappingOptions);
const base64 = Buffer.from(sourceMap).toString("base64");
return `data:application/json;charset=utf-8;base64,${base64}`;
}
export function generateSourceMappingURL(modules: readonly Module[]): string {
return `//# sourceMappingURL=${getInlineSourceMappingURL(modules)}`;
}

Просмотреть файл

@ -5,7 +5,14 @@ require_relative '../../../node_modules/react-native-test-app/test_app'
workspace 'SampleCrossApp.xcworkspace'
use_flipper! false
use_test_app! do |target|
options = {
:fabric_enabled => false,
:hermes_enabled => false,
:turbomodule_enabled => false,
}
use_test_app! options do |target|
target.app do
pod 'MSAL', :modular_headers => true
end

Просмотреть файл

@ -289,11 +289,11 @@ PODS:
- ReactTestApp-DevSupport (1.6.8):
- React-Core
- React-jsi
- ReactTestApp-MSAL (1.0.4):
- ReactTestApp-MSAL (1.0.5):
- MSAL
- RNXAuth
- ReactTestApp-Resources (1.0.0-dev)
- RNXAuth (0.1.4):
- RNXAuth (0.1.5):
- React-Core
- Yoga (1.14.0)
@ -448,9 +448,9 @@ SPEC CHECKSUMS:
React-runtimeexecutor: 8cdd80915ed6dabf2221a689f1f7ddb50ea5e9f3
ReactCommon: 5b1b43a7d81a1ac4eec85f7c4db3283a14a3b13d
ReactTestApp-DevSupport: 477807a2c223e21ea8567c0c0a5880d561e11534
ReactTestApp-MSAL: 4fa86fda02c24009c9dd4b5647d45e0c45421f41
ReactTestApp-MSAL: 803fb430f3d9bf6bc022964c64ac3b64fea88f73
ReactTestApp-Resources: 74a1cf509f4e7962b16361ea4e73cba3648fff5d
RNXAuth: 72b9e2b11d9f56e0f0860da43732dcccad0bcacb
RNXAuth: 1f5001db635f153c400b657b91e06e9b997620e8
Yoga: 2f6a78c58dcc2963bd8e34d96a4246d9dff2e3a7
PODFILE CHECKSUM: 438d9afb225dc2a99c6750449d7231544f5ad6f3

Просмотреть файл

@ -0,0 +1,53 @@
#!/usr/bin/env node
// @ts-check
import { originalPositionFor, TraceMap } from "@jridgewell/trace-mapping";
import * as assert from "assert";
import * as fs from "fs";
const findInSourceFile = (() => {
/** @type {Record<string, string[]>} */
const sourceMap = {};
return (/** @type {string} */ sourcePath, /** @type {string} */ needle) => {
if (!sourceMap[sourcePath]) {
sourceMap[sourcePath] = fs
.readFileSync(sourcePath, { encoding: "utf-8" })
.split("\n");
}
const bundle = sourceMap[sourcePath];
const lines = bundle.length;
for (let i = 0; i < lines; ++i) {
const column = bundle[i].indexOf(needle);
if (column >= 0) {
return { line: i + 1, column };
}
}
return { line: -1, column: -1 };
};
})();
const { [2]: bundlePath } = process.argv;
if (!bundlePath || !fs.existsSync(bundlePath)) {
console.log(`usage: validateSourceMap.mjs /path/to/jsbundle`);
} else {
const sourcemap = fs.readFileSync(bundlePath + ".map", { encoding: "utf-8" });
const tracer = new TraceMap(JSON.parse(sourcemap));
// TODO: This is probably not the best way to validate source maps, but I
// couldn't find one that was up-to-date and didn't throw false positives.
[
{ source: "src/App.native.tsx", needle: "function App(" },
{ source: "src/App.native.tsx", needle: "function Button(" },
{ source: "src/App.native.tsx", needle: "function DevMenu(" },
{ source: "src/App.native.tsx", needle: "function Feature(" },
{ source: "src/App.native.tsx", needle: "function Separator(" },
{ source: "src/App.native.tsx", needle: "function useStyles(" },
].forEach(({ source, needle }) => {
assert.deepEqual(
originalPositionFor(tracer, findInSourceFile(bundlePath, needle)),
{ source, name: null, ...findInSourceFile(source, needle) }
);
});
}